Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rotations about the Z-axis #274

Closed
matthew-e-brown opened this issue Oct 23, 2023 · 2 comments · Fixed by #275
Closed

Rotations about the Z-axis #274

matthew-e-brown opened this issue Oct 23, 2023 · 2 comments · Fixed by #275
Labels
bug 🐛 Something isn't working core:draw ✏️

Comments

@matthew-e-brown
Copy link
Contributor

There is something I want to bring up about my original implementation of arc-through from #206. This was originally going to just be a comment on #273, but after typing so much I figured it was worth posting as a full issue.

#let start = {
    let (x, y, ..) = vector.sub(a, center)
    calc.atan2(x, y) // Typst's atan2 is (x,y) order!!
}

This part was meant to be only temporary; it's supposed to figure out the angle between a and the $x$-axis when center is taken as the origin. What this should be is just a call to vector.angle2(center, a); but when I tried that, I found that my angles were always backwards. That was when I discovered that Typst's atan2 is $(x, y)$ instead of $(y, x)$, like most other atan2 implementations. vector.angle2, which uses calc.atan2, passes its arguments as $(y, x)$ instead of Typst's $(x, y)$, which is why I wrote that little custom block with that comment (it also subtracts a - b instead of b - a, then adds 90 degrees, which I presume was done to account for the incorrect argument order).

Had I gotten around to making a full PR, I had intended to fix this as part of it. So, when I saw #273, I figured I would go ahead and do that little change as a separate one and then request to merge into this branch. But... I've managed to confuse myself.


To cut to the chase: in which direction should rotations around the $z$-axis be done?

Is this 60°, or is this −60°? My intuition when working in two dimensions is that it's 60°, like how the unit circle works. That is, I expected a right-hand coordinate system, where counterclockwise is positive. So, 60° is what I'd expect atan2(x: 1/2, y: sqrt(3)/2) to spit out:

>>> Math.round(Math.atan2(Math.sqrt(3)/2, 1/2) * 180/Math.PI) // JS is (y, x)
60

There is no clear picture, as far as I can discern, for what CetZ uses, though:

(diagram generated with Typst v0.8 and CetZ 2ed733a)

(code is kind of messy, but here it is)

#import "/src/lib.typ" as cetz

#set page(width: auto, height: auto, margin: 12pt)

