Hi Kieren & al.,

Am 22.06.26 um 23:48 schrieb Kieren MacMillan:
Inspired by Jean’s No_break_during_tie_engraver, Lukas and I have been working 
on something that we are calling the Spanner_breaking_control_engraver.
I love this!!
Can’t wait to use it / see it in action.

Gabriel noticed a bug that I introduced when making last-minute changes (thus stopping the "length-limit" setting from working). Here's a corrected version.

It might be desirable to configure the engraver not by giving constant values (in the \with { ... }) clause, but to introduce context properties that can also be changed during the piece, as well as a context property switching the widow/orphan control on/off completely. What do you think?

Lukas
\version "2.26"

#(use-modules (ice-9 match))

#(define (context-mod-assignments context-mod)
   (filter-map
    (match-lambda
     (('assign key value)
      (cons key value))
     (_ #f))
    (ly:get-context-mods context-mod)))

#(define (grob-matches-names grob name-list)
   (memq (grob::name grob) name-list))

#(define (up-to-nth lst pred? n)
   ;; Returns the shortest leading sublist of lst such that
   ;; pred? is true for n elements of the sublist
   ;; (or the full list if such a sublist does not exist).
   (let loop ((lst lst)
              (n n))
     (if (or (zero? n)
             (null? lst))
         '()
         (match
          lst
          ((head . tail)
           (cons head
                 (loop tail
                       (if (pred? head)
                           (1- n)
                           n))))))))

#(define (mark-columns-unbreakable column-list)
   ;;
   ;; column-list is a chronological list of lists of the form
   ;; (paper-column do-not-touch? ...)
   ;; where
   ;; 'do-not-touch?' indicates whether the column's line-break-permission is
   ;; taboo for us (because a user override should be respected).
   ;;
   ;; This procedure prevents breaks between the first and last column in the list;
   ;; i.e. the first column does not get a no-break marking.
   ;;
   (unless (null? column-list)
     (for-each
      (match-lambda
       ((column do-not-touch? . _)
        (unless do-not-touch?
          (ly:grob-set-property! column 'line-break-permission '()))))
      (cdr column-list))))

