Backend programming#

The backend is the process of positioning and drawing graphical objects. This includes determining line and page breaks.

TOOD: this page still needs more examples.

Grob interfaces#

Interfaces group several grob types based on common characteristics. This is similar to how music types have music classes. All interfaces, with the grob types supporting them, are listed in the Internals Reference under Graphical Object Interfaces.

Although a grob::name function exists to return the type of a grob, recognition on interfaces should be preferred over recognition on name.

(grob::has-interface grob interface)

Check whether grob has interface (given as symbol).

Callbacks#

Understanding callbacks#

Grobs are structured around properties. The primary functions for dealing with them are similar to those for probs: ly:grob-property and ly:grob-set-property!. Grob types are listed in the Internals Reference with their associated properties and default values, under All Layout Objects.

However, grobs are not Probs. They are more feature-rich than those.

A grob property may be simply set to a sensible default. The width of a stem, for example, does usually not depend on the spacing on the page or other parameters.

By contrast, many properties are not set in advance but calculated in the backend. This holds for extents. Bar lines made with \bar ":|.|:" are substantially wider than regular bar lines. Thus, the X-extent of a BarLine grob does not have a bare number pair as a default.

There are numerous examples of properties that depend on other properties. LilyPond has no pre-conceived order for computing them – it uses a more flexible model. The approach taken is to enable any grob property to be computed on-the-fly at the moment it is read. More precisely, any grob property may be set to callback, namely a function taking the grob as parameter and returning the value for the property.

This powerful principle is at the heart of the backend and makes it very easy to extend. All of LilyPond’s default grob positioning and drawing is done in callbacks for properties such as stencil and X-extent. Conceptually, there is a big dependency graph between all callbacks, which progressively resolves itself when callbacks access properties, which triggers the callbacks those properties are set to. You simply plug your own callbacks with \override, and they are run in exactly the same way as the default callbacks are, with access to the same data (other properties).

Here is a simple callback example. It changes the color of a note head depending on the position of this note on the staff lines, as given by the staff-position property.

\version "2.25.8"

\layout {
  \context {
    \Voice
    \override NoteHead.color =
      #(lambda (grob)
         (case (ly:grob-property grob 'staff-position)
           ((-8) "red")
           ((-7) "blue")
           ((-6) "green")
           ((-5) "orange")
           ((-4) "purple")
           ((-3) "grey")
           (else "black")))
  }
}

\relative {
  \time 3/4
  a8 b cis b a4
  b fis' b,
  c8 d e d c4
  d2.
}
Output../../images/c808837bc7b52c8910e3f819bf937d58a3760f2988667742fef6ecdcf41e09c4.svg

When the NoteHead object is finally printed, or in case another callback requests its color property with ly:grob-property, the callback the property was set to is executed.

All grob properties are cached. Once the callback has run once, it is never run again. Instead, the property is considered to have been resolved to the return value of the callback, and later accesses to the same property will return the same value.

Since callbacks are run automatically, keep in mind that ly:grob-property does more than just a lookup. Suppose that Grob.property-a is set to a callback that reads Grob.property-b. If, in a callback for Grob.property-b, you access Grob.property-a with ly:grob-property, LilyPond will report a cyclic dependency since Grob.property-a and Grob.property-b depend on each other. Unlike, say, ly:context-property, which just gets the property, ly:grob-property can trigger all sorts of code, which could also have side effects such as killing your grob (as will be seen in Grob suicide).

In case you want to see what a property is really set to, without processing callbacks (which has a few applications, one of which is debugging), you can use ly:grob-property-data instead of ly:grob-property. It may return either a callback, if the property was set to a callback and has not been read yet, or the final property value, if the property either was set to this value directly, or was set to a callback but has already been read (which resolved the callback).

It is untypical to set grob properties directly with ly:grob-set-property!. Do this with care: it can introduce dependencies in the resolution order of callbacks. If callback A reads property B, and callback C sets property B, then if A comes before C, it is too early to see the new value of B, and the property A is cached, calculated from the wrong value of B. This is why writing callbacks for every property that should vary is preferable over using a single callback to set various properties.

Many predefined callbacks are associated to interfaces. This is reflected in their name, for example: ly:side-position-interface::y-aligned-side. Callbacks specific to one grob type are rather prefixed with that type, e.g., ly:note-head::calc-stem-attachment.

Using grob-transformer#

