#[CeTZ] Arrow between 2 nodes

73 messages · Page 1 of 1 (latest)

tough dragon
#

I want to draw an arrow between 2 content nodes, such that the arrow just touches the edges of the 2 nodes.

The TikZ equivalent is:

\node[draw] (A) at (0,0) {A};
\node[draw] (B) at (1,0) {B};
\draw[->] (A) -- (B);

My CeTZ code:

import draw: *
set-style(mark: (size: .05))

content(name: "A", (0,0), trect[A])
content(name: "B", (1,0), trect[B])
line("A", "B", mark: (end: ">"))
#

What I get:

#

What I want:

night lake
#

you can use anchors like this

flat bane
#

we don't use the nodes concept, everything is an "element". Referencing elements by name in coordinates picks a default anchor, here its center

night lake
#

?render

#import "@preview/cetz:0.1.1"
#let trect = rect

#cetz.canvas({
  import cetz.draw: *
  // set-style(mark: (size: .05))
  
  content(name: "A", (0,0), trect[A])
  content(name: "B", (2,0), trect[B])
  line("A.right", "B.left", mark: (end: ">"))
})
flat bane
#

you can use "left" and "right" anchor names as shown lol

night lake
#

dunno how to fix the ever so slight overlap of the arrow mark

flat bane
night lake
#

ah epic

tough dragon
#

Nice thank you!

#

But then for circles, is there a way to have nice arrows if we use the corner anchors? Like what is done in TikZ if we simply do \draw[->] (A) -- (B);

#

?render

#import "@preview/cetz:0.1.1"
#let tcircle = circle

#cetz.canvas({
  import cetz.draw: *
  
  content(name: "A", (0,0), tcircle[A])
  content(name: "B", (2,1), tcircle[B])
  line("A.top-right", "B.bottom-left", mark: (end: ">"))
})
flat bane
#

do you mean the arrow should be touching the circles?

#

if so no, at least not with the circles as content

tough dragon
#

Yes this is what I mean. I understand from what you say that it may be possible using the "circle" of cetz, but I don't think it is possible to have text inside, is it?

flat bane
#

you could place content on top of the circle

night lake
#

?render

#import "@preview/cetz:0.1.1"

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

  circle(name: "A", (0,0), radius: 0.5)
  circle(name: "B", (3,2), radius: 0.5)
  
  content("A.center", [A])
  content("B.center", [B])
  
  line("A.top-right", "B.bottom-left", mark: (end: ">"))
})
night lake
#

even then they dont touch when using the top-* or bottom-* anchors, ig anchors use a bounding box then?

tough dragon
high glacier
#

The circle anchors are currently the bounding box of the circle. Don't know if this is by design or will be fixed, but for now, just use a relative coordinate to set the points of the line:

#

?render ```
#import "@preview/cetz:0.1.1"

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

circle(name: "A", (0,0), radius: 0.5)
circle(name: "B", (3,2), radius: 0.5)

content("A.center", [A])
content("B.center", [B])

line(
(rel:(45deg, .5), to:"A"),
(rel:(225deg, .5), to:"B"), mark: (end: ">"))
})

viral patrol
#

No no no… there is a better way of doing that (see the tree gallery example):

line((a: a, number: .6, abs: true, b: b), (a: b, number: .6, abs: true, b: a), mark: (end: ">", start: ">"))

The .6 is the radius of the circle. abs: true makes .6 get interpreted as an absolute distance (vs. the shorthand relative distance syntax).

#

?render

#import "@preview/cetz:0.1.1"

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

  circle(name: "A", (0,0), radius: 0.5)
  circle(name: "B", (3,2), radius: 0.5)

  content("A.center", [A])
  content("B.center", [B])

  line((a: "A", number: .5, abs: true, b: "B"), (a: "B", number: .5, abs: true, b: "A"), mark: (end: ">", start: ">"))
})
high glacier
viral patrol
#

Yes, they should be 🙂

viral patrol
#

I opened a PR for this.

tough dragon
#

Thank you everyone, but again, I am not sure these techniques would handle well long or multi-line text. I would like a solution that is content-agnostic. As of right now, the TikZ way to do it is simpler to me:

\node[draw, circle] (A) at (0,0) {arbitrary text};
\node[draw, circle, align=center] (B) at (3,2) {arbitrary\\text};
\draw[->] (A) -- (B);
#

?render

#import "@preview/cetz:0.1.1"

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

  circle(name: "A", (0,0), radius: 0.5)
  circle(name: "B", (3,2), radius: 0.5)

  content("A.center", [arbitrary text])
  content("B.center", [arbitrary\ text])

  line((a: "A", number: .5, abs: true, b: "B"), (a: "B", number: .5, abs: true, b: "A"), mark: (end: ">"))
})
tough dragon
#

Ok so apparently, using frame: "circle" on a content element, I can have the "best" of both worlds, however I still find it a bit ugly. It would be nice to have an arrow that follows the line passing through the center of both circles.

#

?render

#import "@preview/cetz:0.1.1"

#cetz.canvas({
  import cetz.draw: *
  
  content((0,0), [arbitrary text], name: "A", frame: "circle")
  content((3,2), [arbitrary\ text], name: "B", frame: "circle")
  line("A.top-right", "B.bottom-left", mark: (end: ">"))
})
viral patrol
#

A somewhat better looking version you can get with the current cetz:
?render ```
#import "@preview/cetz:0.1.1"

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

