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)
}