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