cetz-package / cetz

CeTZ: ein Typst Zeichenpaket - A library for drawing stuff with Typst.
https://cetz-package.github.io
GNU Lesser General Public License v3.0
885 stars 36 forks source link

Rotations about the Z-axis #274

Closed matthew-e-brown closed 11 months ago

matthew-e-brown commented 1 year ago

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) ```typst #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) }) ```

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: https://github.com/johannes-wolf/cetz/compare/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 commented 1 year ago

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.

matthew-e-brown commented 1 year ago

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