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
733 stars 34 forks source link

[bug] Previously defined coordinates shouldn't be affected by `group()`-scoped axis transformations #601

Open tapyu opened 1 month ago

tapyu commented 1 month ago

Consider the following MWE:

#import "@preview/cetz:0.2.2": canvas, plot, draw, coordinate, vector
#set page(width: auto, height: auto, margin: .5cm)

#canvas({
  draw.rect((0,0), (1,1), name: "rect")
  let test = (rel: (0deg, 2), to: "rect.north-east")
  draw.group({
    // draw.rotate(45deg, origin: test)
    draw.circle(test, radius: (0.7, 0.2), anchor: "center")
    draw.get-ctx(ctx => {
      let (ctx, a) = coordinate.resolve(ctx, test)
      draw.content(test, [#a], frame: "rect", stroke: none, fill: white)
    })
  })
})

This produces

image

The command draw.rotate(45deg, origin: test) shouldn't affect test since the rotation is scoped within group() and rotate() is being applied after test is defined. Therefore, uncommenting rotate() should lead to a rotation in circle() around its center.

However, by uncommenting it, we somehow obtain

image

This new (and wrong) position is the because rotate() is changing the x-y axis used to determine test, which shouldn't happen.

tapyu commented 1 month ago

From the manual

Elements after the group are not affected by the changes inside the group.

Well, maybe I misunderstood, but group() should also prevent changes from being applied in elements before the group, no? Otherwise, how could I use test within group() if I want to rotate it?

tapyu commented 1 month ago

In fact, I can retrieve test by using resolve() and them using the resolved coordinates

draw.get-ctx(ctx => {
      let (ctx, a) = coordinate.resolve(ctx, test)
      draw.content(test, [#a], frame: "rect", stroke: none, fill: white)
      draw.rotate(45deg, origin: a)
      draw.circle(a, radius: (0.7, 0.2), anchor: "center")
    })

However, I still don't know if this is the expected behavior. Please, let me know if this is indeed a bug or not

johannes-wolf commented 1 month ago

I don't really get what you mean, it looks correct to me. What are you expecting?

tapyu commented 1 month ago

I was expecting that

#import "@preview/cetz:0.2.2": canvas, plot, draw, coordinate, vector
#set page(width: auto, height: auto, margin: .5cm)

#canvas({
  draw.rect((0,0), (1,1), name: "rect")
  let test = (rel: (0deg, 2), to: "rect.north-east")
  draw.group({
    draw.rotate(45deg, origin: test)
    draw.circle(test, radius: (0.7, 0.2), anchor: "center")
    draw.get-ctx(ctx => {
      let (ctx, a) = coordinate.resolve(ctx, test)
      draw.content(test, [#a], frame: "rect", stroke: none, fill: white)
    })
  })
})

produces

image

but it produced

image

The whole point is that rotate() is affecting the value of test, and I wasn't expecting that since test is defined outside group().

johannes-wolf commented 1 month ago

It is def. broken.

johannes-wolf commented 1 month ago

Oh, I am confused. I think it is correct.

johannes-wolf commented 1 month ago

The problem here is, that test gets resolve with the rotation active. You've got two solutions:

group({
  anchor("tmp", test) // Force resolve coordinate first
  rotate(45deg)
  circle(...)
})

Or use set-origin first:

group({
  set-origin(test)
  rotate(45deg)
  circle((0,0), ...) // At (0,0)
})

Tikz has \begin{scope}[rotate around], so it can handle this case. Maybe we want something similar for our transformations?

tapyu commented 1 month ago

The problem here is, that test gets resolve with the rotation active.

This behavior is counterintuitive. test is set before any rotation is performed. How/why does test gets resolved with the rotation active?

Tikz has \begin{scope}[rotate around], so it can handle this case. Maybe we want something similar for our transformations?

If we agree that it is sensible that test should not be affected by rotate(), we don't need to do anything as draw.rotate(45deg, origin: test) would work as expected. I believe the is the least confusing and simplest behavior one wants to achieve. After all, if we want that test to be affect by the rotation, we can simply define it after rotating the axis:

draw.rotate(45deg, origin: (rel: (0deg, 2), to: "rect.north-east")) // rotation
let test = (rel: (0deg, 2), to: "rect.north-east") // defining `test` on the rotated axis
johannes-wolf commented 1 month ago

It is set before any rotation, but test is just an array which represents a cetz coordinate. Because of how Typst/cetz work, it is not possible to resolve it without calling to cetz. If you create an anchor, that anchor gets resolved where it is declared.

It would work if you call let (_, test) = coordinate.resolve(...).

For cetz to be able to resolve and return coordinates, Typst would need to allow to set some sort of writable global context, which it does not.

tapyu commented 1 month ago

:(

johannes-wolf commented 1 month ago

I think the best thing to do is allowing anchors at the root scope (without a group). That would allow defining coordinates with the current transformation taken into account.

tapyu commented 1 month ago

I am not sure how your idea would work. I just tried to help Cetz saying that it is reasonable test isn't changed by transformations made after its definition. If it is not possible due to Typst's internal behavior, I don't know how would be the solution.

However, I assure that creating anchors like anchor("tmp", test) to get arrays resolved where it declared is more a workaround than a good solution. Beginners will have a hard time to figure it out.

tapyu commented 1 month ago

Hi there! A potentially interesting aspect about this issue:

#import "@preview/cetz:0.2.2": canvas, draw, coordinate
#import draw: *
#set page(width: auto, height: auto, margin: .5cm)

#canvas({
  rect((0,0), (1,1), name: "rect")
  let test = "rect.north-east"
  group({
    rotate(45deg)
    circle(test, radius: (0.7, 0.2), anchor: "center")
    get-ctx(ctx => {
      let (ctx, a) = coordinate.resolve(ctx, test)
      content(test, [#a], frame: "rect", stroke: none, fill: white)
    })
  })
})

image

To my surprise, removing the relative shift in test leads to the expected position. So AFAIU the issue isn't that the array or the anchor itself. Rather, it is due to the fact that the context used inside (where we rotate) and outside (where we don't want that the rotation happens) group() is the same.

Please, let me try to propose a solution for this: isn't possible to work with two contexts? One to apply any transformation inside and the other for anything outside group(). In this way, test (or anything outside group()) would be resolved by CeTZ with the nonrotated context where it was defined.