place-anchors(name: "A", {
content((0,0), [arbitrary text], frame: "circle")
}, (pos: .15, name: ">"))

place-anchors(name: "B", {
content((3,2), [arbitrary\ text], frame: "circle")
}, (pos: .65, name: ">"))
line("A.>", "B.>", mark: (end: ">"))
})

tough dragon
#

Trying to display your code (I don't have access to a computer right now)

#

?render

#import "@preview/cetz:0.1.1"

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

  place-anchors(name: "A", {
    content((0,0), [arbitrary text], frame: "circle")
  }, (pos: .15, name: ">"))
  
  place-anchors(name: "B", {
    content((3,2), [arbitrary\ text], frame: "circle")
  }, (pos: .65, name: ">"))
  line("A.>","B.>", mark: (end: ">"))
})
delicate hamletBOT
tough dragon
#

Ok I cannot render the code on Discord but on typst.app it works well

#

However every time you have to specifiy the position of the anchor on the circle, which I don't find optimal.

#

I think that it is a bit of a shame that line("A", "B", mark: (end: ">")) does not yield the same result as the TikZ examples I gave. Would the CetZ developers accept a PR that redefines the behaviour of line("A", "B", mark: (end: ">")) to match that of TikZ, i.e., it draws an arrow that touches the edges of the 2 circles and that follows the line passing through the centers?

#

If some people still want to get the old behaviour of the arrow going from one center to the other, they can still do line("A.center", "B.center", mark: (end: ">")), so everyone is happy

tough dragon
#

A line takes two coordinates, however if these coordinates are given as names (eg. "A", "B"...), these coordinates are resolved before passing to the line() function, so it poses a problem if you want to draw an arrow that just touches the edges of circle A and B, because it would require the resolution to be done inside the line function (to compute the coordinates depending on the circles' radius and the angle of the line), am I correct?

viral patrol
#

I am the author of cetz (+ @flat bane), and yes, this is currently not very easy to implement. Cetz does not really know of nodes but only anchors. You would have to save nodes by their name (which is easy) and then find the intersection between a nodes shape and the shape of the arrow (also not hard). The functions for path intersection exist. Current way cetz interprets node names is by resolving them to their default anchor.

#

I have no time right now to implement it, but I am happy to accept a PR.

#

Although I am not sure if the tikz syntax (using node names) is the best way to do it in cetz, as it introduces a lot of special paths. (Does this work for all elements in tikz? Like for rectangles?)

#

An easy way would be implementing it as a function, i.e. connect(a, b, ..style), that connects two paths using a straight line.

tough dragon
viral patrol
#

In the current version of cetz, there is a bug regarding intersections(..) with content.

tough dragon
#

That's nice, it does nearly all the work. Do you know when it will be merged?

viral patrol
#

It is merged… but I am not sure when to release a new cetz version.

#

Or are you using a local installation?

tough dragon
#

I am using both

#

Local installation and typst.app

tough dragon
#

Ok so here is the connect() function that I made and that does what I want. This function connects 2 nodes, node_a and node_b, by an arrow that follows the line between the 2 node centers, but that just touches the edges of the 2 nodes.

#let connect(node_a, node_b) = {
  import draw: *
  let name_a = node_a.at(0).name
  let name_b = node_b.at(0).name
  let name_intersections_set = name_a + "-" + name_b + "--intersections"
  intersections(name: name_intersections_set, {
    node_a
    node_b
    line(name_a, name_b, stroke: none)
  })
  line(name_intersections + ".0", name_intersections + ".1", mark: (end: ">"))
}

Example:

#let test = canvas({
  import draw: *
  let a = content((0, 0), [Das ist\ ein Text!], frame: "circle", name: "a")
  let b = content((3, 2), [Hallo!], frame: "circle", name: "b")
  connect(a, b)
})
tough dragon
#

I will try to see how this new feature could be integrated in the current CeTZ codebase, but I'm afraid to do a PR that goes against the design principles of the library. In my head, this feature could be integrated to the existing line() function, but currently, the connect() function takes in parameters nodes and not strings (for node/anchor names). But having a line() function that takes both nodes and strings in parameters would result in ugly code IMHO. However, I don't know how to do otherwise, because we have to pass the nodes (and not the names) to the intersections() function. I would be happy to learn how to retrieve the node based on its name if we do not have access to the context.

#

On another note, it's funny to observe that both TikZ and CeTZ were made by German guys

supple fiber
#

?render

#import "@preview/cetz:0.1.1"

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

  content((0,0), [arbitrary text], name: "A", frame: "circle")
  content((3,2), [arbitrary\ text], name: "B", frame: "circle")

  line((a: "A", number: .5, abs: true, b: "B"), (a: "B", number: .5, abs: true, b: "A"), mark: (end: ">", start: ">"))
})
delicate hamletBOT
supple fiber
#

oh right

#

don't have the radius

#

yeah fine