Using the grob-transformer function, a callback can be added “on top” of the preexisting value or callback of a property. Its signature is (grob-transformer property transformer), where property is the property to take the original value from, and transformer is a procedure taking two arguments, the grob and the original value.

\version "2.25.8"

sharpenBeams =
\override Beam.positions =
  #(grob-transformer 'positions
     (lambda (grob original)
       (let ((left-Y (car original))
             (right-Y (cdr original)))
         (cons
           (+ left-Y (* 2.0 (- left-Y right-Y)))
           (+ right-Y (* 2.0 (- right-Y left-Y)))))))

\relative {
  c'8[ d] e[ c] c[ g'] c'[ c,]
  \sharpenBeams
  c,8[ d] e[ c] c[ g'] c'[ c,]
}
Output../../images/b8059cd94695780ba63f5f2df5fbc494592622653c7871269679c7b075349d3b.svg

Grob pointers#

Understanding grob pointers#

There is a second kind of properties for grobs, using the accessor ly:grob-object instead of ly:grob-property. These “object properties” (not to be confused with Guile’s own concept of “object properties”!) serve to store all links between grobs. For example, NoteHead grobs have a stem internal property, which contains a Stem grob. Conversely, Stem grobs contain a note-heads array of NoteHead grobs belonging to them, and uses the grobs from this array in order to compute its length. These links are put in place by engravers, using ly:grob-set-object!.

As explained in How line breaking clones grobs, the line breaking process includes breaking numerous grobs into pieces for the individual systems. After this is done, the grobs from each system need to be made independent from the other systems. This is the reason for having a separate area of properties for storing grobs and grob arrays. The process of making the systems independent is called break substitution. Concretely, if you access an object’s staff-symbol after line breaking, you will get a staff symbol spanning just the system, whereas if you do it before line breaking, you get a long staff symbol spanning the whole piece, which is to be broken into pieces after line breaking.

In the Internals Reference, such “object properties” are in the Internal Backend Properties section. This does also contain properties of the regular kind, however.

(ly:grob-object grob property [default])
(ly:grob-set-object! grob property value)

Return or set the value of pointer named property (a symbol) in grob.

ly:grob-object falls back to default if the pointer does not exist, or the empty list if default is not specified.

Grob pointers can never have meaningful default values since they store other grobs, which are not created before the score is actually processed. This is why, in the Internals manual, they can only appear in interfaces, not on a grob’s page.

The following example uses ly:grob-object to detect whether a note head has an accidental, and make it red in that case, as an extra reminder to the performer.

\version "2.25.8"

\relative {
  \override Accidental.color = "red"
  \override NoteHead.color =
    #(lambda (grob)
       (if (ly:grob-object grob 'accidental-grob #f)
           "red"
           "black"))
  c'16 d e fis g fis e cis e2
}
Output../../images/18fec2cd1323d7aab711620ab5d86982faeafb87986e786b4b2a5d5eeb9afd69.svg

Grob arrays#

Many of these “object properties” hold arrays of grobs. For technical reasons on the C++ side, these are not implemented with Guile arrays of grob values, but using a dedicated “grob array” type. Even though Scheme interfaces to some operations on grob arrays exist, first converting the array to a list and eventually converting back to an array is preferable in a majority of cases, to suit the nature of the Scheme language. (The difference between an array and a list in computer science can be read about in numerous web articles.)

(ly:grob-array-length grob-array)

Return the length of a grob array.

(ly:grob-array-ref grob-array index)

Retrieve the element located at index (starting from 0) in grob-array.

(ly:grob-array->list grob-array)

Turn grob-array into a Scheme list.

(ly:grob-list->grob-array lst)

Convert the Scheme list of grobs lst into a grob array.

In this example, stems are drawn in an unusual style where they span exactly the extent of the chord they attach to. This requires fetching the array of note heads in the note-heads “object property”.

\version "2.25.8"

