Skip to content

Implement path clipping with exact Bézier curves - #1131

Merged
johannes-wolf merged 1 commit into
cetz-package:0.5.3from
hooyuser:codex/clip-path-pr-0.5.3
Sep 11, 2026
Merged

johannes-wolf merged 1 commit into
cetz-package:0.5.3from
hooyuser:codex/clip-path-pr-0.5.3

Conversation

@hooyuser

Copy link
Copy Markdown
Contributor

This PR builds on top of the path boolean work from #1091 and is the next step toward addressing #618.

It adds a new clip function to clip path drawables against a closed clipping region:

clip(
  clipping-region,
  body,
  mode: "inside",
  ...
)

Depending on mode ("inside" or "outside"), it keeps only the geometry inside or outside the clipping-region. The body parameter takes the objects you want to clip, and each path drawable inside it is clipped independently against clipping-region. Note that unlike paths in clipping-region, paths in body may contain both open and closed subpaths.

Take mode: "inside" as an example. Under the hood, the stroke and fill of each path drawable in body are handled separately:

  • If its stroke is not none: subpaths (open or closed) are trimmed to keep only the portions inside the clipping region.
  • If its fill is not none:
    • Its closed subpaths together define the filled area according to body-fill-rule. A boolean intersection between that area and the clipping-region produces a newly generated, fill-only path.
    • Its open subpaths do not participate in the Boolean operation.

mode: "outside" follows the same split: it retains the outside portions of stroked paths and uses a boolean difference for filled areas.

To avoid repeating logic from #1091, I also reorganized the relevant Typst and Rust code into shared Typst/Rust modules. This is purely an internal cleanup and doesn't change the behavior of the existing boolean function.


Example: string diagram of snake equation

Someone on Discord asked how to draw a string diagram and came up with a solution using boolean operations (discussion link). With clip, we now have a more straightforward way to achieve this kind of effect.

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

  let padded-anchor(name, position) = cetz.draw.group(name: name, {
    import cetz.draw: *

    let padding = 0.3
    anchor("default", position)
    anchor("north", (position.at(0), position.at(1) + padding))
    anchor("south", (position.at(0), position.at(1) - padding))
    anchor("east", (position.at(0) + padding, position.at(1)))
    anchor("west", (position.at(0) - padding, position.at(1)))
  })

  let path = {
    line("F_in", (4, 2))
    bezier((4, 2), "epsilon.east", (4, 3))
    bezier("epsilon.west", (2, 2), (2, 3))
    bezier((2, 2), "eta.east", (2, 1))
    bezier("eta.west", (0, 2), (0, 1))
    line((0, 2), "F_out")
  }

  on-layer(10, {
    circle((1, 1), radius: 3mm, name: "eta", fill: white)
    content((), $eta$)

    circle((3, 3), radius: 3mm, name: "epsilon", fill: white)
    content((), $epsilon$)

    padded-anchor("F_in", (4, 0))
    content("F_in.south", $F$)

    padded-anchor("F_out", (0, 4))
    content("F_out.north", $F$)

    path
  })

  let background(fill) = {
    rect(
      (-0.5, 0),
      (4.5, 4),
      stroke: none,
      fill: fill,
    )
  }

  let aqua-region = {
    merge-path(
      {
        path
        line("F_out", (-0.5, 4), (-0.5, 0))
      },
      close: true,
    )
  }

  on-layer(0, {
    background(lime)
    clip(aqua-region, background(aqua))
  })
})

cetz

@hooyuser hooyuser closed this Aug 26, 2026
@hooyuser
hooyuser deleted the codex/clip-path-pr-0.5.3 branch August 26, 2026 04:46
@hooyuser
hooyuser restored the codex/clip-path-pr-0.5.3 branch August 26, 2026 04:49
@hooyuser hooyuser reopened this Aug 26, 2026
@johannes-wolf
johannes-wolf self-requested a review August 27, 2026 18:18
@johannes-wolf johannes-wolf added this to the 0.5.3 milestone Aug 28, 2026

@johannes-wolf johannes-wolf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you a lot!

Comment thread src/draw/shapes.typ
assert.eq(args.len(), 0)
if current != () {
paths.push(path-util.make-subpath(origin, current, closed: cmd == "z"))
paths.push(path-util.make-subpath(origin, current, closed: true))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@johannes-wolf
johannes-wolf merged commit 5ba7e2d into cetz-package:0.5.3 Sep 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants