How to fix CetZ namespace not being inherited?

I am trying to finalize and clean up some code (see Lozenge tilings) and I can’t make sense of something. I am trying to set up the function:

#let plot_lozenge_tiling(
  /// The lozenge tiling to plot, as returned by `compute_lozenge_coordinates(...)`.
  /// -> array
  tiling,
  /// Fill colors for lozenge types 0, 1, and 2 respectively.
  /// -> array
  colors: (blue.lighten(30%), red.lighten(30%), green.lighten(30%)),
  /// Whether to draw a border around each lozenge globally.
  /// -> bool
  border: true,
  /// Stroke color for lozenge borders (when `border` is `true`).
  /// -> color
  border-color: black,
  /// Stroke thickness for lozenge borders (when `border` is `true`).
  /// -> length
  border-width: 0.5pt,
  /// Whether to render each lozenge's index (its position in `tiling`) at its center.
  /// -> bool
  show-index: false,
  /// Text size for the index labels rendered when `show-index` is `true`.
  /// -> length
  index-size: 6pt,
  /// Per-lozenge style overrides, keyed by lozenge index.
  /// Keys may be integers or their string equivalents.
  /// Each value is a dictionary accepting any subset of:
  ///   - `color` (color)         β€” override fill color for this lozenge
  ///   - `border` (bool)         β€” override border visibility for this lozenge
  ///   - `border-color` (color)  β€” override border stroke color for this lozenge
  ///   - `border-width` (length) β€” override border stroke width for this lozenge
  ///
  /// Example:
  /// ```typst
  /// overrides: (
  ///   "0": (color: red),
  ///   "3": (border: false),
  ///   "7": (color: yellow, border-color: orange, border-width: 1pt),
  /// )
  /// ```
  /// -> dictionary
  overrides: (:),
  /// Debug level for the CetZ canvas.
  /// -> bool
  debug-stat: false,
  /// Content to insert on the CetZ canvas after the lozenge tiling is drawn.
  /// -> content
  overlay: none,
) = {
  cetz.canvas(debug: debug-stat, {
    import cetz.draw: *

    for (idx, lozenge) in tiling.enumerate(start: 0) {
      let ltype = lozenge.at(0)
      let verts = lozenge.at(1)

      // ── Resolve style, starting from global defaults ───────────────────────
      let fill-color = colors.at(ltype)
      let show-border = border
      let s-color = border-color
      let s-width = border-width

      // ── Apply per-lozenge overrides ────────────────────────────────────────
      // Typst stores integer dict keys as their string representation,
      // so str(idx) matches both (0: ...) and ("0": ...) in the caller.
      let ov-key = str(idx)
      if ov-key in overrides {
        let ov = overrides.at(ov-key)
        if "color" in ov { fill-color = ov.at("color") }
        if "border" in ov { show-border = ov.at("border") }
        if "border-color" in ov { s-color = ov.at("border-color") }
        if "border-width" in ov { s-width = ov.at("border-width") }
      }

      // ── Build stroke value ─────────────────────────────────────────────────
      let stroke-style = if show-border {
        (paint: s-color, thickness: s-width)
      } else {
        none
      }

      // ── Draw the lozenge as a closed quadrilateral ─────────────────────────
      line(
        verts.at(0),
        verts.at(1),
        verts.at(2),
        verts.at(3),
        close: true,
        fill: fill-color,
        stroke: stroke-style,
      )

      // ── Optionally label each lozenge with its index ───────────────────────
      if show-index {
        // Geometric centroid = average of the four vertices
        let cx = (verts.at(0).at(0) + verts.at(1).at(0) + verts.at(2).at(0) + verts.at(3).at(0)) / 4
        let cy = (verts.at(0).at(1) + verts.at(1).at(1) + verts.at(2).at(1) + verts.at(3).at(1)) / 4
        content(
          (cx, cy),
          text(size: index-size, str(idx)),
          anchor: "center",
        )
      }

      // Insert user-provided content on the canvas
      if overlay != none { overlay }
    }
  })
}

importantly using the overlay parameter to allow users to insert their own content. However, if I try and test this with

#plot_lozenge_tiling(
  test_2_coords,
  border: true,
  border-color: black,
  border-width: 1.3pt,
  colors: (lq.color.map.petroff10.at(0), lq.color.map.petroff10.at(1), lq.color.map.petroff10.at(3)),
  debug-stat: false,
  show-index: false,
  overlay: {
    circle((50*calc.cos(calc.pi/6), -25), radius: 50*calc.cos(calc.pi/6), stroke: 10pt)
  }
)

I get the error expected length or auto, found float, so I can only assume Typst reverts to its own circle function. However, the overlay content is inserted after the import cetz.draw: * line, so I don’t know why the overlay content doesn’t inherit the namespace. Can anyone clarify what’s going on?

In typst, scripts are real scripts, so passing a script means passing its result, not copying the script as some texts.

Specifically, your example is equivalent to this:

#let your-overlay = {
    circle((50*calc.cos(calc.pi/6), -25), radius: 50*calc.cos(calc.pi/6), stroke: 10pt)
}

#plot_lozenge_tiling(
  test_2_coords,
  border: true,
  border-color: black,
  border-width: 1.3pt,
  colors: (lq.color.map.petroff10.at(0), lq.color.map.petroff10.at(1), lq.color.map.petroff10.at(3)),
  debug-stat: false,
  show-index: false,
  overlay: your-overlay
)

To solve the problem, you can add an extra import cetz.draw: * to the beginning of your-overlay.

By the way, putting a none is equivalent to doing nothing. As a result, if overlay != none { overlay } can be simplified as overlay, and if … { … } else { none } can also be simplified as if … { … }.

1 Like

Okay, thanks. My thinking was that was I hoping to spare the user having to re-import the cetz.draw namespace, but it sounds like that’s unavoidable.