Spanner_breaking_control_engraver =
#(define-scheme-function (spanner-names grob-names settings)
   (symbol-list-or-symbol? symbol-list-or-symbol? ly:context-mod?)

   (let*
    ((spanners (ensure-list spanner-names))
     (grobs (ensure-list grob-names))
     (settings-alist (context-mod-assignments settings))
     ;;
     ;; widow-limit = p: Keep the first p notes under the spanner together (unbroken)
     ;; orphan-limit = q: Keep the last q notes under the spanner together (unbroken)
     ;; length-limit = n: only break spanners that contain more than n notes
     ;;
     (widow-limit (assq-ref settings-alist 'widow-limit))
     (orphan-limit (assq-ref settings-alist 'orphan-limit))
     (length-limit (assq-ref settings-alist 'length-limit)))

    (lambda (context)
      (let
       ((active-spanners '())
        (starting-spanners '())
        (ending-spanners '())
        (found-relevant-grob-in-timestep #f)
        (breaking-control-override-in-timestep #f)
        (spanner-columns (make-hash-table)))

       (make-engraver
        (acknowledgers
         ((spanner-interface engraver spanner source-engraver)
          (when (grob-matches-names spanner spanner-names)
            (set! starting-spanners (cons spanner starting-spanners))))

         ((grob-interface engraver grob source-engraver)
          (when (grob-matches-names grob grob-names)
            (set! found-relevant-grob-in-timestep #t))))

        (end-acknowledgers
         ((spanner-interface engraver spanner source-engraver)
          ;; An end-of-slur gets announced in the timestep *after* the last note.
          (when (grob-matches-names spanner spanner-names)
            (set! ending-spanners (cons spanner ending-spanners)))))

        ((process-music engraver)
         (set! breaking-control-override-in-timestep (ly:context-property context 'forceBreak #f)))

        ((stop-translation-timestep engraver)
         ;; First, remove spanners that have ended from the list of active spanners.
         (for-each
          (lambda (ending-spanner)
            (set! active-spanners (delete! ending-spanner active-spanners)))
          ending-spanners)

         ;; Then, add spanners that have started to the list of active spanners.
         (set! active-spanners (append starting-spanners active-spanners))
         (set! starting-spanners '())

         ;; Next, register current paper column for each active spanner.
         ;; These are the timesteps that, if they lie inside a short spanner, will get
         ;; a “don’t break here” setting.
         ;; We record each paper column as a list
         ;;   (column do-not-touch? found-grob?)
         ;; where
         ;; - do-not-touch? indicates whether there was a user override for breaking
         ;;   control in this time step, meaning that we should not modify the
         ;;   column’s break-permission.
         ;;   (Note that, unintuitively, also an \allowBreak command causes
         ;;   forceBreak to be set to true in the context.)
         ;; - found-grob? indicates whether we found any interesting grobs in the spanner
         ;;   for this paper column (for counting the grobs later).
         (let ((current-command-column (ly:context-property context 'currentCommandColumn)))
           (for-each
            (lambda (active-spanner)
              (hash-set! spanner-columns
                         active-spanner
                         (cons (list current-command-column
                                     breaking-control-override-in-timestep
                                     found-relevant-grob-in-timestep)
                               (hash-ref spanner-columns active-spanner '()))))
            active-spanners))
         (set! found-relevant-grob-in-timestep #f)

         ;; Finally, process ended spanners and remove them from hash tables for saving memory.
         (for-each
          (lambda (ending-spanner)
            ;;
            ;;            .. -------------------------------------- ..
            ;;          /                                              \
            ;;        /                                                  \
            ;;    :  a    :  :  b       :      :  c     :         :    :  d
            ;;    : |     :  : |        :      : |      :         :    : |
            ;;    : |     :  : |        :      : |      :         :    : |
            ;;    :       :  :          :      :        :         :    :
            ;;    :       :  :          :      :        :         :    :
            ;;    1       2  3          4      5        6         7    8
            ;;   (a)        (b)               (c)                     (d)
            ;;
            ;; To keep note heads a, b, c and together (widow control), we must mark
            ;; columns 2, 3, 4, 5 as non-breaking.
            ;;
            ;; To keep note heads b, c, d together (orphan control), we must mark
            ;; columns 4, 5, 6, 7, 8 as non-breaking.
            ;;
            (let ((columns-flags (reverse (hash-ref spanner-columns ending-spanner '()))))
              (hash-remove! spanner-columns ending-spanner)
              (when widow-limit
                (mark-columns-unbreakable
                 (up-to-nth columns-flags
                                 third
                                 widow-limit)))
              (when orphan-limit
                (mark-columns-unbreakable
                 (reverse (up-to-nth (reverse columns-flags)
                                          third
                                          orphan-limit))))
              (when (and length-limit
                         (<= (length (filter third columns-flags))
                             length-limit))
                (mark-columns-unbreakable columns-flags))))
          ending-spanners)

         (set! breaking-control-override-in-timestep #f)
         (set! ending-spanners '())))))))

\layout {
  \context {
    \Voice
    \consists \Spanner_breaking_control_engraver Slur NoteHead \with {
      widow-limit = 2
      orphan-limit = 2
      length-limit = 4
    }
  }
  system-count = 100000
  ragged-right = ##t
}

\relative {
  1( s1 1)
  \bar "||"
  1( 1 s1 1)
  \bar "||"
  1( 1 1 1)
  \bar "||"
  1( 1 1 s1 1)
  \bar "||"
  1( 1 1 1 1 s1 s1 1 1 1 1 1 1 1)
  \bar "||"
}

\relative {
  c'1( r d e f)
}

Reply via email to