1 ;;; page-layout.scm -- page breaking and page layout
3 ;;;; source file of the GNU LilyPond music typesetter
5 ;;;; (c) 2004 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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
33 (string-append "Engraved by LilyPond (version " (lilypond-version) ")"))
35 ;; TODO: take <optimally-broken-page-node> iso. page-number
36 ;; for all of these functions ?
38 (define-public (plain-header paper scopes page-number last?)
39 "Standard header for a part: page number --outside-- and instrument--centered."
41 (let* ((props (page-properties paper))
43 (if (ly:output-def-lookup paper 'printpagenumber)
44 (markup #:bold (number->string page-number))
46 (instr (ly:modules-lookup scopes 'instrument))
48 (line (list "" (if (markup? instr) instr "") pnum)))
50 (if (even? page-number)
51 (set! line (reverse line)))
53 (if ((if (ly:output-def-lookup paper 'printfirstpagenumber)
56 (ly:output-def-lookup paper 'firstpagenumber) page-number)
57 (interpret-markup paper props (make-fill-line-markup line))
60 ;; TODO: add publisher ID on non-first page.
61 (define-public (plain-footer paper scopes page-number last?)
62 "Standard footer. Empty, save for first (copyright) and last (tagline) page."
65 ((props (page-properties paper))
66 (copyright (ly:modules-lookup scopes 'copyright))
67 (tagline-var (ly:modules-lookup scopes 'tagline))
68 (tagline (if (markup? tagline-var) tagline-var TAGLINE))
73 (ly:stencil-combine-at-edge
74 stencil Y DOWN (interpret-markup paper props tagline)
78 (if (and (= 1 page-number)
82 (ly:stencil-combine-at-edge
83 stencil Y DOWN (interpret-markup paper props copyright)
89 (define (page-headfoot paper scopes number sym sepsym dir last?)
90 "Create a stencil including separating space."
92 ((header-proc (ly:output-def-lookup paper sym))
93 (sep (ly:output-def-lookup paper sepsym))
94 (stencil (ly:make-stencil "" '(0 . 0) '(0 . 0)))
96 (if (procedure? header-proc)
97 (header-proc paper scopes number last?)
100 (if (and (number? sep) (ly:stencil? head-stencil))
102 (ly:stencil-combine-at-edge
103 stencil Y dir head-stencil
108 (define-public (default-page-music-height paper scopes number last?)
109 "Printable area for music and titles; matches default-page-make-stencil."
111 ((h (- (ly:output-def-lookup paper 'vsize)
112 (ly:output-def-lookup paper 'topmargin)
113 (ly:output-def-lookup paper 'bottommargin)))
114 (head (page-headfoot paper scopes number 'make-header 'headsep UP last?))
115 (foot (page-headfoot paper scopes number 'make-footer 'footsep DOWN last?)))
116 (- h (if (ly:stencil? head)
117 (interval-length (ly:stencil-extent head Y))
119 (if (ly:stencil? foot)
120 (interval-length (ly:stencil-extent foot Y))
125 (define-public (default-page-make-stencil
126 lines offsets paper scopes number last? )
127 "Construct a stencil representing the page from LINES. "
129 ((topmargin (ly:output-def-lookup paper 'topmargin))
131 ;; TODO: naming vsize/hsize not analogous to TeX.
133 (vsize (ly:output-def-lookup paper 'vsize))
134 (hsize (ly:output-def-lookup paper 'hsize))
136 (lmargin (ly:output-def-lookup paper 'leftmargin))
137 (leftmargin (if lmargin
140 (ly:output-def-lookup paper 'linewidth)) 2)))
142 (rightmargin (ly:output-def-lookup paper 'rightmargin))
143 (bottom-edge (- vsize
144 (ly:output-def-lookup paper 'bottommargin)))
146 (head (page-headfoot paper scopes number 'make-header 'headsep UP last?))
147 (foot (page-headfoot paper scopes number 'make-footer 'footsep DOWN last?))
149 (head-height (if (ly:stencil? head)
150 (interval-length (ly:stencil-extent head Y))
153 (line-stencils (map ly:paper-system-stencil lines))
154 (height-proc (ly:output-def-lookup paper 'page-music-height))
156 (page-stencil (ly:make-stencil '()
157 (cons leftmargin hsize)
158 (cons (- topmargin) 0)))
160 (add-system (lambda (stencil-position)
163 (ly:stencil-translate-axis
164 (car stencil-position)
167 (cadr stencil-position)
175 "leftmargin" leftmargin "rightmargin" rightmargin
178 (set! page-stencil (ly:stencil-combine-at-edge
179 page-stencil Y DOWN head 0. 0.))
181 (map add-system (zip line-stencils offsets))
182 (if (ly:stencil? foot)
186 (ly:stencil-translate
190 (- (car (ly:stencil-extent foot Y)))))
193 (ly:stencil-translate page-stencil (cons leftmargin 0))
199 ;;; optimal page breaking
201 ;;; This is not optimal page breaking, this is optimal distribution of
202 ;;; lines over pages; line breaks are a given.
207 ; - separate function for word-wrap style breaking?
208 ; - raggedbottom? raggedlastbottom?
210 (define-public (ly:optimal-page-breaks
212 "Return pages as a list starting with 1st page. Each page is a list
216 (define MAXPENALTY 1e9)
217 (define bookpaper (ly:paper-book-book-paper paper-book))
218 (define scopes (ly:paper-book-scopes paper-book))
220 (define (page-height page-number last?)
222 ((p (ly:output-def-lookup bookpaper 'page-music-height)))
225 (p bookpaper scopes page-number last?)
228 (define (get-path node done)
229 "Follow NODE.PREV, and return as an ascending list of pages. DONE
230 is what have collected so far, and has ascending page numbers."
232 (if (is-a? node <optimally-broken-page-node>)
233 (get-path (node-prev node) (cons node done))
236 (define (combine-penalties force user best-paths)
238 ((prev-force (if (null? best-paths)
240 (node-force (car best-paths))))
241 (prev-penalty (if (null? best-paths)
243 (node-penalty (car best-paths))))
244 (inter-system-space (ly:output-def-lookup bookpaper 'betweensystemspace))
245 (force-equalization-factor 0.3)
246 (relative-force (/ force inter-system-space))
247 (abs-relative-force (abs relative-force))
251 (+ (* abs-relative-force (+ abs-relative-force 1))
253 (* force-equalization-factor (/ (abs (- prev-force force)) inter-system-space))
256 (define (space-systems page-height lines ragged?)
259 (ly:output-def-lookup bookpaper 'betweensystemspace))
260 (system-vector (list->vector
262 (if (= (length lines) 1)
270 ly:paper-system-staff-extents
272 (if (= (length lines) 1)
280 (lambda (sys) (ly:paper-system-extent sys Y)) lines)
281 (if (= (length lines) 1)
285 (no-systems (vector-length real-extents))
286 (topskip (cdr (vector-ref real-extents 0)))
287 (space-left (- page-height
288 (apply + (map interval-length (vector->list real-extents)))
292 (space (- page-height
294 (- (car (vector-ref real-extents (1- no-systems))))
297 (fixed-dist (ly:output-def-lookup bookpaper 'betweensystempadding))
301 ((this-system-ext (vector-ref staff-extents idx))
302 (next-system-ext (vector-ref staff-extents (1+ idx)))
303 (fixed (max 0 (- (+ (cdr next-system-ext)
305 (car this-system-ext))))
306 (title1? (and (vector-ref system-vector idx)
307 (ly:paper-system-title? (vector-ref system-vector idx))))
309 (vector-ref system-vector (1+ idx))
310 (ly:paper-system-title? (vector-ref system-vector (1+ idx)))))
313 ((and title2? title1?)
314 (ly:output-def-lookup bookpaper 'betweentitlespace))
316 (ly:output-def-lookup bookpaper 'aftertitlespace))
318 (ly:output-def-lookup bookpaper 'beforetitlespace))
319 (else inter-system-space))
321 (hooke (/ 1 (- ideal fixed)))
326 (springs (map calc-spring (iota (1- no-systems))))
330 ((this-system-ext (vector-ref real-extents idx))
331 (next-system-ext (vector-ref real-extents (1+ idx)))
332 (distance (max (- (+ (cdr next-system-ext)
334 (car this-system-ext)
336 (entry (list idx (1+ idx) distance)))
338 (rods (map calc-rod (iota (1- no-systems))))
340 ;; we don't set ragged based on amount space left.
341 ;; raggedbottomlast = ##T is much more predictable
342 (result (ly:solve-spring-rod-problem
346 (force (car (ly:solve-spring-rod-problem
347 springs rods space #f)))
356 (display (list "\n# systems: " no-systems
357 "\nreal-ext" real-extents "\nstaff-ext" staff-extents
358 "\ninterscore" inter-system-space
359 "\nspace-letf" space-left
360 "\npage empty" page-very-empty?
361 "\nspring,rod" springs rods
364 "\npage-height" page-height
368 "\npositions" positions "\n"))))
370 (cons force positions)))
372 (define (walk-paths done-lines best-paths current-lines last? current-best)
373 "Return the best optimal-page-break-node that contains
374 CURRENT-LINES. DONE-LINES.reversed ++ CURRENT-LINES is a consecutive
375 ascending range of lines, and BEST-PATHS contains the optimal breaks
376 corresponding to DONE-LINES.
378 CURRENT-BEST is the best result sofar, or #f."
380 (let* ((this-page-num (if (null? best-paths)
381 (ly:output-def-lookup bookpaper 'firstpagenumber)
382 (1+ (node-page-number (car best-paths)))))
385 (ragged? (or (eq? #t (ly:output-def-lookup bookpaper 'raggedbottom))
386 (and (eq? #t (ly:output-def-lookup bookpaper 'raggedlastbottom))
388 (page-height (page-height this-page-num last?))
389 (vertical-spacing (space-systems page-height current-lines ragged?))
390 (satisfied-constraints (car vertical-spacing))
391 (force (if satisfied-constraints satisfied-constraints 10000))
392 (positions (cdr vertical-spacing))
393 (user-penalty (ly:paper-system-break-penalty (car current-lines)))
394 (total-penalty (combine-penalties
401 (< total-penalty (node-penalty current-best))))
402 (new-best (if better?
403 (make <optimally-broken-page-node>
404 #:prev (if (null? best-paths)
407 #:lines current-lines
408 #:pageno this-page-num
410 #:configuration positions
411 #:penalty total-penalty)
417 "\nuser pen " user-penalty
418 "\nsatisfied-constraints" satisfied-constraints
419 "\nlast? " last? "ragged?" ragged?
420 "\nbetter? " better? " total-penalty " total-penalty "\n"
421 "\nconfig " positions
423 "\nlines: " current-lines "\n")))
426 (display (list "\nnew-best is " (node-lines new-best)
428 (if (null? best-paths)
430 (node-lines (car best-paths))))))
432 (if (and (pair? done-lines)
433 ;; if this page is too full, adding another line won't help
434 satisfied-constraints)
435 (walk-paths (cdr done-lines) (cdr best-paths)
436 (cons (car done-lines) current-lines)
441 (define (walk-lines done best-paths todo)
442 "Return the best page breaking as a single
443 <optimal-page-break-node> for optimally breaking TODO ++
444 DONE.reversed. BEST-PATHS is a list of break nodes corresponding to
448 (let* ((this-line (car todo))
449 (last? (null? (cdr todo)))
450 (next (walk-paths done best-paths (list this-line) last? #f)))
452 ; (display "\n***************")
453 (walk-lines (cons this-line done)
454 (cons next best-paths)
457 (define (line-number node)
458 (ly:paper-system-number (car (node-lines node))))
460 (let* ((best-break-node (walk-lines '() '() lines))
461 (break-nodes (get-path best-break-node '())))
463 (if #f; (ly:get-option 'verbose)
466 "\nbreaks: " (map line-number break-nodes))
467 "\nsystems " (map node-lines break-nodes)
468 "\npenalties " (map node-penalty break-nodes)
469 "\nconfigs " (map node-configuration break-nodes))))
475 ((ly:output-def-lookup bookpaper 'page-make-stencil)
477 (node-configuration node)
480 (node-page-number node)
481 (eq? node best-break-node)))