1 ;;;; page-layout.scm -- page breaking and page layout
3 ;;;; source file of the GNU LilyPond music typesetter
5 ;;;; (c) 2004--2005 Jan Nieuwenhuizen <janneke@gnu.org>
6 ;;;; Han-Wen Nienhuys <hanwen@cs.uu.nl>
8 (use-modules (oop goops describe)
12 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14 (define-class <optimally-broken-page-node> ()
15 (prev #:init-value '() #:accessor node-prev #:init-keyword #:prev)
16 (page #:init-value 0 #:accessor node-page-number #:init-keyword #:pageno)
17 (force #:init-value 0 #:accessor node-force #:init-keyword #:force)
18 (penalty #:init-value 0 #:accessor node-penalty #:init-keyword #:penalty)
19 (configuration #:init-value '() #:accessor node-configuration #:init-keyword #:configuration)
20 (lines #:init-value 0 #:accessor node-lines #:init-keyword #:lines))
22 (define-method (display (node <optimally-broken-page-node>) port)
23 (map (lambda (x) (display x port))
25 "Page " (node-page-number node)
26 " Lines: " (node-lines node)
27 " Penalty " (node-penalty node)
30 (define-method (node-system-numbers (node <optimally-broken-page-node>))
31 (map ly:paper-system-number (node-lines node)))
33 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
35 (define (page-headfoot layout scopes number sym sepsym dir last?)
36 "Create a stencil including separating space."
37 (let* ((header-proc (ly:output-def-lookup layout sym))
38 (sep (ly:output-def-lookup layout sepsym))
39 (stencil (ly:make-stencil "" '(0 . 0) '(0 . 0)))
41 (if (procedure? header-proc)
42 (header-proc layout scopes number last?)
45 (if (and (number? sep)
46 (ly:stencil? head-stencil)
47 (not (ly:stencil-empty? head-stencil)))
49 (ly:stencil-combine-at-edge
50 stencil Y dir head-stencil
55 (define-public (default-page-music-height layout scopes number last?)
56 "Printable area for music and titles; matches default-page-make-stencil."
57 (let* ((h (- (ly:output-def-lookup layout 'vsize)
58 (ly:output-def-lookup layout 'topmargin)
59 (ly:output-def-lookup layout 'bottommargin)))
60 (head (page-headfoot layout scopes number 'make-header 'headsep UP last?))
61 (foot (page-headfoot layout scopes number 'make-footer 'footsep DOWN last?))
63 (- h (if (ly:stencil? head)
64 (interval-length (ly:stencil-extent head Y))
66 (if (ly:stencil? foot)
67 (interval-length (ly:stencil-extent foot Y))
70 ;; (display (list "\n available" available head foot))
73 (define-public (default-page-make-stencil
74 lines offsets layout scopes number last?)
75 "Construct a stencil representing the page from LINES.
77 Offsets is a list of increasing numbers. They must be negated to
81 (let* ((topmargin (ly:output-def-lookup layout 'topmargin))
83 ;; TODO: naming vsize/hsize not analogous to TeX.
85 (vsize (ly:output-def-lookup layout 'vsize))
86 (hsize (ly:output-def-lookup layout 'hsize))
88 (system-xoffset (ly:output-def-lookup layout 'horizontalshift 0.0))
89 (system-separator-markup (ly:output-def-lookup layout 'systemSeparatorMarkup))
90 (system-separator-stencil (if (markup? system-separator-markup)
91 (interpret-markup layout
92 (layout-extract-page-properties layout)
93 system-separator-markup)
95 (lmargin (ly:output-def-lookup layout 'leftmargin))
96 (leftmargin (if lmargin
99 (ly:output-def-lookup layout 'linewidth)) 2)))
101 (rightmargin (ly:output-def-lookup layout 'rightmargin))
102 (bottom-edge (- vsize
103 (ly:output-def-lookup layout 'bottommargin)))
105 (head (page-headfoot layout scopes number 'make-header 'headsep UP last?))
106 (foot (page-headfoot layout scopes number 'make-footer 'footsep DOWN last?))
108 (head-height (if (ly:stencil? head)
109 (interval-length (ly:stencil-extent head Y))
112 (height-proc (ly:output-def-lookup layout 'page-music-height))
114 (page-stencil (ly:make-stencil '()
115 (cons leftmargin hsize)
116 (cons (- topmargin) 0)))
119 (add-to-page (lambda (stencil y)
121 (ly:stencil-add page-stencil
122 (ly:stencil-translate stencil
125 (- 0 head-height y topmargin))
129 (lambda (stencil-position)
130 (let* ((system (car stencil-position))
131 (stencil (ly:paper-system-stencil system))
132 (y (cadr stencil-position))
133 (is-title (ly:paper-system-title?
134 (car stencil-position))))
135 (add-to-page stencil y)
136 (if (and (ly:stencil? system-separator-stencil)
138 (not (ly:paper-system-title? system))
139 (not (ly:paper-system-title? last-system)))
141 system-separator-stencil
143 (car (ly:paper-system-staff-extents last-system)))
145 (cdr (ly:paper-system-staff-extents system))))))
146 (set! last-system system)
151 "leftmargin " leftmargin "rightmargin " rightmargin
154 (set! page-stencil (ly:stencil-combine-at-edge
158 (not (ly:stencil-empty? head)))
160 (ly:make-stencil "" (cons 0 0) (cons 0 0)))
163 (map add-system (zip lines offsets))
164 (if (and (ly:stencil? foot)
165 (not (ly:stencil-empty? foot)))
169 (ly:stencil-translate
173 (- (car (ly:stencil-extent foot Y)))))))))
175 (ly:stencil-translate page-stencil (cons leftmargin 0))))
177 ;;; optimal page breaking
179 ;;; This is not optimal page breaking, this is optimal distribution of
180 ;;; lines over pages; line breaks are a given.
185 ;; - separate function for word-wrap style breaking?
186 ;; - raggedbottom? raggedlastbottom?
188 (define-public (optimal-page-breaks lines paper-book)
189 "Return pages as a list starting with 1st page. Each page is a list
193 (define MAXPENALTY 1e9)
194 (define paper (ly:paper-book-paper paper-book))
195 (define scopes (ly:paper-book-scopes paper-book))
196 (define force-equalization-factor #f)
198 (define (page-height page-number last?)
199 (let ((p (ly:output-def-lookup paper 'page-music-height)))
202 (p paper scopes page-number last?)
205 (define (get-path node done)
206 "Follow NODE.PREV, and return as an ascending list of pages. DONE
207 is what have collected so far, and has ascending page numbers."
209 (if (is-a? node <optimally-broken-page-node>)
210 (get-path (node-prev node) (cons node done))
213 (define (combine-penalties force user best-paths)
214 (let* ((prev-force (if (null? best-paths)
216 (node-force (car best-paths))))
217 (prev-penalty (if (null? best-paths)
219 (node-penalty (car best-paths))))
220 (inter-system-space (ly:output-def-lookup paper 'betweensystemspace))
221 (relative-force (/ force inter-system-space))
222 (abs-relative-force (abs relative-force)))
225 (+ (* abs-relative-force (+ abs-relative-force 1))
227 (* force-equalization-factor (/ (abs (- prev-force force))
231 (define (space-systems page-height lines ragged?)
232 (let* ((global-inter-system-space
233 (ly:output-def-lookup paper 'betweensystemspace))
235 (ly:output-def-lookup paper 'pagetopspace))
236 (global-fixed-dist (ly:output-def-lookup paper 'betweensystempadding))
238 (system-vector (list->vector
240 (if (= (length lines) 1)
245 (append (map ly:paper-system-staff-extents lines)
246 (if (= (length lines) 1)
254 (lambda (sys) (ly:paper-system-extent sys Y)) lines)
255 (if (= (length lines) 1)
259 (system-count (vector-length real-extents))
263 (interval-end (vector-ref staff-extents 0)))
264 (interval-end (vector-ref real-extents 0))
266 (last-system (vector-ref system-vector (1- system-count)))
267 (bottom-space (if (ly:paper-system? last-system)
268 (ly:paper-system-property last-system 'bottom-space 0.0)
270 (space-left (- page-height
272 (apply + (map interval-length
273 (vector->list real-extents)))))
275 (space (- page-height
279 (vector-ref real-extents (1- system-count))))))
284 (upper-system (vector-ref system-vector idx))
285 (between-space (ly:paper-system-property upper-system 'next-space
286 global-inter-system-space))
287 (fixed-dist (ly:paper-system-property upper-system 'next-padding
290 (this-system-ext (vector-ref staff-extents idx))
291 (next-system-ext (vector-ref staff-extents (1+ idx)))
292 (fixed (max 0 (- (+ (interval-end next-system-ext)
294 (interval-start this-system-ext))))
295 (title1? (and (vector-ref system-vector idx)
296 (ly:paper-system-title? (vector-ref system-vector idx))))
298 (vector-ref system-vector (1+ idx))
299 (ly:paper-system-title? (vector-ref system-vector (1+ idx)))))
302 ((and title2? title1?)
303 (ly:output-def-lookup paper 'betweentitlespace))
305 (ly:output-def-lookup paper 'aftertitlespace))
307 (ly:output-def-lookup paper 'beforetitlespace))
308 (else between-space))
310 (hooke (/ 1 (- ideal fixed))))
311 (list ideal hooke))))
313 (springs (map calc-spring (iota (1- system-count))))
317 (upper-system (vector-ref system-vector idx))
318 (fixed-dist (ly:paper-system-property upper-system 'next-padding
320 (this-system-ext (vector-ref real-extents idx))
321 (next-system-ext (vector-ref real-extents (1+ idx)))
323 (distance (max (- (+ (interval-end next-system-ext)
325 (interval-start this-system-ext)
327 (entry (list idx (1+ idx) distance)))
329 (rods (map calc-rod (iota (1- system-count))))
331 ;; we don't set ragged based on amount space left.
332 ;; raggedbottomlast = ##T is much more predictable
333 (result (ly:solve-spring-rod-problem
345 (display (list "\n# systems: " system-count
346 "\nreal-ext" real-extents "\nstaff-ext" staff-extents
347 "\ninterscore" inter-system-space
348 "\nspace-letf" space-left
349 "\nspring,rod" springs rods
352 "\npage-height" page-height
356 "\npositions" positions "\n"))))
358 (cons force positions)))
360 (define (walk-paths done-lines best-paths current-lines last? current-best)
361 "Return the best optimal-page-break-node that contains
362 CURRENT-LINES. DONE-LINES.reversed ++ CURRENT-LINES is a consecutive
363 ascending range of lines, and BEST-PATHS contains the optimal breaks
364 corresponding to DONE-LINES.
366 CURRENT-BEST is the best result sofar, or #f."
369 (let* ((this-page-num (if (null? best-paths)
370 (ly:output-def-lookup paper 'firstpagenumber)
371 (1+ (node-page-number (car best-paths)))))
373 (ragged-all? (eq? #t (ly:output-def-lookup paper 'raggedbottom)))
374 (ragged-last? (eq? #t (ly:output-def-lookup paper 'raggedlastbottom)))
375 (ragged? (or ragged-all?
378 (page-height (page-height this-page-num last?))
379 (vertical-spacing (space-systems page-height current-lines ragged?))
380 (satisfied-constraints (car vertical-spacing))
381 (force (if satisfied-constraints
382 (if (and last? ragged-last?)
384 satisfied-constraints)
386 (positions (cdr vertical-spacing))
387 (user-nobreak-penalties
389 (apply + (filter negative?
390 (map ly:paper-system-break-before-penalty
391 (cdr current-lines))))))
394 (max (ly:paper-system-break-before-penalty (car current-lines)) 0.0)
395 user-nobreak-penalties))
396 (total-penalty (combine-penalties
403 (< total-penalty (node-penalty current-best))))
404 (new-best (if better?
405 (make <optimally-broken-page-node>
406 #:prev (if (null? best-paths)
409 #:lines current-lines
410 #:pageno this-page-num
412 #:configuration positions
413 #:penalty total-penalty)
416 ;; (display total-penalty) (newline)
420 "\nuser pen " user-penalty
421 "\nsatisfied-constraints" satisfied-constraints
422 "\nlast? " last? "ragged?" ragged?
423 "\nbetter? " better? " total-penalty " total-penalty "\n"
424 "\nconfig " positions
426 "\nlines: " current-lines "\n")))
429 (display (list "\nnew-best is " (node-lines new-best)
431 (if (null? best-paths)
433 (node-lines (car best-paths))))))
435 (if (and (pair? done-lines)
436 ;; if this page is too full, adding another line won't help
437 satisfied-constraints)
438 (walk-paths (cdr done-lines) (cdr best-paths)
439 (cons (car done-lines) current-lines)
443 (define (walk-lines done best-paths todo)
444 "Return the best page breaking as a single
445 <optimal-page-break-node> for optimally breaking TODO ++
446 DONE.reversed. BEST-PATHS is a list of break nodes corresponding to
450 (let* ((this-line (car todo))
451 (last? (null? (cdr todo)))
452 (next (walk-paths done best-paths (list this-line) last? #f)))
454 ;; (display "\n***************")
455 (walk-lines (cons this-line done)
456 (cons next best-paths)
459 (define (line-number node)
460 (ly:paper-system-number (car (node-lines node))))
462 (ly:message (_ "Calculating page breaks..."))
463 (set! force-equalization-factor
464 (ly:output-def-lookup paper 'verticalequalizationfactor 0.3))
466 (let* ((best-break-node (walk-lines '() '() lines))
467 (break-nodes (get-path best-break-node '()))
468 (last-node (car (last-pair break-nodes))))
470 (define (node->page-stencil node)
471 (if (not (eq? node last-node))
474 ((ly:output-def-lookup paper 'page-make-stencil)
476 (node-configuration node)
479 (node-page-number node)
480 (eq? node best-break-node))))
481 (if (not (eq? node last-node))
483 (ly:progress (number->string
484 (car (last-pair (node-system-numbers node)))))
488 (if #f; (ly:get-option 'verbose)
491 "\nbreaks: " (map line-number break-nodes))
492 "\nsystems " (map node-lines break-nodes)
493 "\npenalties " (map node-penalty break-nodes)
494 "\nconfigs " (map node-configuration break-nodes))))
496 (let ((stencils (map node->page-stencil break-nodes)))