]> git.donarmstrong.com Git - lilypond.git/blob - scm/layout-page-layout.scm
e370a171323254050179f6e777eea2bece37b859
[lilypond.git] / scm / layout-page-layout.scm
1 ;;;; layout-page-layout.scm -- page breaking and page layout
2 ;;;;
3 ;;;;  source file of the GNU LilyPond music typesetter
4 ;;;;
5 ;;;; (c) 2004--2006 Jan Nieuwenhuizen <janneke@gnu.org>
6 ;;;;          Han-Wen Nienhuys <hanwen@xs4all.nl>
7
8 (define-module (scm layout-page-layout)
9   #:use-module (srfi srfi-1)
10   #:use-module (oop goops describe)
11   #:use-module (oop goops)
12   #:use-module (scm paper-system)
13   #:use-module (scm page)
14   #:use-module (scm layout-page-dump)
15   #:use-module (lily)
16   #:export (post-process-pages optimal-page-breaks make-page-from-systems
17             page-breaking-wrapper
18             ;; utilities for writing custom page breaking functions
19             line-height line-next-space line-next-padding
20             line-minimum-distance line-ideal-distance
21             first-line-position
22             line-ideal-relative-position line-minimum-relative-position
23             line-minimum-position-on-page stretchable-line?
24             page-maximum-space-to-fill page-maximum-space-left space-systems))
25
26 (define (page-breaking-wrapper paper-book)
27   "Compute line and page breaks by calling the page-breaking paper variable,
28   then performs the post process function using the page-post-process paper
29   variable. Finally, return the pages."
30   (let* ((paper (ly:paper-book-paper paper-book))
31          (pages ((ly:output-def-lookup paper 'page-breaking) paper-book)))
32     ((ly:output-def-lookup paper 'page-post-process) paper pages)
33     pages))
34
35 (define (post-process-pages layout pages)
36   "If the write-page-layout paper variable is true, dumps page breaks
37   and tweaks."
38   (if (ly:output-def-lookup layout 'write-page-layout #f)
39       (write-page-breaks pages)))
40
41 ;;;
42 ;;; Utilities for computing line distances and positions
43 ;;;
44 (define (line-height line)
45   "Return the system height, that is the length of its vertical extent."
46   (interval-length (paper-system-extent line Y)))
47
48 (define (line-next-space line next-line layout)
49   "Return space to use between `line' and `next-line'.
50   `next-line' can be #f, meaning that `line' is the last line."
51   (let* ((title (paper-system-title? line))
52          (next-title (and next-line (paper-system-title? next-line))))
53     (cond ((and title next-title)
54            (ly:output-def-lookup layout 'between-title-space))
55           (title
56            (ly:output-def-lookup layout 'after-title-space))
57           (next-title
58            (ly:output-def-lookup layout 'before-title-space))
59           (else
60            (ly:prob-property
61             line 'next-space
62             (ly:output-def-lookup layout 'between-system-space))))))
63
64 (define (line-next-padding line next-line layout)
65   "Return padding to use between `line' and `next-line'.
66   `next-line' can be #f, meaning that `line' is the last line."
67   (ly:prob-property
68    line 'next-padding
69    (ly:output-def-lookup layout 'between-system-padding)))
70
71
72 (define (line-minimum-distance line next-line layout ignore-padding)
73   "Minimum distance between `line' reference position and `next-line'
74  reference position. If next-line is #f, return #f."
75   (and next-line
76        (max 0 (- (+ (interval-end (paper-system-extent next-line Y))
77                     (if ignore-padding 0 (line-next-padding line next-line layout)))
78                  (interval-start (paper-system-extent line Y))))))
79
80 (define (line-ideal-distance line next-line layout ignore-padding)
81   "Ideal distance between `line' reference position and `next-line'
82  reference position. If next-line is #f, return #f."
83   (and next-line
84        (+ (max 0 (- (+ (interval-end (paper-system-staff-extents next-line))
85                        (if ignore-padding 0 (line-next-padding line next-line layout)))
86                     (interval-start (paper-system-staff-extents line))))
87           (line-next-space line next-line layout))))
88
89 (define (first-line-position line layout)
90   "Position of the first line on page"
91   (max (+ (ly:output-def-lookup layout 'page-top-space)
92           (interval-end (paper-system-staff-extents line)))
93        (interval-end (paper-system-extent line Y))))
94
95 (define (line-ideal-relative-position line prev-line layout ignore-padding)
96   "Return ideal position of `line', relative to `prev-line' position.
97   `prev-line' can be #f, meaning that `line' is the first line."
98   (if (not prev-line)
99       ;; first line on page
100       (first-line-position line layout)
101       ;; not the first line on page
102       (max (line-minimum-distance prev-line line layout ignore-padding)
103            (line-ideal-distance prev-line line layout ignore-padding))))
104
105 (define (line-minimum-relative-position line prev-line layout ignore-padding)
106   "Return position of `line', relative to `prev-line' position.
107   `prev-line' can be #f, meaning that `line' is the first line."
108   (if (not prev-line)
109       ;; first line on page
110       (first-line-position line layout)
111       ;; not the first line on page
112       (line-minimum-distance prev-line line layout ignore-padding)))
113
114 (define (line-minimum-position-on-page line prev-line prev-position page)
115   "If `line' fits on `page' after `prev-line', which position on page is
116   `prev-position', then return the line's postion on page, otherwise #f.
117   `prev-line' can be #f, meaning that `line' is the first line."
118   (let* ((layout (ly:paper-book-paper (page-property page 'paper-book)))
119          (position (+ (line-minimum-relative-position line prev-line layout #f)
120                       (if prev-line prev-position 0.0)))
121          (bottom-position (- position
122                              (interval-start (paper-system-extent line Y)))))
123     (and (or (not prev-line)
124              (< bottom-position (page-printable-height page)))
125          position)))
126
127 (define (stretchable-line? line)
128   "Say whether a system can be stretched."
129   (not (or (ly:prob-property? line 'is-title)
130            (let ((system-extent (paper-system-staff-extents line)))
131              (= (interval-start system-extent)
132                 (interval-end   system-extent))))))
133
134 (define (page-maximum-space-to-fill page lines paper)
135   "Return the space between the first line top position and the last line
136   bottom position. This constitutes the maximum space to fill on `page'
137   with `lines'."
138   (let ((last-line (car (last-pair lines))))
139     (- (page-printable-height page)
140        (first-line-position (first lines) paper)
141        (ly:prob-property last-line
142                          'bottom-space 0.0)
143        (- (interval-start (paper-system-extent last-line Y))))))
144
145 (define (page-maximum-space-left page)
146   (let ((paper (ly:paper-book-paper (page-property page 'paper-book))))
147     (let bottom-position ((lines (page-property page 'lines))
148                           (prev-line #f)
149                           (prev-position #f))
150       (if (null? lines)
151           (page-printable-height page)
152           (let* ((line (first lines))
153                  (position (line-minimum-position-on-page
154                             line prev-line prev-position page)))
155             (if (null? (cdr lines))
156                 (and position
157                      (- (page-printable-height page)
158                         (- position
159                            (interval-start (paper-system-extent line Y)))))
160                 (bottom-position (cdr lines) line position)))))))
161
162 ;;;
163 ;;; Utilities for distributing systems on a page
164 ;;;
165
166 (define (space-systems space-to-fill lines ragged paper ignore-padding)
167   "Compute lines positions on page: return force and line positions as a pair.
168  force is #f if lines do not fit on page."
169   (let* ((empty-stencil (ly:make-stencil '() '(0 . 0) '(0 . 0)))
170          (empty-prob (ly:make-prob 'paper-system (list `(stencil . ,empty-stencil))))
171          (cdr-lines (append (cdr lines)
172                             (if (<= (length lines) 1)
173                                 (list empty-prob)
174                                 '())))
175          (springs (map (lambda (prev-line line)
176                          (list (line-ideal-distance prev-line line paper ignore-padding)
177                                (/ 1.0 (line-next-space prev-line line paper))))
178                        lines
179                        cdr-lines))
180          (rods (map (let ((i -1))
181                       (lambda (prev-line line)
182                         (set! i (1+ i))
183                         (list i (1+ i)
184                               (line-minimum-distance prev-line line paper ignore-padding))))
185                        lines
186                        cdr-lines))
187          (space-result
188           (ly:solve-spring-rod-problem springs rods space-to-fill ragged)))
189     (cons (car space-result)
190           (map (let ((topskip (first-line-position (first lines) paper)))
191                  (lambda (y)
192                    (+ y topskip)))
193                (cdr space-result)))))
194
195 (define (make-page-from-systems paper-book lines page-number ragged last)
196   "Return a new page, filled with `lines'."
197   (let* ((page (make-page paper-book
198                           'lines lines
199                           'page-number page-number
200                           'is-last last))
201          (posns (if (null? lines)
202                     (list)
203                     (let* ((paper (ly:paper-book-paper paper-book))
204                            (space-to-fill (page-maximum-space-to-fill
205                                             page lines paper))
206                            (spacing (space-systems space-to-fill lines ragged paper #f)))
207                       (if (or (not (car spacing)) (inf? (car spacing)))
208                           (cdr (space-systems space-to-fill lines ragged paper #t))
209                           (cdr spacing))))))
210     (page-set-property! page 'configuration posns)
211     page))
212
213 ;;;
214 ;;; Page breaking function
215 ;;;
216
217 ;; Optimal distribution of
218 ;; lines over pages; line breaks are a given.
219
220 ;; TODO:
221 ;;
222 ;; - density scoring
223 ;; - separate function for word-wrap style breaking?
224 ;; - ragged-bottom? ragged-last-bottom?
225
226 (define (get-path node done)
227   "Follow NODE.PREV, and return as an ascending list of pages. DONE
228 is what have collected so far, and has ascending page numbers."
229   (if (page? node)
230       (get-path (page-prev node) (cons node done))
231       done))
232
233 (define (combine-penalties force user best-paths
234                            inter-system-space force-equalization-factor)
235   (let* ((prev-force (if (null? best-paths)
236                          0.0
237                          (page-force (car best-paths))))
238          (prev-penalty (if (null? best-paths)
239                            0.0
240                            (page-penalty (car best-paths))))
241          (relative-force (/ force inter-system-space))
242          (abs-relative-force (abs relative-force)))
243     (+ (* abs-relative-force (+ abs-relative-force 1))
244        prev-penalty
245        (* force-equalization-factor (/ (abs (- prev-force force))
246                                        inter-system-space))
247        user)))
248
249 (define (walk-paths done-lines best-paths current-lines last current-best
250                     paper-book page-alist)
251   "Return the best optimal-page-break-node that contains
252 CURRENT-LINES. DONE-LINES.reversed ++ CURRENT-LINES is a consecutive
253 ascending range of lines, and BEST-PATHS contains the optimal breaks
254 corresponding to DONE-LINES.
255
256 CURRENT-BEST is the best result sofar, or #f."
257   (let* ((paper (ly:paper-book-paper paper-book))
258          (this-page (make-page
259                      paper-book
260                      'is-last last
261                      'page-number (if (null? best-paths)
262                                       (ly:output-def-lookup paper 'first-page-number)
263                                       (1+ (page-page-number (first best-paths))))))
264          (ragged-all (eq? #t (ly:output-def-lookup paper 'ragged-bottom)))
265          (ragged-last (eq? #t (ly:output-def-lookup paper 'ragged-last-bottom)))
266          (ragged (or ragged-all (and ragged-last last)))
267          (space-to-fill (page-maximum-space-to-fill this-page current-lines paper))
268          (vertical-spacing (space-systems space-to-fill current-lines ragged paper #f))
269          (satisfied-constraints (car vertical-spacing))
270          (force (if satisfied-constraints
271                     (if (and last ragged-last)
272                         0.0
273                         satisfied-constraints)
274                     10000))
275          (positions (cdr vertical-spacing))
276          (get-break-penalty (lambda (sys)
277                               (ly:prob-property sys 'penalty 0.0)))
278          (user-nobreak-penalties (- (apply + (filter negative?
279                                                      (map get-break-penalty
280                                                           (cdr current-lines))))))
281          (user-penalty (+ (max (get-break-penalty (car current-lines)) 0.0)
282                           user-nobreak-penalties))
283          (total-penalty (combine-penalties
284                          force user-penalty best-paths
285                          (ly:output-def-lookup paper 'between-system-space)
286                          (ly:output-def-lookup paper 'verticalequalizationfactor 0.3)))
287          (new-best (if (or (not current-best)
288                            (and satisfied-constraints
289                                 (< total-penalty (page-penalty current-best))))
290                        (begin
291                          (map (lambda (x)
292                                 (page-set-property! this-page
293                                                     (car x)
294                                                     (cdr x)))
295                               (list (cons 'prev (if (null? best-paths)
296                                                     #f
297                                                     (car best-paths)))
298                                     (cons 'lines current-lines)
299                                     (cons 'force force)
300                                     (cons 'configuration positions)
301                                     (cons 'penalty total-penalty)))
302                          this-page)
303                        current-best)))
304     (if #f ;; debug
305         (display
306          (list
307           "\nuser pen " user-penalty
308           "\nsatisfied-constraints" satisfied-constraints
309           "\nlast? " last "ragged?" ragged
310           "\nis-better " is-better " total-penalty " total-penalty "\n"
311           "\nconfig " positions
312           "\nforce " force
313           "\nlines: " current-lines "\n")))
314     (if #f ; debug
315         (display (list "\nnew-best is " (page-lines new-best)
316                        "\ncontinuation of "
317                        (if (null? best-paths)
318                            "start"
319                            (page-lines (car best-paths))))))
320     (if (and (pair? done-lines)
321              ;; if this page is too full, adding another line won't help
322              satisfied-constraints)
323         (walk-paths (cdr done-lines) (cdr best-paths)
324                     (cons (car done-lines) current-lines)
325                     last new-best
326                     paper-book page-alist)
327         new-best)))
328
329 (define (walk-lines done best-paths todo paper-book page-alist)
330   "Return the best page breaking as a single
331 page node for optimally breaking TODO ++
332 DONE.reversed. BEST-PATHS is a list of break nodes corresponding to
333 DONE."
334   (if (null? todo)
335       (car best-paths)
336       (let* ((this-line (car todo))
337              (last (null? (cdr todo)))
338              (next (walk-paths done best-paths (list this-line) last #f
339                                paper-book page-alist)))
340         (walk-lines (cons this-line done)
341                     (cons next best-paths)
342                     (cdr todo)
343                     paper-book
344                     page-alist))))
345
346 (define-public (optimal-page-breaks paper-book)
347   "Return pages as a list starting with 1st page. Each page is a 'page Prob."
348   (let* ((paper (ly:paper-book-paper paper-book))
349          (lines (ly:paper-book-systems paper-book))
350          (page-alist (layout->page-init paper)) 
351          (force-equalization-factor (ly:output-def-lookup
352                                      paper 'verticalequalizationfactor 0.3)))
353     (ly:message (_ "Calculating page breaks..."))
354     (let* ((best-break-node (walk-lines '() '() lines paper-book page-alist))
355            (break-nodes (get-path best-break-node '())))
356       (page-set-property! (car (last-pair break-nodes)) 'is-last #t)
357       (if #f; (ly:get-option 'verbose)
358           (begin
359             (display (list
360                       "\nbreaks: " (map (lambda (node)
361                                           (ly:prob-property (car (page-lines node))
362                                                             'number))
363                                         break-nodes)
364                       "\nsystems " (map page-lines break-nodes)
365                       "\npenalties " (map page-penalty break-nodes)
366                       "\nconfigs " (map page-configuration break-nodes)))))
367       ;; construct page stencils.
368       (for-each page-stencil break-nodes)
369       break-nodes)))