#cetz.canvas(length: 2.5cm, {
  import cetz.draw: *

  let origin = (0, 0)
  let point = (1/2, calc.sqrt(3)/2)
  let label = $ (1/2, sqrt(3)/2) $
  let angle = cetz.vector.angle2(origin, point)

  // Unit circle and origin dot
  circle(origin, radius: 1, stroke: gray)
  on-layer(2, { circle(origin, radius: 2pt, fill: black) })

  // Axes
  line((-1.5, 0), (1.5, 0), mark: (end: ">", stroke: black, fill: black), name: "x-axis")
  content((), padding: 0.5em, anchor: "left", $x$)
  line((0, -1.5), (0, 1.5), mark: (end: ">", stroke: black, fill: black), name: "y-axis")
  content((), padding: 0.5em, anchor: "bottom", $y$)

  // Arrow through point, label, and intersection dot
  line(origin, ((), 1.5, point), stroke: red, mark: (end: ">", stroke: red, fill: red))
  content((), anchor: "left", padding: 8pt, $#raw("p") = (1/2, sqrt(3)/2)$)
  circle(point, radius: 2pt, fill: black)

  // Angle and theta
  cetz.angle.angle(origin, (1, 0), point, radius: 1/3, stroke: red)
  content(
    (origin, 0.5, -30deg, point), anchor: "left",
    frame: "rect", fill: white, stroke: none, padding: 3pt,
    text(fill: red, $ #raw("vector.angle2(origin, p)") = #angle $)
  )

  // 45 degree blue line
  line(origin, (origin, 1.5, 45deg, point), stroke: blue)
  circle((), radius: 1pt, fill: black)
  content((), anchor: "right", padding: 5pt, text(fill: blue, `(origin, 1.5, 45deg, p)`))

  // -85 degree blue line
  line(origin, (origin, 1.5, -85deg, point), stroke: blue)
  circle((), radius: 1pt, fill: black)
  content((), anchor: "left", padding: 5pt, text(fill: blue, `(origin, 1.5, -85deg, p)`))

  // purple line
  group(name: "rotated", {
    rotate(120deg)
    line(origin, (1.5, 0), stroke: purple)
    circle((), radius: 1pt, fill: black, name: "dot")
    copy-anchors("dot")
  })
  content(
    "rotated", anchor: "right", padding: 5pt,
    text(fill: purple)[`(1.5, 0.0)` after `rotate(120deg)`]
  )

  // Arcs
  arc((-1, 0), start: 0deg, stop: -60deg, stroke: olive, name: "arc")
  circle("arc.end", radius: 1pt, fill: black)
  content(
    (), anchor: "right", padding: 5pt,
    text(fill: olive)[`arc((-1,0), start: 0deg, stop: -60deg)`]
  )

  arc((-1, 0), start: 0deg, stop: 60deg, stroke: olive, name: "arc")
  circle("arc.end", radius: 1pt, fill: black)
  content(
    (), anchor: "right", padding: 5pt,
    text(fill: olive)[`arc((-1,0), start: 0deg, stop: 60deg)`]
  )

  // Dot for arcs
  circle((-1, 0), radius: 2pt, fill: black)
})

  • For the current implementation of vector.angle2, counterclockwise is negative (left-handed; red).
  • For the Interpolation coordinate mode's angle parameter, counterclockwise is positive (right-handed; blue).
  • For the rotate transformation function, clockwise is positive (left-handed; purple).
  • For arcs start, stop, and delta parameters, counterclockwise is positive (right-handed; olive).

The arc one being different from vector.angle2 is the problem I initially ran into, and what prompted that custom start block in my original arc-through function from #206. The rotate one could make sense either way: technically, the coordinate space was rotated by 120°, my line was drawn, and then it was rotated back. But that means that a call to rotate(120deg) rotated my shapes by -120°, which feels counterintuitive.

Like I alluded to, I started working on a PR to fix this: johannes-wolf:2ed733a...matthew-e-brown:feb07cc. I "fixed" vector.angle2 to work in-line with the trigonometric functions, and subsequently "fixed" the few spots that used vector.angle2. But then when making my MWE and writing my PR, I noticed that rotate—which didn't depend on angle2—also happened to be left-handed. I could "fix" the rotate function to also be right-handed, but because the transformation matrices feel like the "standard" way to do rotations, that felt a little drastic. I suddenly became unsure: what is the intended direction of rotation for CetZ?


I have been staring at matrices and curves and arcs, twisting my left and right hands around in the air for so long now that I could very well just be getting my angles in a knot. I wouldn't be too surprised if it turns out this is all completely intended behaviour and I've missed something silly. I hope not! 😆 I'll open a draft PR with my (potential) fixes for now. Once I know which way things are supposed to spin, I can correct the last few discrepancies and mark it as ready. Or, I can close it if I got things the wrong way.

One last thing I want to note is that Typst's native rotate function is left-handed; so clockwise is positive. Not that CetZ has to use the same system, but it certainly didn't help me come to any conclusions.

Sorry 🍁 this issue's so long. 😅

@fenjalien
Copy link
Member

Huh we really messed that up! Rotations should be positive going anticlockwise (which follows your intuition right?) so its the same as tikz. Also its okay that cetz's rotate is opposite to typst's as cetz uses up as positive but typst uses down.

@johannes-wolf johannes-wolf added bug 🐛 Something isn't working core:draw ✏️ labels Oct 23, 2023
@matthew-e-brown
Copy link
Contributor Author

Yes, that follows my intuition! Okay, I'm glad I wasn't going crazy. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something isn't working core:draw ✏️
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants