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
819 stars 35 forks source link

`above`, `below`, and other "default" anchors are not affected by rotations #253

Closed matthew-e-brown closed 10 months ago

matthew-e-brown commented 11 months ago

I think it makes sense to put this in a separate issue.

When working on my MWE for #252, I noticed something peculiar. While the circle's top and bottom anchors are swapped, above and below seem to work fine:

Code ```typst #import "@preview/cetz:0.1.2" #set page(width: auto, height: auto, margin: 1cm) #cetz.canvas(length: 1cm, { import cetz.draw: * circle((0, 0), name: "bob", radius: 5, stroke: 1pt + black) line((0, 0), (0, 3.5), mark: (end: ">", size: 0.25, fill: red), stroke: 1pt + red) line((0, 0), (3.5, 0), mark: (end: ">", size: 0.25, fill: green), stroke: 1pt + green) content((0, 3.75), $x$) content((3.75, 0), $y$) let anchors = ( "top", "top-left", "top-right", "bottom", "bottom-left", "bottom-right", "center", "left", "right", "above", "below", ) for pos in anchors { circle("bob." + pos, radius: 0.2, fill: gray, stroke: 0.5pt + black) let (y, anchor) = if pos == "above" or pos == "below" { (+0.3, "bottom") } else { (-0.3, "top") } content( (rel: (0, y)), anchor: anchor, frame: "rect", fill: white, stroke: 1pt + gray, padding: 0.15, text(size: 0.85em, raw(pos)) ) } }) ```

This led me to snoop around and find where (I'm pretty sure) above and below are set, and I found something else.

https://github.com/johannes-wolf/cetz/blob/282e400b4e0ff583f2a3cabbffa507b94b6f53db/src/canvas.typ#L121-L154

Looking at this code, it seems to me that:

  1. Elements' default anchors are set based on their AABB.
  2. Then, above and below are derived from the default top and bottom anchors.
  3. Then, the custom-anchors-ctx function for the element gets run. In the case of circle, the top and bottom anchors are overwritten with the (currently incorrect) ones defined in _circle-anchors.
  4. Since _circle-anchors does not define above/below, they remain as they were, derived from the AABB.
  5. Then, the new custom-anchors-ctx anchors are transformed according to the current context before being inserted into the anchors list.

This results in above and below being based on the element's AABB, but top and bottom being based on the current transformation context:

Code ```typst #cetz.canvas(length: 1cm, { import cetz.draw: * rotate(+22.5deg) { circle((0, 0), name: "bob", radius: 5, stroke: 1pt + black) line((0, 0), (0, 3.5), mark: (end: ">", size: 0.25, fill: red), stroke: 1pt + red) line((0, 0), (3.5, 0), mark: (end: ">", size: 0.25, fill: green), stroke: 1pt + green) content((0, 3.75), $x$) content((3.75, 0), $y$) } rotate(-22.5deg) let anchors = ( "top", "top-left", "top-right", "bottom", "bottom-left", "bottom-right", "center", "left", "right", "above", "below", ) for pos in anchors { circle("bob." + pos, radius: 0.2, fill: gray, stroke: 0.5pt + black) let (y, anchor) = if pos == "above" or pos == "below" { (+0.3, "bottom") } else { (-0.3, "top") } // https://github.com/johannes-wolf/cetz/issues/252 -- fixed manually to demonstrate // different issue let pos = if pos == "top" { "bottom" } else if pos == "bottom" { "top" } else { pos } content( (rel: (0, y)), anchor: anchor, frame: "rect", fill: white, stroke: 1pt + gray, padding: 0.15, text(size: 0.85em, raw(pos)) ) } }) ```

In this case, it manifests as above and below; but if there were any other cases of an element returning only some anchors from custom-anchors-ctx, then any anchors not returned would remain in their default from-AABB state. The fact that most anchors do seem to get properly transformed and the comment saying, "add alternate names", both lead me to believe they are meant to be affected by rotations.

matthew-e-brown commented 11 months ago

Oh, boy... I found more anchor weirdness while fiddling with these examples. I cannot come up with an explanation for this one. When rotations are present, content elements seem to get whisked away on a magical journey.

I'd really hate to spam you with three issues back-to-back-to-back, so I'll just tack this on here...

See below: the square is translated (1, 2), then rotated 85deg, and the circle is translated (10, -5) and rotated -75deg.

All three instances of content that say "origin #" are placed directly at (0, 0) with anchor: "left", but they end up nowhere near their dots.

This effect is compounded by both the magnitude of the rotation as well as the size of the content. What's more, it seems to add a ton of extra space around the canvas. These images are uploaded exactly as they are exported from Typst, with set page(width: auto, height: auto, margin: 1cm).

When I make the content longer: Image (far too tall to paste into this comment)
When I change 85deg89deg: Image (just preposterously massive in height)

A potential hint here is that attempting to go all the way to 90 degrees causes the compiler to throw a divide-by-zero error (which is probably not desired either):

Code for this example ```typst #import "@preview/cetz:0.1.2" #set page(width: auto, height: auto, margin: 1cm) #cetz.canvas(length: 1cm, { import cetz.draw: * grid((-5, -10), (15, 5), stroke: silver) line((-5, 0), (15, 0)) line((0, -10), (0, 5)) circle((0, 0), radius: 0.1, fill: blue, stroke: blue) content((0, 0), anchor: "left", padding: 0.25)[#text(fill: blue)[origin 1, anchor left at $(0, 0)$]] translate((1, 2)) rotate(85deg) { rect((0, 0), (5, 5), stroke: 1pt + black, name: "steve") line((2.5, 2.5), (2.5, 4), mark: (end: ">", size: 0.25, fill: red), stroke: 1pt + red) line((2.5, 2.5), (4, 2.5), mark: (end: ">", size: 0.25, fill: green), stroke: 1pt + green) content((2.5, 4.35), $x$) content((4.35, 2.5), $y$) circle((0, 0), radius: 0.1, fill: orange, stroke: orange) content((0, 0), anchor: "left", padding: 0.25)[#text(fill: orange)[origin 2, anchor left at $(1, 2)$]] } rotate(-85deg) translate((-1, -2)) translate((10, -5)) rotate(-75deg) { circle((0, 0), radius: 2, name: "roger") circle((0, 0), radius: 0.1, fill: purple, stroke: purple) content((), anchor: "left", padding: 0.25)[#text(fill: purple)[origin 3, anchor left at $(10, -5)$]] } rotate(75deg) translate((-10, 5)) content("steve.top", frame: "rect", fill: white, stroke: 1pt + gray, padding: 0.15, text(size: 0.85em, `top`)) content("steve.above", frame: "rect", fill: white, stroke: 1pt + gray, padding: 0.15, text(size: 0.85em, `above`)) content("roger.top", frame: "rect", fill: white, stroke: 1pt + gray, padding: 0.15, text(size: 0.85em, `top`)) content("roger.above", frame: "rect", fill: white, stroke: 1pt + gray, padding: 0.15, text(size: 0.85em, `above`)) }) ```

Hopefully this isn't the result of me missing some line in the manual somewhere. It does say that "content itself is not transformed," but I wouldn't expect that to rocket the coordinates off into the great beyond like this.

johannes-wolf commented 11 months ago

Thank you for bringing this up. Yes I need to clean this mess up… The content positioning is indeed very strange. Will have a look.

johannes-wolf commented 11 months ago

I had added super stupid code with #108 that lead to this content positioning. Opened a fix PR.

matthew-e-brown commented 11 months ago

Awesome! You're fast. 😄

johannes-wolf commented 10 months ago

I think this can be closed because it got resolved in 0.2.0.