{
  <c' e' g' c'' g'' c'''>
  \override Stem.length =
    #(lambda (grob)
       (let* ((note-heads (ly:grob-array->list (ly:grob-object grob 'note-heads)))
              (head-positions (map (lambda (head)
                                     (ly:grob-property head 'staff-position))
                                   note-heads)))
         (- (apply max head-positions)
            (apply min head-positions))))
  \override Stem.thickness = 4
  q
}
Output../../images/c4064d63cf03a42bcf190909bc675075244104c8fd8fff83be7f5a2dd23e6846.svg

The causation chain#

The cause property of a grob holds an event, or another grob, or the empty list, as set by the engraver with ly:engraver-make-grob.

This property can be used in engravers or in the backend. Most importantly, it is the way point-and-click can trace back the origin of a grob in the source file. If the cause of a grob is an event (but not if it is another grob), then clicking on this grob brings to the place in the source where the event’s original music object was created.

(event-cause grob)

Find the cause of grob in stream events.

If the cause of grob is an event, it is returned. If the cause is a grob, its own cause is retrieved, or the cause of its cause, etc., until an event is found.

Grob suicide#

Occasionally, the need for removing a grob entirely can arise in callbacks. Here are a few examples:

  • When the last note of a hairpin is on the beginning of a system, removing the part of the hairpin on that system (see also Dummy properties),

  • Removing system start delimiters when they are smaller than their collapse-height threshold,

  • In general, removing a grob if you detect an abnormal condition (after having issued a warning or programming error), if you can’t do anything useful.

(ly:grob-suicide! grob)

Cause grob to commit suicide.

After this, no properties will be defined on the grob anymore. All calls to ly:grob-property will return the fallback parameter or '(). Since callbacks are stored in properties, this also means no further callbacks will run on this grob.

(grob::is-live? grob)

Return a boolean telling whether grob is alive or dead.

Grob flavors#

Grobs come in several fashions. They are distinguished by the role they play in spacing, and how they are broken. In C++, this is reflected by two main subclasses of Grob, Item and Spanner. Items are horizontally fixed elements, like note heads, stems or bar lines. Spanners extend horizontally between two locations in the score. They have two bounds, which are always items; their X parent is usually their left bound. They include beams, hairpins, slurs. The item-interface and the spanner-interface characterize the type of a grob.

(ly:spanner-bound spanner direction)
(ly:spanner-set-bound! spanner direction item)

Retrieve or set the bound of spanner in direction (LEFT or RIGHT).

Columns form a subtype of items. They are abstract, invisible grobs. They group items on a the same horizontal position. The horizontal spacing within one column is normally simple to compute, so spacing the music means determining the offsets between successive columns. The score is built as an alternation of “musical” columns, of type PaperColumn, and “non-musical” columns, of type NonMusicalPaperColumn. For each moment, there is one non-musical column, then one musical column. The non-musical one holds any “prefatory” items, such as clefs, time signatures, key signatures, or bar lines. The musical one holds items that are related to notes, such as the note heads themselves, stems, articulations, or text scripts. The items in a musical column are quite logically called “musical items”, the others “non-musical” items.

How line breaking clones grobs#

Some grobs need to be printed several times due to line breaks. Think about clefs:

\version "2.25.8"

\paper {
  ragged-right = ##t
}

{
  c'1
  \break
  \clef bass
  c1
}
Output../../images/a4ee8b9ff75395341c29661fc5a87445cceab4243714f26113f971114f13b81a.svg

or hairpins:

\version "2.25.8"

\paper {
  ragged-right = ##t
}

\relative {
  c'\< d e g
  \break
  c g e f
  \break
  g-- a-- g2->\!
}
Output../../images/3264cf3f412d5ec12d94de2042f81c2749a5ba144736445ccc75e18ee68af63b.svg

This is handled by creating several clones of those grobs, which initially inherit the properties of the original grob, but afterwards are independent.

One of the important differences between items and spanners is how they behave with respect to line breaks.

A spanner, being arbitrarily long, is broken into as many clones as necessary to have one clone per system it spans. Each clone has its bounds adjusted: a line break in the middle of a spanner causes the bounds of the two broken pieces to become the NonMusicalPaperColumn grob the break was made onto. Because there are many possible line breaking configurations, breaking spanners is only done after line breaking.

How an item is broken, on the other hand, depends on whether it is musical. A musical item is never broken – you would not imagine a note head or a text script having several copies on different systems. Non-musical items, which are also called “breakable” items, are always broken into exactly three pieces. Unlike spanners, this is done earlier than line breaking. The three pieces are characterized by a break status direction, which is LEFT at end of line, CENTER in the middle of a line and RIGHT at the beginning of a line. One can imagine the original item on the center of the breakable column and two siblings, one at the left and one at the right, which replace the original at the end of a system and the beginning of the next system in case the column is the place of a line break.

../_images/item-breaking.svg

However, the broken clones should not always be visible; e.g., for bar numbers, only the broken grobs with break direction RIGHT (beginning of line) are printed. This is controlled by the break-visibility property, which is a vector of three booleans, corresponding to the break status directions LEFT, CENTER and RIGHT. If the break-visibility component for an item’s break status direction is #f, this item is killed (see Grob suicide).

(ly:grob-original grob)

Return the unbroken original version of grob. For an item, this is the corresponding sibling whose break status direction is CENTER. For a spanner, it is the unbroken spanner common to all broken pieces.

(ly:item-break-dir item)

Return the break status direction of item.

(ly:spanner-broken-into spanner)

Return the list of all children of this spanner, if it is broken. To get the siblings of a broken spanner, call this function on the original.

Note that for items, there is no direct way to get the siblings.

TODO: unbroken-or-last-broken-spanner?, etc.

Drawing objects#

Stencils are inkings of objects. Markups are pieces of code ready to be turned into stencils.

Stencils#

The stencil of every object is determined by its stencil property. Internally, the representation of stencils is close to what is output as vector commands in PostScript or SVG.

There are two special stencils: empty-stencil and point-stencil. The former is, as its name suggests, completely empty. Its extents are equal to empty-interval. It does typically not trigger additional spacing. By contrast, point-stencil is a stencil with extents reduced to the single-point interval (0.0 . 0.0).

empty-stencil and point-stencil have different use cases. The empty stencil is the only way to suppress any space taken by a stencil in alignment. This is often desirable, but not always. For instance, ties on omitted NoteHead objects do not work (and lead to a crash, issue 6040.

{
  \omit NoteHead
  e'1~ e'1
}

Instead of #f (which \omit sets the stencil to under the hood, equivalently to empty-stencil), the head should have point-stencil in order to provide an attachment point for the tie.

\version "2.25.8"

{
  \override NoteHead.stencil = #point-stencil
  e'1~ e'1
}
Output../../images/1e1e3a95597cc15f0359d97ec79b0b7735cf113216af49bb63d3653ecade3846.svg

There are many functions related to stencils, mostly for use in stencil callbacks. They are all documented in the Internals Reference under Scheme Functions (all functions named ly:stencil-... or stencil-...). The most important ones follow.

(ly:stencil-add stencil1 stencil2 ...)

Combine any number of stencils.

(ly:stencil-translate-axis stencil amount axis)

A copy of stencil, translated by amount on axis.

(ly:stencil-extent stencil axis)

The dimensions of stencil on axis.

(ly:stencil-aligned-to stencil axis side)

Translate stencil on axis to align it on side using its own extents.

For example, when axis is X and side is LEFT, for a stencil having X extent '(-2 . 3), this yields a stencil having X extent (0 . 5).

side is not necessarily -1 or 1. Interpolation is performed, so 0 centers the stencil for example.

(ly:stencil-outline stencil outline)

Return stencil with dimensions and outline from outline.

The dimensions are used in many grob extents. The outline is used for skylines in the process of collision avoidance. You can \override the show-horizontal-skylines and show-vertical-skylines of an object to visualize specific skylines.

{
  \override Score.PaperColumn.show-horizontal-skylines = ##t
  c'^"Hi"
}
(make-line-stencil width start-X start-Y end-X end-Y)

Draw a line between the points (start-X, start-Y) and (end-X, end-Y) with given width.

(make-filled-box-stencil X-extent Y-extent)

Make a filled box with the specified extents.

(make-circle-stencil radius thickness filled)

Make a circle with radius and thickness, optionally filled.

Markups#

Markups are commonly constructed by hand with the dedicated \markup syntax:

\markup \bold \center-column { Arranged by }

There is also another possibility which is sometimes useful. To each markup command \something corresponds a Scheme function make-something-markup. Thus, a second way to write the example above is

\markup #(make-bold-markup (make-center-column-markup '("Arranged" "by")))

The type predicate for markups is markup?. It also returns true on strings, which are one of the simplest forms markups can take.

The \stencil markup function makes a constant stencil markup.

\markup \stencil #(make-oval-stencil 4 3 0.1 #t)

A markup is not a stencil. It is the code for a stencil. The operation of turning the markup into stencil is called interpreting the markup. This requires two ingredients:

  • an output definition (\layout block),

  • properties.

The interpret-markup function takes layout, properties, and markup, and interprets the markup into a stencil.

The empty string "" is always interpreted to an empty stencil. You can also use the more understandable empty-markup instead of "". Use (make-null-markup) for a markup that is interpreted into a point stencil.

New markup commands may be defined with the define-markup-command macro. This is similar to defining music functions, except that the name of the function is included just before the arguments (no LilyPond assignment), and two extra parameters are passed, layout and props. Markup functions must return stencils.

\version "2.25.8"

#(define-markup-command (strong-emphasis layout props arg) (markup?)
   (interpret-markup layout props
     (make-bold-markup
       (make-italic-markup
         (make-underline-markup
           arg)))))

\markup \strong-emphasis "Very important!"
Output../../images/ef431ed36d265edd688b28d04888214a5745f03fb38046e3901e9881fae41d75.svg

The #:properties keyword is a convenient way to retrieve properties from the props argument. A good example is found in the official documentation:

\version "2.25.8"

#(define-markup-command (double-box layout props text) (markup?)
  #:properties ((inter-box-padding 0.4)
                (box-padding 0.6))
  "Draw a double box around text."
  (interpret-markup layout props
    (markup #:override `(box-padding . ,inter-box-padding) #:box
            #:override `(box-padding . ,box-padding) #:box text)))

\markup \override #'(inter-box-padding . 0.2) \double-box "Erik Satie"
Output../../images/a26ac54c19043feabc1b5099b6b98b3c8db5f091165b642b25b8e0fd98ac3163.svg

The grob-interpret-markup function takes a grob and a markup. It interprets the markup using layout and properties from the grob. This is where the notion of markups as code makes sense. The same markup, interpreted by two different grobs, can produce stencils varying in appearance. This is the reason why this works:

\version "2.25.8"

{
  \override TextScript.thickness = 10
  c'^\markup \draw-line #'(10 . 10)
}
Output../../images/5746607adae6f8562e3d999a22504a655cc84961a60bd9cc25a2faed1314d9c7.svg

This might seem surprising, since TextScript has no interface for the thickness property. Yet, its properties are used during markup interpretation, and the \draw-line markup command does read thickness.

Here is an example using grob-interpret-markup from a stencil callback to print a time signature with a slash:

\version "2.25.8"

#(define (slashed-time-signature-stencil grob)
   (let* ((fraction (ly:grob-property grob 'fraction))
          (num (number->string (car fraction)))
          (den (number->string (cdr fraction))))
     (ly:stencil-aligned-to
       (grob-interpret-markup grob
         (make-line-markup (list num "/" den)))
       Y
       CENTER)))

\layout {
  \context {
    \Staff
    \override TimeSignature.stencil = #slashed-time-signature-stencil
  }
}

{ c'1 }
Output../../images/67a09983066a4466fa30dadcfe9c6e7cd755fda159a01d80bf1e6cf64ca67dc1.svg

Interpreting a certain markup is frequent for stencil callbacks. The predefined function ly:text-interface::print interprets the markup in the text property of an object. Many grobs use it as stencil callback. It is a practical way to change the drawing of a grob to any markup.

\version "2.25.8"

{
  \tweak stencil #ly:text-interface::print
  \tweak text \markup \circle G
  \tweak thickness 2
  g'1
}
Output../../images/521ecf0dc20cafa1a39e68e83fede4ab6ed36a57898cd5ab983749c04f4aa74e.svg

In the above example, note in particular how the tweak for thickness affects the note head, even though NoteHead objects are not supposed a priori to respond to the thickness property as per the Internals Reference.

Spacing topics#

Directions and axes#

Some functions expect an axis. By convention, 0 denotes the X axis, and 1 the Y axis. For code understandability, the global constants X and Y are defined as 0 and 1, respectively.

Many properties can be up or down, left or right, sometimes center. Directions are to be understood as axis-agnostic directions. They have only three possible values: -1, 0, and 1.

  • -1: left, down, begin;

  • 0: center;

  • 1: right, up, end;

The type predicate ly:dir? checks that its argument is one of these three values.

The constants for directions are LEFT, RIGHT, DOWN, UP and CENTER.

Many properties that are primarily intended as directions are not restricted to these three values, but interpolate when they receive intermediary values. This is the case of self-alignment-X for example.

Intervals#

An interval is any kind of numeric span. Grob extents, for example, are intervals. In Scheme, an interval is a pair of numbers.

The empty interval is defined as (+inf.0 . -inf.0), and available under the convenient name empty-interval. This is not equivalent to a single-point interval like (0 . 0)!

The following helpers analyze intervals:

(interval-start interval)
(interval-end interval)

Human-friendly aliases for car and cdr.

(interval-bound interval direction)

Either the left or right bound of interval, depending on direction.

(interval-empty? interval)

True if interval is empty. The empty interval and the interval (1 . -1) are both empty in the sense of this function.

(interval-length interval)

The length that this interval spans.

(interval-center interval)

The middle of this interval.

(interval-index interval position)

Interpolate between the left and right bound of interval, according to position: -1 for the left bound, 1 for the right bound, 0 for the center, and any number for intermediate values.

(interval-sane? interval)

Test if this interval contains no infinities or NaNs, and has bounds in the right order.

The following helpers construct new intervals:

(interval-scale interval factor)

Scale interval by factor on both sides.

(interval-widen interval amount)

This interval, with amount added as padding on every side.

(interval-union interval1 interval2)

Unite two intervals.

(interval-intersection interval1 interval2)

The part common to these intervals.

(reverse-interval interval)

Invert the bounds of interval.

(add-point interval point)

Enlarge interval as necessary on one side to include point.

(symmetric-interval x)

The interval from -x to x.

(ordered-cons a b)

An interval with bounds a and b. The smallest one is the left bound.

Coordinates, extents and reference points#

Every grob has parents for positioning. The functions related to parents are:

(ly:grob-parent grob axis)
(ly:grob-set-parent! grob axis)

Return or set the parent of grob on axis (X or Y).

The X-offset and Y-offset properties of any grob hold its two coordinates relative to its parent on each axis. X-extent and Y-extent are its extents relative to itself.

Because all coordinates are relative, the offset between any two grobs is not directly accessible through their basic coordinates. This is why functions exist to search common parents and compute coordinates relative to these parents.

(ly:grob-common-refpoint grob1 grob2 axis)

Search and return a common reference point for grob1 and grob2 on axis (#f if not found).

A common reference point is a grob that is a direct or indirect parent of both grobs.

(ly:grob-relative-coordinate grob refpoint axis)
(ly:grob-extent grob refpoint axis)

Compute the coordinate or extent of grob along axis relative to refpoint, which must be a direct or indirect parent of grob along axis.

In general, care must be taken with coordinates on the Y axis. They are computed fairly lately, and retrieving them without caution can lead to cyclic dependencies. The delicate topic of Y positioning is tackled in the section about Unpure-pure containers.

Conversely, things are simple on the X axis, where it is almost always safe to ask for the extent of an item, or the extent of a spanner after line breaking (but most spanners naturally can’t compute their X extent before line breaking).

Units#

The two main parameters related to the font size are the staff space and the line thickness. Many distances are expressed in staff space so they can have unified numeric defaults for all staff sizes. All thicknesses are expressed as multiples of the line thickness. Both staff space and line thickness are properties of the StaffSymbol. The idea is that you can tweak them independently.

The staff symbol a grob resides on (when this makes sense) is found in its staff-symbol pointer.

(ly:staff-symbol-staff-space staff-symbol)
(ly:staff-symbol-line-thickness staff-symbol)

These give the units you should scale by when manipulating properties of a grob that are expressed in staff-space or in line-thickness. These functions access the staff symbol, and retrieve its staff-space or line-thickness property, then multiply by the value found in the output definition.

Dummy properties#

A few properties have no interesting value. They are only used to trigger a callback at a specific point of the positioning. They are only meaningfully set to a callback with side effects. These are:

  • before-line-breaking and after-line-breaking, computed at the moment their respective names suggest. A frequent callback for after-line-breaking is ly:spanner::kill-zero-spanned-time, which removes, for example, a hairpin, when it is a broken part that does not extend farther than one note.

    \version "2.25.8"
    
    {
      \override Hairpin.to-barline = ##f
      c'1\< \break c'1\! \break
      \once \override Hairpin.after-line-breaking = #'()
      c'1\< \break c'1\!
    }
    
    Output../../images/19fa88f08fe3207224927b6b3cf74a0785e204c394780b841df7273ce89b58cf.svg
  • positioning-done is called to space certain objects. For instance, ly:stem::calc-positioning-done shifts certain note heads when a chord has seconds in order to avoid collisions. Every note head calls its stem’s positioning-done at some point. Since grob properties are cached, this ensures that the positioning is only done once.

  • springs-and-rods is triggered just after before-line-breaking. Its purpose is to add spacing springs (flexible distances) and rods (minimum distances) to columns, setting up the horizontal spacing problem.

Unpure-pure containers#

XXX to be rewritten when I have done my wholesale restructuring of purity.

TODO: add examples. TODO: explain why pure functions must be pure (to avoid dependency on callback order).

The problem#

Unpure-pure containers are meant to address a thorny issue. Observe this input, with kneed beams between staves:

<<
  \new Staff = "up" \relative c'' {
    a8 g f e d c \change Staff = "down" b a
    \change Staff = "up" c' b a g c c \change Staff = "down" c,, c
  }
  \new Staff = "down" {
    \clef bass
    s1
    s1
  }
>>

To determine how many systems can be placed on the page, the page breaker would like to know how tall the system is. This depends on whether the beams are kneed or not – if they are, more space should be allocated for the beam.

On the other hand, the page breaker may go for a tightly packed configuration, perhaps because a small number of systems was requested by the user through systems-per-page. In this case, beams should rather not be kneed, because kneed beams take up more space.

Similar trouble is run into with text scripts.

filler = \relative c' { c8 e g c g c g e }

\relative c' {
  \repeat unfold 4 \filler
  c4^\markup "Crescendo poco a poco" d e f
  \mark "Tempo primo"
  g^"Rallentando ma non troppo" f e d
  \repeat unfold 20 \filler
}

In this example, observe how the musical indications avoid each other on the second system, which is tall.

The page breaker could have chosen this configuration instead:

filler = \relative c' { c8 e g c g c g e }

\relative c' {
  \repeat unfold 4 \filler
  c4^\markup "Crescendo poco a poco" d e f
  \break
  \mark "Tempo primo"
  g^"Rallentando ma non troppo" f e d
  \repeat unfold 20 \filler
}

Because the break separates the two measures with colliding text scripts, the second system is now smaller, while the first system got slightly taller. This may in turn influence page breaking decisions: a page break could be inserted between the first and the second system (assuming more music before) to space pages more evenly.

Both of these examples show how a cyclic dependency can occur between calculating the spacing inside a system and choosing breaks.

Page and line breaking rely on scoring different configurations, attributing them penalties on criteria such as cramped spacing. It is infeasible, however, to score all possible configurations – that would make the compilation gigantically slower. Other classic constraint optimization techniques are not workable, either. There simply are too many parameters to take into account.

This is why LilyPond has heuristic algorithms instead. The goal of pure properties is to provide estimates of certain spacing parameters before line breaking in order to help the breaking algorithms make informed decisions. Afterwards, when one configuration is finally settled on, the properties are computed for real.

The art of writing pure callbacks is to make the best estimates possible while ensuring no cyclic dependencies, including corner cases. This is very difficult.

It has to be noted that purity is concerned with all Y axis positioning. There is no concept of pure X-offset or X-extent because, by design, positioning constraints on the X axis are determined in advance (springs and rods). The operation of page breakers is to try a spacing that seems sensible on the X axis and see how it looks like on the Y axis using pure properties.

Writing an unpure-pure container#

To begin with, an unpure-pure container is only needed if the callback needs different code paths for pure and unpure positioning. For instance, many stencil callbacks do not depend on line breaking, such as the one for NoteHead. Consequently, there is no unpure-pure container involved even though the callback is triggered before line breaking. When this differentiation is required, the callback is replaced with an unpure-pure container containing two callbacks.

(ly:make-unpure-pure-container unpure-function pure-function)

Construct an unpure-pure container.

unpure-function is a regular callback.

pure-function takes three arguments: the grob, a start column, and an end column. It is meant to estimate the property for a potential broken part of the grob between start and end. For items, these parameters are mandatory, but they are not meaningful: an item should only check that it is between these columns. A spanner should try to make use of the start and end parameters for better estimates.

In a more advanced form, unpure-function can take n + 1 arguments, the grob and other arguments. In this case, pure-function takes n + 3 arguments, the grob, start and end column, and the other arguments. The property can then be read using ly:unpure-call or ly:pure-call, passing these arguments. They may be used for estimates as well.

(ly:unpure-pure-container-unpure-part container)
(ly:unpure-pure-container-pure-part container)

Extract either part of container.

(ly:unpure-call data grob . rest)
(ly:pure-call data grob start end . rest)

Call a procedure with arguments. If the data is an unpure-pure container, extract its unpure or pure part to get the procedure. If data is already a procedure, use it directly.

Further reading on purity#