How can I use hobby-to-cubic from the API in CetZ?

I would like to use the hobby-to-cubic function mentioned in the CetZ documentation here.

Various LLMs suggest using code like the following with cetz.hobby.hobby-to-cubic:

#import "@preview/cetz:0.5.0"

#let my-points = ((0, 0), (1, 2), (2, 0))
// Calling the function to get Bézier segments
#let segments = cetz.hobby.hobby-to-cubic(
my-points, 
close: true
)

However, I just get the error message that:

module `cetz` does not contain `hobby`

In the absence of documented examples of how to use functions from the API, it is hard to know how to proceed, hence my question in this forum.

1 Like

The function hobby.hobby-to-cubic is private API and not exposed to consuming packages.

If you want to draw a hobby-curve, use cetz.draw.hobby(...).

Thanks for the quick reply! It’s strange to see it in the manual given that it is an internal-only function and not available for use.

However, I should say the actual goal I had was in calculating the outer bounds of the hobby curve that I had created in order to have a grid that it completely fits inside.

Because a hobby curve can go outside the boundaries of the control points, it’s not just a matter of calculating the minimum and maximum values of the points provided to the hobby function.

Do you want to draw the curve at all or use cetz only for bound calculation?

Oh, those functions appearing in the manual is actually a mistake.

To measure the bounds of a hobby curve, you could draw it inside a group and the get the groups bounds:

#import "@preview/cetz:0.5.0"

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

  group(name: "curve", {
    hobby((0,0), (1,2), (4,3), (2,-2), close: true)
  })

  get-ctx(ctx => {
    let (_, a, b) = cetz.coordinate.resolve(ctx, "curve.north-west", "curve.south-east")
    let (w, h, d) = cetz.vector.sub(b, a).map(calc.round.with(digits: 2)).map(calc.abs)
    content("curve.center", [Size: $vec(#str(w), #str(h))$])
  })
})
1 Like

Thanks for this.

For my purposes, I can adapt what you’ve got to make something more like this, which gives the grid (apologies if this isn’t the most elegant way to solve the problem, but it seems to work!)

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

  group(name: "curve", {
    hobby((0,0), (1,2), (4,3), (2,-2), close: true)
  })

  get-ctx(ctx => {
    let (_, a, b, c, d) = cetz.coordinate.resolve(ctx, "curve.west", "curve.east", "curve.south", "curve.north")
    let xmin = a.at(0)
    let xmax = b.at(0)
    let ymin = c.at(1)
    let ymax = d.at(1)
    grid((xmin, ymin), (xmax, ymax))
  })
})

You can just use the “curve” group’s anchors as grid parameters:

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

  group(name: "curve", {
    hobby((0,0), (1,2), (4,3), (2,-2), close: true)
  })

  grid("curve.south-west", "curve.north-east")
})
1 Like

This is excellent - thanks! I wouldn’t have thought to try that based on the examples of grid that I had seen.

Actually I was oversimplifying in my code snippet as I wanted to have an integer grid (so if xmin was 1.8 then I would take the floor of that to start the grid at x=1, and if xmax was 7.3 I’d take the ceiling to be 8).

But maybe there is also a function that can take an anchor and “floor it” or “ceiling it”!

There is: you can pass any function as coordinate by passing a list of the function and it’s coordinate arguments: (<function>, <coordinate...>). The function the gets called with the resolved arguments.

Example with your floored/ceiled points:

#import "@preview/cetz:0.5.0"
#cetz.canvas({
  import cetz.draw: *

  group(name: "curve", {
    hobby((0,0), (1,2), (4,3), (2,-2), close: true)
  })

  let floor-point = pt => pt.map(calc.floor)
  let ceil-point = pt => pt.map(calc.ceil)
  
  grid((floor-point, "curve.south-west"), (ceil-point, "curve.north-east"))
})

Wow - this is excellent! I see now that this functionality is documented but I must admit that for a casual user of CetZ it is quite hard to work it all out.

I am hopeful that one day the LLMs can apply it all once there are more examples out in the wild.