Draw according to distance in Cetz

I am using Cetz to make a 3d diagram of of spheres and balls. I am using the ortho function to project the diagram and make it appear 3d. The problem is that Cetz doesn’t draw the elements in the correct order. What I mean is that circles that appear behind disks are actually in front of the disk. I understand that this happens because the sorted argument of the ortho function takes the maximum z-coordinate of any element and draws it according to that. This is problematic in multiple ways. First, this results in incorrect perspectives for elements that are large and are both behind and in front of other elements in certain parts. Second, this only respects the depth with respect to the z-coordinate, although the much more natural approach would of course be the depth with respect to the “camera” so to say and would then depend on the angles of the orthograhpic projection. Below is the Typst-code that I am having trouble with:

#cetz.canvas({
  import cetz.draw: ortho,on-xy,on-xz,on-yz,content,set-style,stroke,on-layer,rect,rotate
  set-style(content: (frame: "circle", padding: 0cm, fill: white, stroke: none))
  ortho(x: 15deg, y: -20deg, z: 00deg, sorted: true,{
    on-xz(y: 0,{
      cetz.draw.circle((0,0), radius: 2, stroke: blue, fill: white)
    })
    on-layer(1,
      content((0,2,0), circle(radius: 0.1cm, fill: blue, stroke: none)),
    )
    on-xy(z: 0,{
      cetz.draw.circle((0,0), radius: 2, stroke: (dash: "dashed", paint: red))
    })
    on-yz(x: 0,{
      cetz.draw.circle((0,0), radius: 2, stroke: (dash: "dashed", paint: red))
    })
  })
})

My solution right now is to split each element up into smaller peaces such that the maximum z-coordinate actually corresponds to their distance to the camera. But this is very ugly and has some unfortunate side effect:

#cetz.canvas({
  import cetz.draw: ortho,on-xy,on-xz,on-yz,content,set-style,stroke,on-layer,rect,rotate
  set-style(content: (frame: "circle", padding: 0cm, fill: white, stroke: none))
  ortho(x: 15deg, y: -20deg, z: 00deg, sorted: true,{
    on-xz(y: 0,{
      let pat = tiling(size: (10pt, 10pt))[
        #place(line(start: (100%, 0%), end: (0%, 100%), stroke: 0.5pt))
      ]
      rotate(z: 45deg)
      rect((-1.414,-1.414),(1.414,1.414), fill: white, stroke: none)
      rotate(z: -45deg)
      // cetz.draw.circle((0,0), radius: 2, stroke: blue, fill: white)
      cetz.draw.arc((0,0), start: 0deg, stop: 90deg, radius: 2, stroke: blue, fill: white, anchor: "origin")
      cetz.draw.arc((0,0), start: 90deg, stop: 180deg, radius: 2, stroke: blue, fill: white, anchor: "origin")
      cetz.draw.arc((0,0), start: 180deg, stop: 270deg, radius: 2, stroke: blue, fill: white, anchor: "origin")
      cetz.draw.arc((0,0), start: 270deg, stop: 360deg, radius: 2, stroke: blue, fill: white, anchor: "origin")
    })
    on-layer(1,
      content((0,2,0), circle(radius: 0.1cm, fill: blue, stroke: none)),
    )
    on-xy(z: 0,{
      // cetz.draw.circle((0,0), radius: 2, stroke: (dash: "dashed", paint: red))
      cetz.draw.arc((0,0), start: 0deg, stop: 90deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
      cetz.draw.arc((0,0), start: 90deg, stop: 180deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
      cetz.draw.arc((0,0), start: 180deg, stop: 270deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
      cetz.draw.arc((0,0), start: 270deg, stop: 360deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
    })
    on-yz(x: 0,{
      // cetz.draw.circle((0,0), radius: 2, stroke: (dash: "dashed", paint: red))
      cetz.draw.arc((0,0), start: 0deg, stop: 90deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
      cetz.draw.arc((0,0), start: 90deg, stop: 180deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
      cetz.draw.arc((0,0), start: 180deg, stop: 270deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
      cetz.draw.arc((0,0), start: 270deg, stop: 360deg, radius: 2, stroke: (dash: "dashed"), anchor: "origin")
    })
  })
})
1 Like