Jollywatt / typst-fletcher

Typst package for drawing diagrams with arrows, built on top of CeTZ.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

add support for nodes spanning multiple grid cells

PanieriLorenzo opened this issue · comments

I would like to draw nodes that can span multiple grid cells. When I change the size of a node, it makes the cells bigger rather than spanning multiple cells. I get that this behavior makes sense in a lot of cases, but sometimes I wish I could override this behavior somehow.

My specific use-case is drawing block diagrams for DSP algorithms. I sometimes have blocks with a lot of inputs and would like to draw them as parallel arrows left-to-right, like parallel "lanes", entering into a single blocks spanning multiple "lanes".

The Typst package Quill can do something similar, but it's specifically for drawing quantum circuits, and doesn't really fit my use case. Here's a diagram similar to what I'm thinking of, from Quill's docs:

image

I managed to do it using the render function, but it's still a bit of a hassle. Essentially just made a bounding box and the nodes in said box transparent.

image

It's worth mentioning that you can get around this by dividing the coordinates by the node's span, so that in the quantum circuit example the y-coordinates are all -0.33, 0, +0.33 instead of -1, 0, +1. But that's kinda a hack.

Otherwise, I could maybe add:

  • a way to opt-out of an elastic grid altogether, making fletcher.diagram essentially the same as cetz.canvas
  • a weight attribute to nodes which adjusts its strength of influence on the grid layout, so that zero weight means the row/column sizes aren't affected
  • some new kind of node that has multiple centres and spans all of them. Not sure how that would work in detail, but maybe that could also be used to create "node groups" which is a similar but separate feature request (#5)

Open to ideas:)

Seeing as my workaround is essentially what #5 describes, it sounds like a good idea :) I used cetz in the render function to draw a box around nodes with a certain color, then I made those same nodes invisible. Perhaps if it was easier to add some sort of arbitrary tag on nodes, they could be grouped by these tags?

Something like:

// ...
node((0,0), "foo", tag: "my-tag"),
node((0,1), "bar", tag: "my-tag"),
group(by: "my-tag", /* etc */),
// ...

Perhaps if it was easier to add some sort of arbitrary tag on nodes, they could be grouped by these tags?

You're in luck - version 0.4.3 introduced node names, which don't have to be unique. Of course, that only works if you weren't already giving them other names.

And I agree - since this kind of solution has come up twice now, I think it should be built in!

Here's an example:

#import "@preview/fletcher:0.4.3" as fletcher: diagram, node, edge, hide

// return the four corner points of a rectangle
#let corner-points(center, size) = {
  let (x, y) = center
  let (w, h) = size
  (
    (x - w/2, y - h/2),
    (x - w/2, y + h/2),
    (x + w/2, y - h/2),
    (x + w/2, y + h/2),
  )
}

// draw a bounding rectangle around nodes
#let enclose-nodes(nodes, ..args) = {
  let points = nodes.map(node => {
    corner-points(node.final-pos, node.size)
  }).join()
  let (center, size) = fletcher.bounding-rect(points)
  fletcher.cetz.draw.content(
    center,
    rect(
      width: size.at(0),
      height: size.at(1),
      ..args
    )
  )
}

#diagram(
  node-stroke: 1pt,
  node((0, 0), [A]),
  edge("r", "=>"),
  node((0, 1), [XYZ]),
  edge("r", "->"),
  hide({
    node((1, 0), width: 1cm, height: 1cm, name: <group>)
    node((1, 1), width: 1cm, height: 1cm, name: <group>)
  }),
  node((1,0.5), $Sigma$, stroke: none), 
  node((2, 0), [ABC]),
  edge("l", "<="),
  node((2, 1), [1234567890]),
  edge("l", "<-"),

  render: (grid, nodes, edges, options) => {
    // lookup nodes by name
    let group = nodes.filter(node => node.name == <group>)
    fletcher.cetz.canvas({
      // draw the diagram as usual
      fletcher.draw-diagram(grid, nodes, edges, debug: options.debug)
      // draw a custom group rectangle
      enclose-nodes(group)
    })
  })

that's perfect, makes it much more readable, thanks!

On the main branch, you can now do this:

#diagram(
  node-stroke: .7pt,
  edge-stroke: .7pt,
  spacing: 10pt,

  node((0,1), [volume]),
  node((0,2), [gain]),
  node((0,3), [fine]),

  edge((0,1), "r", "->", snap-to: (auto, <bar>)),
  edge((0,2), "r", "->", snap-to: (auto, <bar>)),
  edge((0,3), "r", "->", snap-to: (auto, <bar>)),

  // a node that encloses/spans multiple grid points,
  node($Sigma$, enclose: ((1,1), (1,3)), inset: 10pt, name: <bar>),

  edge((1,1), "r,u", "->", snap-to: (<bar>, auto)),
  node((2,0), $ times $, radius: 8pt),
)

…to get this:

Screen Shot 2024-04-08 at 9 00 59 PM

Future things to consider:

  • Currently node labels are always centered; for more control you have to use another node for the label.
  • Automatic snapping only happens for edges near nodes’ centers, but that isn’t as useful for nodes spanning multiple centers; you can manually control snap-to, but that’s a tiny bit annoying.

amazing thanks!