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
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
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
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
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
orRIGHT
).
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
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
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.
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
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 isLEFT
, 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
theshow-horizontal-skylines
andshow-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
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
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
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
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
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
andcdr
.
- (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
orline-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
andafter-line-breaking
, computed at the moment their respective names suggest. A frequent callback forafter-line-breaking
isly: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
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’spositioning-done
at some point. Since grob properties are cached, this ensures that the positioning is only done once.springs-and-rods
is triggered just afterbefore-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
orly: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.