Diagrams/Dev/Arrows

(Difference between revisions)

diagrams-lib ought to have a module with some convenient functions for creating arrows pointing from one part of a diagram to another. This page is for gathering ideas and plans for such a module.

Contents

There should probably be a collection of "standard" arrowheads for the user to choose from. In addition it ought to be possible for the user to specify any diagram they like for use as an arrowhead.

1.1 Scale-invariance

The idea is for arrowheads to be scale-invariant, just like line width. We now have a `ScaleInv` wrapper for accomplishing this, which works correctly with freezing etc.

2 Drawing arrows

At the most basic level one could imagine an API like

`arrow :: Arrowhead -> P2 -> P2 -> Diagram`

and we probably should indeed have such a function, but we'll need to also generalize along several axes.

First, the most frequent use case will be drawing an arrow on top of an existing diagram in order to connect two points. So we want something like

`arrow :: IsName n => Arrowhead -> n -> n -> (Diagram -> Diagram)`

which draws an arrow between the named points on the given diagram. There are several ways this can be generalized:

1. Instead of drawing an arrow between the named points one could draw the arrow between the named subdiagrams (using their traces to find the edges).
2. There should also be a way to leave gaps, i.e. don't draw the arrow precisely from point to point or edge to edge, but leave a gap of a certain absolute size or a certain percentage of the arrow length on either end of the arrow.

Re: leaving gaps, this will require some generic code to shrink paths. There is already some code to shrink/extend individual segments (and compute the length of segments) in Diagrams.Segment. This would need to be extended to shrinking/extending trails and paths appropriately.

One might also want to have control over the middle of the arrow -- i.e. whether it curves, and if so how much and in which direction, etc.

3 A History of Arrow.hs

3.1 ScaleInv

The original semantics we choose for arrows was for only the shaft length to change under scaling transformations. This was done not only because we wanted the head size (from here on what ever I say about head applies to tail as well) to remain constant, but becuase non-uniorm scalings would cause the head to point in the wrong direction. See http://projects.haskell.org/diagrams/doc/manual.html#scale-invariance. It turned out that our solution (wrapping the head in the `ScaleInv` wrapper) was not enough. Once an arrow was stroked, it would not behave correctly under scales. We solved the problems of keeping the head size constant and making the arrow point in the correct direction, but now a scaling could cause the head and shaft to separate. As explained in https://github.com/diagrams/diagrams-lib/issues/112. Hence the birth of delayed subtrees.

3.2 DelayedLeaf

At the risk of stating the obvious, `Diagrams` (i.e. `DualTree`s) are created from the bottom (leaves) up, but `DTree`s and `RTrees` from the top (root) down. Suppose we are creating an arrow using diagrams functions and combinators. When we are finished we have essentially added a subtree to a diagram. That subtree cannot use any of the informtion about transforms and styles above it in the tree (they have not yet been created) so the arrow is unable to use its final size and location (i.e. accumulated transform) in its subtree. Brent's elegant solution was twofold:

1. To hold off on creating the arrow by creating a new type of leaf, the `DelayedLeaf` in addition to `Prim b v` that we already had. The `DelayedLeaf` contains a funtion `DownAnnots v -> QDiagram b v m`, in other words a set of instructions about how to create the arrow once its `DownAnnots` are known.
2. A modification of the `arrow'` function so that it returns a `DelayedLeaf`. The function wrapped in the `DelayedLeaf`, `delayedArrow` takes as parameter the `DownAnnots` and hence the styles and transforms.

The upshot is that an arrow gets to know the accumulated `DownAnnots` above it to use it's conversion to a diagram.

Since creation of a `DTree` is top down, by the time we reach a `DelayedLeaf` in the `DualTree` we already have all of the accumlated transforms and styles that will be applied to the arrow. So the `DelayedLeaf` can be expanded by running the function `DownAnnots v -> QDiagram b v m` on the accumulated `DownAnnots` and recursively calling `fromDTree` to insert the arrow into the `DTree`. It is import ant to note, though at this point the arrow is essentially finished, all transforms and style have been applied. By the way, I'm pretty sure that at this point we could have removed the `scaleInv` wrapper from the head.

(As a side benefit, we are able to use all of the styles that are applied above the arrow in the tree to do things like change the color of the entire arrow with functions like `lc red`.)

There were still some enhancements we wanted to make, https://github.com/diagrams/diagrams-lib/issues/162, but for the most part all was well in Arrow land until ...

3.3 Measure

With the death of `freeze` and the switch to `Measure` in the units branch, it seems like `headSize` should have a value of `Measure Double`. This means that `headSize` can have one of four possible units:

1. `Local` which are transformable and in particular scalable. Note that this is in contrast with our original notion of arrows having scale invariant heads. Although it is somewhat similar in behavior to a `freeze` followed by a uniform scaling.
2. `Output` this is what we have in 1.1.
3. `Global` which scales based on the final size of the diagram.
4. `Normalized` which scales in relation to a logical size.

See http://www.haskell.org/haskellwiki/Diagrams/Dev/Freezing for more detail or even better the implementation of `convert` in the units branch, https://github.com/diagrams/diagrams-core/blob/units/src/Diagrams/Core/Compile.hs#L167.

The backends should only see `Output` units therefore before passing an `RTree` to a backend, all `Measure`s must be converted to `Output`. This is done by traversing the `RTree` and returning a new `RTree` where all of the `Measure`s have been properly converted. The important thing to notice is that this occurs after the `DelayedLeaf` has been expanded and hence the arrow cannot make any use to the units change to render.

3.4 What to do

I really don't know but some things to think about are:

1. I don't think it is possible to adjust the arrow `Measure` at the `RTree` level, since arrows make no further use of units after the `delayedArrow` function is executed during the conversion to `DTree`. Unless we can come up with another way of creating arrows.
2. Does every attribute with a value of `Measure` have to implement all 4 value types? In particular is it OK for arrows to only implement `Output` and `Local`. If this is what we decide then we are done.
3. Move the conversion to `Output` units back up to the `QDiagram -> DTree` level. We tried this and decided it was a bad idea.
4. Find another way (as opposed to delayed leaves) to handle arrows.