Excerpt below. Works for all test cases I threw at it and it significantly less
logic.
I *think* I can see how to do this without an engraver but the last 4-5 days
has been long enough. I think that in the unpure call I can do something like:
- Get VerticalAxis Group (VAG) with ly:grob-get-vertical-axis-group-index
- Probe around the elements of the that parent's Vertical Alignment
- Find the two VAG's and do the same calculation.
The only thing that I think I am missing is to make sure that the staves I am
working with are up and down and not an occasional oasis.
Thank you, Kieren
-kwb
%%% SNIPPET BEGINS
\version "2.24.3"
% Define new grob properties so that every grob can have a reference to
% what is it going to use for alignment later
#(set-object-property! 'up-staff 'backend-type? ly:grob?)
#(set-object-property! 'down-staff 'backend-type? ly:grob?)
#(define (Multi_measure_rest_positioner context)
(let (
; collection of staff refs that we may want to use later
; this will hold a list of (grob . source-engraver) pairs
(maybe-staff-ref '())
; After determining that they are the staves we need, they will
; be stored here
(staff-ref (cons '() '()))
; If we see a MM rest number, store it here.
(maybe-mm-number-ref '())
)
(make-engraver
(acknowledgers
((staff-symbol-interface this-engraver grob source-engraver)
; Catch any staff symbols and set them to be processed in a second
(when (or (null? (car staff-ref)) (null? (cdr staff-ref)))
(set! maybe-staff-ref (cons (cons grob source-engraver)
maybe-staff-ref))))
; And record the MM rest number we see
((multi-measure-rest-number-interface this-engraver grob source-engraver)
(set! maybe-mm-number-ref (cons (cons grob source-engraver)
maybe-mm-number-ref))))
((process-acknowledged this-engraver)
; Process staff symbols first. If we do not have two staves, we need to
not even worry about
; the MMrest numbers as there is nothing to center it to.
; TODO: Move this to a fold
(for-each (lambda (ms)
(let (
(ctx-id (ly:context-id (ly:translator-context (cdr
ms)))))
(cond
((string=? ctx-id "up")
(set-car! staff-ref (car ms)))
((string=? ctx-id "down")
(set-cdr! staff-ref (car ms)))
(else
(ly:warning "Unknown staff found: ~a~%" ctx-id)))))
maybe-staff-ref)
; then see if we have a MMRest number that lives in the down ctx
(let (
(mm-ref-res (find (lambda (mrr)
(string=? (ly:context-id (ly:context-find
(ly:translator-context (cdr mrr)) 'Staff)) "down")) maybe-mm-number-ref)))
(when
(not (eqv? mm-ref-res #f))
(ly:grob-set-object! (car mm-ref-res) 'up-staff (car staff-ref))
(ly:grob-set-object! (car mm-ref-res) 'down-staff (cdr staff-ref))))
; Clean up before next round
(set! maybe-staff-ref '())
(set! maybe-mm-number-ref '())
) ; End process acknowledger block
)))
#(ly:set-option 'debug-skylines #t)
% Begin Score Output
\score {
\new PianoStaff <<
\new Staff = "up" \with {
\omit MultiMeasureRestNumber
\override MultiMeasureRest.expand-limit = #1
} \relative c'' {
\clef treble \key a \major \time 6/8
R2.*2
\repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
R2.*2 |
\repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
\break
\repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
R2.*2 |
\repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
}
\new Dynamics {
s2.*2 | s2.*2 | s2.*2 | s8 s8 s8\ff s4. | s2.
s2.*2 | s2.*2 | s8 s8 s8\ff s4. | s2.
}
\new customStaffDown = "down" \relative c {
\clef bass \key a \major \time 6/8
\compressMMRests { R2.*2 } |
\repeat unfold 2 {
r8 r8 <e e'> r8 r8 <e e'> |
}
\compressMMRests { R2.*2 } |
\repeat unfold 2 {
r8 r8 <e e'> r8 r8 <e e'> |
}
\break
\repeat unfold 2 {
r8 r8 <e e'> r8 r8 <e e'> |
}
\compressMMRests { R2.*2 } |
\repeat unfold 2 {
r8 r8 e r8 r8 e |
}
}
>>
\layout {
#(layout-set-staff-size 18.5)
\context {
\Staff
\name customStaffDown
\alias Staff
\inherit-acceptability customStaffDown Staff
\override MultiMeasureRest.expand-limit = #1
\override MultiMeasureRestNumber.font-size = #2.0
}
\context {
\PianoStaff
\consists #Multi_measure_rest_positioner
}
\context {
\customStaffDown
% Turn off skylines so that Lilypond doesn't get any wise ideas and try
% to space or align this for us.
\override MultiMeasureRestNumber.vertical-skylines = ##f
\override MultiMeasureRestNumber.Y-offset =
#(ly:make-unpure-pure-container
; Unpure
(lambda (grob)
(let*
(
(up-staff-ref (ly:grob-object grob 'up-staff))
(down-staff-ref (ly:grob-object grob 'down-staff))
; We need to get to a place where the up staff and the
; down staff share a common parent. We should be able to
; get this with the below method
(common-ref (ly:grob-common-refpoint-of-array grob
(ly:grob-list->grob-array (list up-staff-ref down-staff-ref)) Y))
; Now get the distances
(up-y-offset (ly:grob-relative-coordinate up-staff-ref common-ref
Y))
(down-y-offset (ly:grob-relative-coordinate down-staff-ref
common-ref Y))
(offset-center (/ (+ up-y-offset down-y-offset) 2))
)
; Set parent of MMR Number to the common parent from above
(ly:grob-set-parent! grob Y common-ref)
; Return middle. Offset by one. I believe to account for different
ref points positions on
; staves and text.
(- offset-center 1)
))
; Pure
; Skylines are off so this doesn't really matter where it goes. Credo.
(lambda (grob start stop) 0)
)
}
}
}
%%% SNIPPET ENDS
> On Jun 21, 2025, at 6:00 AM, Kieren MacMillan <[email protected]>
> wrote:
>
> Hi again,
>
> Here’s the example with the layout bug fixed, and starting with an MMR (per
> your other issue). The only thing I’d want to fix is to decouple the vertical
> positioning from the axis line, so that the MMR number would float down to
> true centre (the way dynamics do, as in that earlier piano example I sent).
>
> Cheers,
> Kieren.
>
> %%% SNIPPET BEGINS
>
> \layout {
>
> #(layout-set-staff-size 18.5)
>
> \context {
> \Staff
> \name customStaffDown
> \alias Staff
> \inherit-acceptability customStaffDown Staff
> \omit MultiMeasureRestNumber
> \override MultiMeasureRest.expand-limit = #1
> }
>
> \context {
> \Staff
> \name customStaffUp
> \alias Staff
> \inherit-acceptability customStaffUp Staff
> \accidentalStyle piano
> \omit MultiMeasureRestNumber
> \override MultiMeasureRest.expand-limit = #1
> }
>
> \context {
> \name PianoMMR
> \type "Engraver_group"
> \inherit-acceptability PianoMMR Dynamics
> \consists "Axis_group_engraver"
> \override VerticalAxisGroup.staff-affinity = #CENTER
> \override VerticalAxisGroup.nonstaff-relatedstaff-spacing =
> #'((basic-distance . 2) (minimum-distance . 1) (padding . 0.5)
> (stretchability . 1))
> \consists "Dynamic_engraver"
> \consists "Multi_measure_rest_engraver"
> \hide MultiMeasureRest
> \override MultiMeasureRestNumber.direction = #DOWN
> \override MultiMeasureRestNumber.Y-offset = -1
> }
>
> \context {
> \StaffGroup
> \name CustomStaffGroup
> \alias StaffGroup
> \inherit-acceptability CustomStaffGroup StaffGroup
> systemStartDelimiterHierarchy = #'(SystemStartBrace a b)
> }
>
> }
>
> % Begin Score Output
> theGroup = \new CustomStaffGroup <<
> \new customStaffUp = "up" \relative c'' {
> \clef treble \key a \major \time 6/8
> R2.*2
> \repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
> R2.*2 |
> \repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
> \break
> \repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
> R2.*2 |
> \repeat unfold 2 { r8 r8 <e e'> r8 r8 <e e'> | }
> }
> \new PianoMMR {
> \compressMMRests { R2.*2 }
> \repeat unfold 2 {
> s2.*2 | \compressMMRests { R2.*2 } | s8 s8 s8\ff s4. | s2.
> }
> }
> \new customStaffDown = "down" \relative c {
> \clef bass \key a \major \time 6/8
> R2.*2
> \repeat unfold 2 {
> r8 r8 <e e'> r8 r8 <e e'> |
> }
> R2.*2 |
> \repeat unfold 2 {
> r8 r8 <e e'> r8 r8 <e e'> |
> }
> \break
> \repeat unfold 2 {
> r8 r8 <e e'> r8 r8 <e e'> |
> }
> R2.*2 |
> \repeat unfold 2 {
> r8 r8 e r8 r8 e |
> }
> }
>>>
>
> \score {
> \theGroup
> \layout {
> \context {
> \CustomStaffGroup
> \override VerticalAxisGroup.staff-staff-spacing.padding = #4
> }
> }
> }
>
> \markup \vspace #10
>
> \score {
> \theGroup
> \layout {
> \context {
> \CustomStaffGroup
> \override VerticalAxisGroup.staff-staff-spacing.padding = #14
> }
> }
> }
>
> \version “2.24.4"
> %%% SNIPPET ENDS
> __________________________________________________
>
> My work day may look different than your work day. Please do not feel
> obligated to read or respond to this email outside of your normal working
> hours.
>