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

arc: Add a function `arc-through` #206

Closed johannes-wolf closed 9 months ago

johannes-wolf commented 11 months ago

That function should connect two points with an arc touching the third point (see circle-through).

fenjalien commented 11 months ago

We could allow passing 3 positional arguments in the current arc function? When 3 are dectected it draws an arc through them. We could extend this to circle-through

johannes-wolf commented 11 months ago

I think overloading existing functions too much only brings complexity with no real benefit. I would favor having the extra *-through functions.

fenjalien commented 11 months ago

what would be best is allowing dot access notation on the element function. for example circle(...) and circle.through(...). But this can't be done until typst structs or custom elements are implemented. So I guess *-through would be okay... In terms of complexity though, they could be implemented by transforming the three points into a radius and position and calling the primative element function.

johannes-wolf commented 11 months ago

Yes, if the dot syntax becomes available we can switch to that.

matthew-e-brown commented 11 months ago

Would you two be okay with it if I had a go at implementing this? I know you're in the middle of a rework, so I feel like I should ask before making a PR.

I've already gotten the basics working using what CetZ provides out of the box. I would just need to convert it to the same style that the other functions use, where they return that fancy dictionary with custom-anchors and whatnot.

#let arc-through(a, b, c, ..args) = cetz.draw.get-ctx(ctx => {
    import cetz.vector
    import cetz.coordinate

    let a = coordinate.resolve(ctx, a)
    let b = coordinate.resolve(ctx, b)
    let c = coordinate.resolve(ctx, c)

    let center = coordinate.util.calculate-circle-center-3pt(a, b, c)
    let radius = vector.dist(center, a)
    let start = {
        let (x, y, ..) = vector.sub(a, center)
        calc.atan2(x, y) // Typst's atan2 is (x,y) order!!
    }
    let delta = vector.angle(a, center, c)

    cetz.draw.arc(a, ..args, start: start, delta: delta, radius: radius, anchor: "start")
})

arc-through

Let me know what you think. I'll hold off if it should wait until the rework is complete.


Related to arcs/curves, I'd also like to take a stab at #239 at some point, if that isn't going to be part of the rework, as well... It just so happens that my latest Algorithms assignment fits really well with drawing curved arrows 😅

johannes-wolf commented 11 months ago

Thank you for the work! There are some edge cases where your implementation did not work correctly. We have to check on which side of a-c, b and center lay and swap the delta if needed. I hope this version works as expected.

#import "@preview/cetz:0.1.2"
#set page(width: auto, height: auto)

#let side-on-line(a, b, pt) = {
  let (x1, y1, ..) = a
  let (x2, y2, ..) = b
  let (x,  y, ..)  = pt
  return (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1)
}

#let arc-through(a, b, c, ..args) = cetz.draw.get-ctx(ctx => {
    import cetz.vector
    import cetz.coordinate

    let a = coordinate.resolve(ctx, a)
    let b = coordinate.resolve(ctx, b)
    let c = coordinate.resolve(ctx, c)

    let center = coordinate.util.calculate-circle-center-3pt(a, b, c)
    let radius = vector.dist(center, a)
    let start = {
        let (x, y, ..) = vector.sub(a, center)
        calc.atan2(x, y) // Typst's atan2 is (x,y) order!!
    }
    let delta = vector.angle(a, center, c)

    let center-is-left = side-on-line(a, c, center) < 0
    let b-is-left = side-on-line(a, c, b) < 0

    // If the center and point b are on the same side of a-c,
    // the arcs delta must be > 180deg
    if center-is-left == b-is-left {
      delta = 360deg - delta
    }

    // If b is left of a-c, swap a-c to c-a by using a negative delta
    if b-is-left {
      delta *= -1
    }

    cetz.draw.content(center, [#delta])
    cetz.draw.arc(a, ..args, start: start, delta: delta, radius: radius, anchor: "start")
})

#let test(a, b, c) = {
  import cetz.draw: *
  group({
    anchor("default", (0,0))
    circle(a, radius: .1, fill: green)
    circle(b, radius: .1, fill: yellow)
    circle(c, radius: .1, fill: red)
    arc-through(a, b, c, name: "a")
  }, name: "g", anchor: "left")

  on-layer(-1, rect("g.bottom-left", "g.top-right", stroke: none, fill: gray.lighten(80%)))
  set-origin((rel: (1,0), to: "g.right"))
}

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

  test((0,0), (1,1), (2,0))
  test((0,0), (1,-1), (2,0))
  test((0,-1), (-1,0), (0,1))
  test((0,-1), (1,0), (0,1))
  test((0,0), (1,3), (4,0))
  test((0,0), (1,3), (4,3))
  test((1,0), (2,3), (0,3))
  test((0,0), (1,-3), (4,0))
  test((0,0), (1,-3), (4,3))
})

grafik

You can create a PR, but I would wait to merge it until the rework has been merged.