\version "2.19.83" #(use-modules (ice-9 regex)) \paper { #(include-special-characters) line-width = 15\cm top-margin = 1.5\cm bottom-margin = 1.5\cm indent = 1\cm ragged-right = ##t ragged-bottom = ##f ragged-last-bottom = ##f top-markup-spacing = #'((basic-distance . 0) (minimum-distance . 0) (padding . 2) (stretchability . 0)) last-bottom-spacing = #'((basic-distance . 0) (minimum-distance . 0) (padding . 0) (stretchability . 100)) markup-markup-spacing = #'((basic-distance . 0) (minimum-distance . 4) (padding . 4) (stretchability . 0)) markup-system-spacing = #'((basic-distance . 0) (minimum-distance . 10) (padding . 2) (stretchability . 0)) score-markup-spacing = #'((basic-distance . 0) (minimum-distance . 10) (padding . 2) (stretchability . 0)) } \layout { \context { \Score timing = ##f } \context { \Staff \omit Clef \omit TimeSignature } } \markup \fontsize #4 \bold \typewriter "\\shiftOttavaBracket" \markup \italic \justify { NOTE: This whitepaper is not affiliated with nor validated by the GNU LilyPond project. It is not a substitute for official documentation. Information within is based on observed behavior of the program and might not align with the intended design. For reference, the version of LilyPond used was 2.19.83. } \markup \column { \larger \bold Overview \justify { We would like a method for shifting an OttavaBracket away from its associated staff by a specified distance while preserving as much of the automatic collision avoidance behavior. } \vspace #1 \justify { There are several obstacles to overcome. An OttavaBracket's final vertical position relative to the staff is not determined by any single grob property. Shifting the bracket will require modifying multiple properties, some of which will have side-effects that must be compensated. } } \markup \column { \larger \bold \line { Y-offset } \justify { The Y-offset property is the distance, in staff spaces, between the reference points of an object and its parent. The reference point of an OttavaBracket aligns with the horizontal segment of its bracket symbol. The parent of an OttavaBracket is the VerticalAxisGroup, whose reference point coincides with the middle of the StaffSymbol. } } #(define color-good '(0.2 0.5 0.8)) #(define color-bad '(0.8 0.2 0.3)) #(define line-thickness 0.2) #(define whiteout-thickness 1.5) visualize-Y-offset = #(define-music-function (grob-path color) (symbol-list? color?) (define (proc grob) (let* ((orig (ly:grob-property grob 'stencil)) (xex (ly:stencil-extent orig X)) (y (abs (ly:grob-property grob 'Y-offset)))) (ly:grob-set-property! grob 'layer 1000) (ly:grob-set-property! grob 'stencil (grob-interpret-markup grob #{ \markup \overlay { \stencil #orig \with-dimensions-from \null \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \translate #(cons (interval-index xex 0) 0) \scale #(cons 1 (- (ly:grob-property grob 'direction))) \path #line-thickness #`( (moveto -1 ,y) (lineto 1 ,y) (moveto 0 ,y) (lineto 0 0) (moveto -0.5 1) (lineto 0 0) (lineto 0.5 1) (moveto -1 0) (lineto 1 0)) } #})))) #{ \override $grob-path .after-line-breaking = #proc #}) \fixed c' { \visualize-Y-offset Staff.OttavaBracket #color-good \ottava 1 d''8[ 8] 4 \ottava 2 \stemUp f'''8[ 8] 4 \stemNeutral \ottava -1 g,8[ 8] 4 \ottava -2 \stemDown e,,8[ 8] 4 \stemNeutral } \markup \column { \justify { By default, the OttavaBracket does not specify a fixed value for Y-offset but rather uses a procedure to compute it. This procedure considers the position of some grobs, including NoteHeads and Stems, to determine where the bracket should be placed. Several associated properties are consulted. } \vspace #1 \justify { The staff-padding property specifies the \italic minimum distance between an OttavaBracket's reference point and the nearest extent of the associated StaffSymbol. Note the presence of nearby grobs may result in the OttavaBracket being further away than this amount, as shown in the last scenario below. } } visualize-staff-padding = #(define-music-function (grob-path color) (symbol-list? color?) (define (proc grob) (let* ((orig (ly:grob-property grob 'stencil)) (xex (ly:stencil-extent orig X)) (dir (ly:grob-property grob 'direction)) (y (ly:grob-property grob 'staff-padding))) (ly:grob-set-property! grob 'layer 1000) (ly:grob-set-property! grob 'stencil (grob-interpret-markup grob #{ \markup \overlay { \stencil #orig \with-dimensions-from \null \translate #(cons (interval-index xex 0) 0) \overlay { \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 (- dir)) \path #0.2 #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 dir) \translate #(cons 0 (- y)) \path #line-thickness #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) } } #})))) #{ \override $grob-path .after-line-breaking = #proc #}) \fixed c' { \visualize-staff-padding Staff.OttavaBracket #color-good \ottava 1 d''8[ 8] 4 \ottava -1 g,8[ 8] 4 \visualize-staff-padding Staff.OttavaBracket #color-bad \ottava 2 a'''8[ 8] 4 } \markup \column { \justify { The padding property specifies the \italic minimum distance between the OttavaBracket and neighboring grobs. } } visualize-padding = #(define-music-function (grob-path color) (symbol-list? color?) (define (proc grob) (let* ((orig (ly:grob-property grob 'stencil)) (xex (ly:stencil-extent orig X)) (yex (ly:stencil-extent orig Y)) (dir (ly:grob-property grob 'direction)) (y (ly:grob-property grob 'padding))) (ly:grob-set-property! grob 'layer 1000) (ly:grob-set-property! grob 'stencil (grob-interpret-markup grob #{ \markup \overlay { \stencil #orig \with-dimensions-from \null \translate #(cons (interval-index xex 0) (interval-index yex (- dir))) \overlay { \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 (- dir)) \path #0.2 #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 dir) \translate #(cons 0 (- y)) \path #line-thickness #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) } } #})))) #{ \override $grob-path .after-line-breaking = #proc #}) \fixed c' { \visualize-padding Staff.OttavaBracket #color-good % Changing the default value for demonstration purposes. \override Staff.OttavaBracket.padding = 0.8 \ottava 1 \stemUp e''8[ 8] 4 \stemNeutral \ottava -1 \stemDown f,8[ 8] 4 \stemNeutral } \markup \column { \justify { The default procedure for computing an OttavaBracket's Y-offset does not factor in all grobs. Instead, Accidentals, Slurs, and Scripts are among those that are considered in a later round of processing. Should the value of Y-offset be too small, the bracket will be offset accordingly to avoid collisions. } \vspace #1 \justify { In the examples below, the arrow being overlaid visualizes only the computed value of Y-offset. Its base \italic should align with the middle of the StaffSymbol; however, these OttavaBrackets were moved. } } \fixed c' { \visualize-Y-offset Staff.OttavaBracket #color-bad % Changing the default value for demonstration purposes. \override Staff.OttavaBracket.outside-staff-padding = 0.8 \ottava 1 f''8[ gis''8] f''4 \ottava -1 e,8[( 8] 4) \ottava 2 b''8[\upbow 8] 4 } \markup \column { \larger \bold \line { outside-staff-padding } \justify { The outside-staff-padding property, similar to the padding property, is consulted for shifting an OttavaBracket to avoid being too close to other grobs that are outside the staff. } } visualize-outside-staff-padding = #(define-music-function (grob-path both-sides? color) (symbol-list? (boolean? #f) color?) (define (proc grob) (let* ((orig (ly:grob-property grob 'stencil)) (xex (ly:stencil-extent orig X)) (yex (ly:stencil-extent orig Y)) (dir (ly:grob-property grob 'direction)) (y (ly:grob-property grob 'outside-staff-padding 0.46))) (ly:grob-set-property! grob 'layer 1000) (ly:grob-set-property! grob 'stencil (grob-interpret-markup grob #{ \markup \overlay { \stencil #orig \with-dimensions-from \null \translate #(cons (- (interval-index xex 0) (if both-sides? 1 0)) (interval-index yex (- dir))) \overlay { \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 (- dir)) \path #0.2 #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 dir) \translate #(cons 0 (- y)) \path #line-thickness #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) } $(if both-sides? #{ \markup { \with-dimensions-from \null \translate #(cons (1+ (interval-index xex 0)) (interval-index yex dir)) \overlay { \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 dir) \path #0.2 #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \scale #(cons 1 (- dir)) \translate #(cons 0 (- y)) \path #line-thickness #`( (moveto 0 -2) (lineto 0 0) (moveto -0.5 -1) (lineto 0 0) (lineto 0.5 -1) (moveto -1 0) (lineto 1 0)) } } #}) } #})))) #{ \override $grob-path .after-line-breaking = #proc #}) \fixed c' { \visualize-outside-staff-padding Staff.OttavaBracket #color-good % Changing the default value for demonstration purposes. \override Staff.OttavaBracket.outside-staff-padding = 0.8 \ottava 1 f''8[ gis''8] f''4 \ottava -1 e,8[( 8] 4) \ottava 2 b''8[\upbow 8] 4 } \markup \column { \justify { It is important to note that outside-staff-padding applies to both sides of an OttavaBracket, affecting the spacing with grobs on the side opposite to the StaffSymbol, such as RehearsalMarks. For each example below, outside-staff-padding is being increased by one staff space. } } boxMark = { \once \override Score.RehearsalMark.self-alignment-X = #LEFT \mark \markup \box \fontsize #-3 \sans MARK } downMark = { \once \override Score.RehearsalMark.direction = #DOWN } \fixed c' { \visualize-outside-staff-padding Staff.OttavaBracket ##t #color-good \override Staff.OttavaBracket.outside-staff-padding = 1 s128 \boxMark \ottava 1 d''8[( 8] 4) \ottava 0 \override Staff.OttavaBracket.outside-staff-padding = 2 s128 \boxMark \ottava 1 d''8[( 8] 4) \ottava 0 \override Staff.OttavaBracket.outside-staff-padding = 3 s128 \boxMark \ottava 1 d''8[( 8] 4) } \markup \column { \larger \bold \line { extra-offset } \justify { Unlike the properties previously discussed, extra-offset is only applied after all layout logic has been performed. It represents an arbitrary shift in position that does not account for potential collisions. As shown below, extra-offset is a two-dimensional value; though we will only be interested in vertical adjustments. } } visualize-extra-offset = #(define-music-function (grob-path color) (symbol-list? color?) (define (proc grob) (let* ((orig (ly:grob-property grob 'stencil)) (xex (ly:stencil-extent orig X)) (yex (ly:stencil-extent orig Y)) (off (ly:grob-property grob 'extra-offset '(0 . 0)))) (ly:grob-set-property! grob 'layer 1000) (ly:grob-set-property! grob 'stencil (grob-interpret-markup grob #{ \markup \overlay { \with-color #'(0.6 0.6 0.6) \translate #(cons (- (car off)) (- (cdr off))) \stencil #orig \stencil #orig \with-dimensions-from \null \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \translate #(cons (interval-index xex 0) (interval-index yex 0)) \path #line-thickness #`( (moveto -0.6 0) (rlineto 0.6 0.6) (rlineto 0.6 -0.6) (rlineto -0.6 -0.6) (closepath) (moveto 0 0) (lineto ,(- (car off)) ,(- (cdr off))) (rmoveto -0.6 0) (rlineto 0.6 0.6) (rlineto 0.6 -0.6) (rlineto -0.6 -0.6) (closepath) ) } #})))) #{ \override $grob-path .after-line-breaking = #proc #}) \fixed c' { \visualize-extra-offset Staff.OttavaBracket #color-bad \override Staff.OttavaBracket.extra-offset = #'(4.5 . -1.5) s128 \boxMark \ottava 1 d''8[ 8] 4 \override Staff.OttavaBracket.extra-offset = #'(1.5 . 4.5) \ottava -1 e,8[( 8] 4) } #(define (code-snippet sym str) (ly:parser-define! sym (string-split ;; Inject zero-width non-joiners to prevent ligatures. (regexp-substitute/global #f "[a-z]" str 'pre 0 "‌" 'post) #\nl)) (ly:parser-include-string str)) #(define-markup-command (code layout props lines) (list?) (interpret-markup layout props #{ \markup \translate #'(6 . 0) \smaller \bold \typewriter \override #'(baseline-skip . 2) \left-column { #lines } #})) #(code-snippet 'firstAttempt "shiftOttavaBracket = #(define-music-function (amount) (number?) #{ \\override Staff.OttavaBracket .outside-staff-padding = #(+ 0.46 amount) #} )") \pageBreak \markup \column { \larger \bold \line { First Attempt } \justify { The simplest way to shift our OttavaBracket would be to "\\offset" the outside-staff-padding property. Unfortunately, OttavaBracket does not have a base value that "\\offset" can build upon. We can work around this issue by noting that, internally, the value 0.46 is used as the default outside-staff-padding. Instead of "\\offsetting" the property, we will "\\override" it after manually computing the value. } \vspace #1 \code \firstAttempt \vspace #1 \justify { NOTE: For the remainder of the whitepaper, the examples will be testing with no shift, a shift of one staff space, and a shift of two staff spaces. } } \fixed c' { \visualize-outside-staff-padding Staff.OttavaBracket #color-good \ottava 1 d''8[( 8] 4) \ottava -1 g,8[( 8] 4) \shiftOttavaBracket 1 \ottava 1 d''8[( 8] 4) \ottava -1 g,8[( 8] 4) \shiftOttavaBracket 2 \ottava 1 d''8[( 8] 4) \ottava -1 g,8[( 8] 4) } \markup \column { \justify { Initial testing seems promising, however we need to remember that the outside-staff-padding property affects spacing on both sides of the OttavaBracket. Let us add in some RehearsalMarks to see the impact of "\\shiftOttavaBracket". } } \fixed c' { \visualize-outside-staff-padding Staff.OttavaBracket ##t #color-good s128 \boxMark \ottava 1 d''8[( 8] 4) \once \downMark \boxMark \ottava -1 g,8[( 8] 4) \visualize-outside-staff-padding Staff.OttavaBracket ##t #color-bad \shiftOttavaBracket 1 \boxMark \ottava 1 d''8[( 8] 4) \once \downMark\boxMark \ottava -1 g,8[( 8] 4) \shiftOttavaBracket 2 \boxMark \ottava 1 d''8[( 8] 4) \once \downMark \boxMark \ottava -1 g,8[( 8] 4) } \markup \column { \justify { The RehearsalMarks are moving by \italic twice the amount we requested the OttavaBracket to move. This is because the extra amount added to outside-staff-padding exists on both sides of the bracket. Since we cannot selectively apply our extra padding to just one side, we will need a clever trick. } \vspace #1 \justify { The first thing to recognize is that we should only be adding \italic half of the requested amount to outside-staff-padding. This ensures that a grob like RehearsalMark will be moved the correct distance. However, doing this means the OttavaBracket only moves by half of what we need. This is where extra-offset can be used, as it will take care of moving our bracket the rest of the way. } \vspace #1 \justify { On its own, extra-offset would not avoid collisions; but we have adjusted outside-staff-padding to ensure other grobs will already be moved sufficiently far to accommodate the additional nudging. } } #(code-snippet 'secondAttempt "shiftOttavaBracket = #(define-music-function (amount) (number?) (let ((half (/ amount 2))) #{ \\override Staff.OttavaBracket .outside-staff-padding = #(+ 0.46 half) \\override Staff.OttavaBracket .extra-offset = #(lambda (grob) (let ((dir (ly:grob-property grob 'direction))) (cons 0 (* dir half)))) #} ))") \pageBreak \markup \column { \larger \bold \line { Second Attempt } \justify { In review, the method now involves applying half of the requested shift amount to outside-staff-padding and half to extra-offset. Note that care must be taken to ensure extra-offset is offsetting the proper direction. } \vspace #1 \code \secondAttempt } \fixed c' { \visualize-extra-offset Staff.OttavaBracket #color-good s128 \boxMark \ottava 1 d''8[( 8] 4) \once \downMark \boxMark \ottava -1 g,8[( 8] 4) \shiftOttavaBracket 1 \boxMark \ottava 1 d''8[( 8] 4) \once \downMark\boxMark \ottava -1 g,8[( 8] 4) \shiftOttavaBracket 2 \boxMark \ottava 1 d''8[( 8] 4) \once \downMark \boxMark \ottava -1 g,8[( 8] 4) } \markup \column { \justify { Testing shows improved results, where both the OttavaBracket and RehearsalMark appear to move as a single unit, maintaining their normal separation. } \vspace #1 \justify { Thus far, we have only considered the situation when Y-offset is not large enough to avoid collision. In order for "\\shiftOttavaBracket" to be useful, it must work also when the bracket is being influenced by Y-offset alone. } \vspace #1 \justify { To see what happens with a larger Y-offset, let us increase the staff-padding property, which will in turn increase Y-offset. We will also remove the Slurs, so there is plenty of room between the bracket and the staff. } } visualize-ruler = #(define-music-function (grob-path color) (symbol-list? color?) (define (proc grob) (let* ((orig (ly:grob-property grob 'stencil)) (xex (ly:stencil-extent orig X)) (dir (ly:grob-property grob 'direction))) (ly:grob-set-property! grob 'layer 1000) (ly:grob-set-property! grob 'stencil (grob-interpret-markup grob #{ \markup \overlay { \stencil #orig \with-dimensions-from \null \with-color #color \override #'(style . outline) \override #`(thickness . ,whiteout-thickness) \whiteout \translate #(cons (interval-index xex 0) 0) \scale #(cons 1 (- dir)) \path #line-thickness #`( (moveto -1.2 0) (lineto 1.2 0) (moveto -0.8 1) (lineto 0.8 1) (moveto -0.8 2) (lineto 0.8 2) (moveto -0.8 3) (lineto 0.8 3) (moveto -0.8 4) (lineto 0.8 4) (moveto -1.2 5) (lineto 1.2 5) ) } #})))) #{ \override $grob-path .after-line-breaking = #proc #}) \fixed c' { \visualize-ruler Staff.OttavaBracket #color-good \override Staff.OttavaBracket.staff-padding = 3 s128 \boxMark \ottava 1 d''8[ 8] 4 \once \downMark \boxMark \ottava -1 g,8[ 8] 4 \visualize-ruler Staff.OttavaBracket #color-bad \shiftOttavaBracket 1 \boxMark \ottava 1 d''8[ 8] 4 \once \downMark\boxMark \ottava -1 g,8[ 8] 4 \shiftOttavaBracket 2 \boxMark \ottava 1 d''8[ 8] 4 \once \downMark \boxMark \ottava -1 g,8[ 8] 4 } \markup \column { \justify { Since the OttavaBrackets are far enough away from any grobs, the increase of outside-staff-padding has no impact on the bracket itself. There is nothing to push against. With only extra-offset applying, the brackets have moved half of what we need. } \vspace #1 \justify { However, we have a good idea for our next improvement. We should only need to "\\offset" Y-offset by the remaining distance to get the OttavaBrackets in the correct spot. } } #(code-snippet 'thirdAttempt "shiftOttavaBracket = #(define-music-function (amount) (number?) (let ((half (/ amount 2))) #{ \\override Staff.OttavaBracket .Y-offset = #(grob-transformer 'Y-offset (lambda (grob orig) (let ((dir (ly:grob-property grob 'direction))) (+ orig (* dir half))))) \\override Staff.OttavaBracket .outside-staff-padding = #(+ 0.46 half) \\override Staff.OttavaBracket .extra-offset = #(lambda (grob) (let ((dir (ly:grob-property grob 'direction))) (cons 0 (* dir half)))) #} ))") \pageBreak \markup \column { \larger \bold \line { Third Attempt } \justify { Since Y-offset is a signed value, we cannot simply "\\offset" it by a fixed amount. This would only work for positive values. LilyPond provides grob-transformer, a handy construction that makes it easy to modify a base value with a custom procedure, even when that base value might be the result of another procedure. } \vspace #1 \code \thirdAttempt } \fixed c' { \visualize-ruler Staff.OttavaBracket #color-good \override Staff.OttavaBracket.staff-padding = 3 s128 \boxMark \ottava 1 d''8[ 8] 4 \once \downMark \boxMark \ottava -1 g,8[ 8] 4 \shiftOttavaBracket 1 \boxMark \ottava 1 d''8[ 8] 4 \once \downMark\boxMark \ottava -1 g,8[ 8] 4 \shiftOttavaBracket 2 \boxMark \ottava 1 d''8[ 8] 4 \once \downMark \boxMark \ottava -1 g,8[ 8] 4 } \markup \column { \justify { We can also verify that this new function still behaves properly when nearby grobs have pushed the OttavaBracket further than Y-offset. } } \fixed c' { \visualize-ruler Staff.OttavaBracket #color-good s128 \boxMark \ottava 1 a''8[( 8] 4) \once \downMark \boxMark \ottava -1 c,8[( 8] 4) \shiftOttavaBracket 1 \boxMark \ottava 1 a''8[( 8] 4) \once \downMark\boxMark \ottava -1 c,8[( 8] 4) \shiftOttavaBracket 2 \boxMark \ottava 1 a''8[( 8] 4) \once \downMark \boxMark \ottava -1 c,8[( 8] 4) } \markup \column { \larger \bold \line { Closing } \justify { In summary, we have produced a utility function that properly adjusts grob properties to shift an OttavaBracket away from the staff by a specified amount. } \vspace #1 \justify { What are the next steps? First would be testing the function given real-world projects. These artificial scenarios could be hiding bad behavior. Second would be ensuring the function can cope with other adjustments. For instance, the function assumes that OttavaBracket will not have outside-staff-padding set, thus it can safely base its computation on the known default value. } }