*** empty log message ***
[lilypond/patrick.git] / scm / page-layout.scm
blob08590f2f89d99865eecb08071f437d122d10cab7
1 ;;;; page-layout.scm -- page breaking and page layout
2 ;;;;
3 ;;;;  source file of the GNU LilyPond music typesetter
4 ;;;;
5 ;;;; (c) 2004--2005 Jan Nieuwenhuizen <janneke@gnu.org>
6 ;;;;          Han-Wen Nienhuys <hanwen@cs.uu.nl>
8 (use-modules (oop goops describe)
9              (oop goops))
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))
24        (list
25         "Page " (node-page-number node)
26         " Lines: " (node-lines node)
27         " Penalty " (node-penalty node)
28         "\n")))
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)))
40        (head-stencil
41         (if (procedure? header-proc)
42             (header-proc layout scopes number last?)
43             #f)))
45     (if (and (number? sep)
46              (ly:stencil? head-stencil)
47              (not (ly:stencil-empty? head-stencil)))
48         (set! head-stencil
49               (ly:stencil-combine-at-edge
50                stencil Y dir head-stencil
51                sep 0.0)))
53     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?))
62        (available
63         (- h (if (ly:stencil? head)
64                  (interval-length (ly:stencil-extent head Y))
65                  0)
66            (if (ly:stencil? foot)
67                (interval-length (ly:stencil-extent foot Y))
68                0))))
70     ;; (display (list "\n available" available head foot))
71     available))
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
78 create offsets.
79  "
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-separator-markup (ly:output-def-lookup layout 'systemSeparatorMarkup))
89          (system-separator-stencil (if (markup? system-separator-markup)
90                                        (interpret-markup layout
91                                                          (layout-extract-page-properties layout)
92                                                          system-separator-markup)
93                                        #f))
94          (lmargin (ly:output-def-lookup layout 'leftmargin))
95          (leftmargin (if lmargin
96                        lmargin
97                        (/ (- hsize
98                              (ly:output-def-lookup layout 'linewidth)) 2)))
100        (rightmargin (ly:output-def-lookup layout 'rightmargin))
101        (bottom-edge (- vsize
102                        (ly:output-def-lookup layout 'bottommargin)))
104        (head (page-headfoot layout scopes number 'make-header 'headsep UP last?))
105        (foot (page-headfoot layout scopes number 'make-footer 'footsep DOWN last?))
107        (head-height (if (ly:stencil? head)
108                         (interval-length (ly:stencil-extent head Y))
109                         0.0))
111        (height-proc (ly:output-def-lookup layout 'page-music-height))
113        (page-stencil (ly:make-stencil '()
114                                       (cons leftmargin hsize)
115                                       (cons (- topmargin) 0)))
116        (last-system #f)
117        (last-y 0.0)
118        (add-to-page (lambda (stencil y)
119                       (set! page-stencil
120                             (ly:stencil-add page-stencil
121                                             (ly:stencil-translate-axis stencil
122                                              (- 0 head-height y topmargin) Y)))))
123        (add-system
124         (lambda (stencil-position)
125           (let* ((system (car stencil-position))
126                  (stencil (ly:paper-system-stencil system))
127                  (y (cadr stencil-position))
128                  (is-title (ly:paper-system-title?
129                             (car stencil-position))))
130             (add-to-page stencil y)
131             (if (and (ly:stencil? system-separator-stencil)
132                      last-system
133                      (not (ly:paper-system-title? system))
134                      (not (ly:paper-system-title? last-system)))
135                 (add-to-page
136                  system-separator-stencil
137                  (average (- last-y
138                              (car (ly:paper-system-staff-extents last-system)))
139                           (- y
140                              (cdr (ly:paper-system-staff-extents system))))))
141             (set! last-system system)
142             (set! last-y y)))))
144     (if #f
145         (display (list
146                   "leftmargin " leftmargin "rightmargin " rightmargin
147                   )))
149     (set! page-stencil (ly:stencil-combine-at-edge
150                         page-stencil Y DOWN
151                         (if (and
152                              (ly:stencil? head)
153                              (not (ly:stencil-empty? head)))
154                             head
155                             (ly:make-stencil "" (cons 0 0) (cons 0 0)))
156                             0. 0.))
158     (map add-system (zip lines offsets))
159     (if (and (ly:stencil? foot)
160              (not (ly:stencil-empty? foot)))
161         (set! page-stencil
162               (ly:stencil-add
163                page-stencil
164                (ly:stencil-translate
165                 foot
166                 (cons 0
167                       (+ (- bottom-edge)
168                          (- (car (ly:stencil-extent foot Y)))))))))
170     (ly:stencil-translate page-stencil (cons leftmargin 0))))
172 ;;; optimal page breaking
174 ;;; This is not optimal page breaking, this is optimal distribution of
175 ;;; lines over pages; line breaks are a given.
177 ;; TODO:
179 ;; - density scoring
180 ;; - separate function for word-wrap style breaking?
181 ;; - raggedbottom? raggedlastbottom?
183 (define-public (ly:optimal-page-breaks
184                 lines paper-book)
185   "Return pages as a list starting with 1st page. Each page is a list
186 of lines. "
189   (define MAXPENALTY 1e9)
190   (define paper (ly:paper-book-paper paper-book))
191   (define scopes (ly:paper-book-scopes paper-book))
193   (define (page-height page-number last?)
194     (let ((p (ly:output-def-lookup paper 'page-music-height)))
196       (if (procedure? p)
197           (p paper scopes page-number last?)
198           10000)))
200   (define (get-path node done)
201     "Follow NODE.PREV, and return as an ascending list of pages. DONE
202 is what have collected so far, and has ascending page numbers."
204     (if (is-a? node <optimally-broken-page-node>)
205         (get-path (node-prev node) (cons node done))
206         done))
208   (define (combine-penalties force user best-paths)
209     (let* ((prev-force (if (null? best-paths)
210                            0.0
211                            (node-force (car best-paths))))
212            (prev-penalty (if (null? best-paths)
213                              0.0
214                              (node-penalty (car best-paths))))
215          (inter-system-space (ly:output-def-lookup paper 'betweensystemspace))
216          (force-equalization-factor 0.3)
217          (relative-force (/ force inter-system-space))
218          (abs-relative-force (abs relative-force)))
221       (+ (* abs-relative-force (+ abs-relative-force 1))
222          prev-penalty
223          (* force-equalization-factor (/ (abs (- prev-force force)) inter-system-space))
224          user)))
226   (define (space-systems page-height lines ragged?)
227     (let* ((inter-system-space
228             (ly:output-def-lookup paper 'betweensystemspace))
229            (system-vector (list->vector
230                            (append lines
231                                    (if (= (length lines) 1)
232                                        '(#f)
233                                        '()))))
234          (staff-extents
235           (list->vector
236            (append (map ly:paper-system-staff-extents lines)
237                    (if (= (length lines) 1)
238                        '((0 . 0))
239                        '()))))
240          (real-extents
241           (list->vector
242            (append
243             (map
244              (lambda (sys) (ly:paper-system-extent sys Y)) lines)
245             (if (= (length lines) 1)
246                 '((0 .  0))
247                 '()))))
248          (no-systems (vector-length real-extents))
249          (topskip (interval-end (vector-ref real-extents 0)))
250          (space-left (- page-height
251                         (apply + (map interval-length (vector->list real-extents)))))
253          (space (- page-height
254                    topskip
255                    (-  (interval-start (vector-ref real-extents (1- no-systems))))))
257          (fixed-dist (ly:output-def-lookup paper 'betweensystempadding))
258          (calc-spring
259           (lambda (idx)
260             (let* ((this-system-ext (vector-ref staff-extents idx))
261                  (next-system-ext (vector-ref staff-extents (1+ idx)))
262                  (fixed (max 0 (- (+ (interval-end next-system-ext)
263                                       fixed-dist)
264                                    (interval-start this-system-ext))))
265                  (title1? (and (vector-ref system-vector idx)
266                                (ly:paper-system-title? (vector-ref system-vector idx))))
267                  (title2? (and
268                            (vector-ref system-vector (1+ idx))
269                            (ly:paper-system-title? (vector-ref system-vector (1+ idx)))))
270                  (ideal (+
271                          (cond
272                           ((and title2? title1?)
273                            (ly:output-def-lookup paper 'betweentitlespace))
274                           (title1?
275                            (ly:output-def-lookup paper 'aftertitlespace))
276                           (title2?
277                            (ly:output-def-lookup paper 'beforetitlespace))
278                           (else inter-system-space))
279                          fixed))
280                  (hooke (/ 1 (- ideal fixed))))
281               (list ideal hooke))))
283          (springs (map calc-spring (iota (1- no-systems))))
284          (calc-rod
285           (lambda (idx)
286             (let* ((this-system-ext (vector-ref real-extents idx))
287                  (next-system-ext (vector-ref real-extents (1+ idx)))
288                  (distance (max  (- (+ (interval-end next-system-ext)
289                                        fixed-dist)
290                                     (interval-start this-system-ext)
291                                     ) 0))
292                  (entry (list idx (1+ idx) distance)))
293               entry)))
294          (rods (map calc-rod (iota (1- no-systems))))
296          ;; we don't set ragged based on amount space left.
297          ;; raggedbottomlast = ##T is much more predictable
298          (result (ly:solve-spring-rod-problem
299                   springs rods space
300                   ragged?))
302          (force (car result))
303          (positions
304           (map (lambda (y)
305                  (+ y topskip))
306                (cdr  result))))
308       (if #f ;; debug.
309           (begin
310             (display (list "\n# systems: " no-systems
311                            "\nreal-ext" real-extents "\nstaff-ext" staff-extents
312                            "\ninterscore" inter-system-space
313                            "\nspace-letf" space-left
314                            "\nspring,rod" springs rods
315                            "\ntopskip " topskip
316                            " space " space
317                            "\npage-height" page-height
318                            "\nragged" ragged?
319                            "\nforce" force
320                            "\nres" (cdr result)
321                            "\npositions" positions "\n"))))
323       (cons force positions)))
325   (define (walk-paths done-lines best-paths current-lines  last? current-best)
326     "Return the best optimal-page-break-node that contains
327 CURRENT-LINES.  DONE-LINES.reversed ++ CURRENT-LINES is a consecutive
328 ascending range of lines, and BEST-PATHS contains the optimal breaks
329 corresponding to DONE-LINES.
331 CURRENT-BEST is the best result sofar, or #f."
333     (let* ((this-page-num (if (null? best-paths)
334                               (ly:output-def-lookup paper 'firstpagenumber)
335                               (1+ (node-page-number (car best-paths)))))
337            (ragged-all? (eq? #t (ly:output-def-lookup paper 'raggedbottom)))
338            (ragged-last? (eq? #t (ly:output-def-lookup paper 'raggedlastbottom)))
339            (ragged? (or ragged-all?
340                         (and ragged-last?
341                              last?)))
342            (page-height (page-height this-page-num last?))
343            (vertical-spacing (space-systems page-height current-lines ragged?))
344            (satisfied-constraints (car vertical-spacing))
345            (force (if satisfied-constraints
346                       (if (and last? ragged-last?)
347                           0.0
348                           satisfied-constraints)
349                       10000))
350            (positions (cdr vertical-spacing))
351            (user-nobreak-penalties
352             (-
353              (apply + (filter negative?
354                               (map ly:paper-system-break-before-penalty
355                                    (cdr current-lines))))))
356            (user-penalty
357             (+
358              (max (ly:paper-system-break-before-penalty (car current-lines)) 0.0)
359              user-nobreak-penalties))
360            (total-penalty (combine-penalties
361                            force user-penalty
362                            best-paths))
365            (better? (or
366                      (not current-best)
367                      (< total-penalty (node-penalty current-best))))
368            (new-best (if better?
369                          (make <optimally-broken-page-node>
370                            #:prev (if (null? best-paths)
371                                       #f
372                                       (car best-paths))
373                            #:lines current-lines
374                            #:pageno this-page-num
375                            #:force force
376                            #:configuration positions
377                            #:penalty total-penalty)
378                          current-best)))
380       (if #f ;; debug
381           (display
382            (list
383             "\nuser pen " user-penalty
384             "\nsatisfied-constraints" satisfied-constraints
385             "\nlast? " last? "ragged?" ragged?
386             "\nbetter? " better? " total-penalty " total-penalty "\n"
387             "\nconfig " positions
388             "\nforce " force
389             "\nlines: " current-lines "\n")))
391       (if #f ; debug
392           (display (list "\nnew-best is " (node-lines new-best)
393                          "\ncontinuation of "
394                          (if (null? best-paths)
395                              "start"
396                              (node-lines (car best-paths))))))
398       (if (and (pair? done-lines)
399                ;; if this page is too full, adding another line won't help
400                satisfied-constraints)
401           (walk-paths (cdr done-lines) (cdr best-paths)
402                       (cons (car done-lines) current-lines)
403                       last? new-best)
404           new-best)))
406   (define (walk-lines done best-paths todo)
407     "Return the best page breaking as a single
408 <optimal-page-break-node> for optimally breaking TODO ++
409 DONE.reversed. BEST-PATHS is a list of break nodes corresponding to
410 DONE."
411     (if (null? todo)
412         (car best-paths)
413         (let* ((this-line (car todo))
414                (last? (null? (cdr todo)))
415                (next (walk-paths done best-paths (list this-line) last? #f)))
417           ;; (display "\n***************")
418           (walk-lines (cons this-line done)
419                       (cons next best-paths)
420                       (cdr todo)))))
422   (define (line-number node)
423     (ly:paper-system-number (car (node-lines node))))
425   (ly:message (_ "Calculating page breaks..."))
427   (let* ((best-break-node (walk-lines '() '() lines))
428          (break-nodes (get-path best-break-node '()))
429          (last-node (car (last-pair break-nodes))))
431     (define (node->page-stencil node)
432       (if (not (eq? node last-node))
433           (ly:progress "["))
434       (let ((stencil
435              ((ly:output-def-lookup paper 'page-make-stencil)
436               (node-lines node)
437               (node-configuration node)
438               paper
439               scopes
440               (node-page-number node)
441               (eq? node best-break-node))))
442         (if (not (eq? node last-node))
443             (begin
444               (ly:progress (number->string
445                             (car (last-pair (node-system-numbers node)))))
446               (ly:progress "]")))
447         stencil))
449     (if #f; (ly:get-option 'verbose)
450         (begin
451           (display (list
452                     "\nbreaks: " (map line-number break-nodes))
453                    "\nsystems " (map node-lines break-nodes)
454                    "\npenalties " (map node-penalty break-nodes)
455                    "\nconfigs " (map node-configuration break-nodes))))
457     (let ((stencils (map node->page-stencil break-nodes)))
458       (ly:progress "\n")
459       stencils)))