]> git.donarmstrong.com Git - lilypond.git/blob - lily/axis-group-interface.cc
Doc-de: update macros.itely and nitpicks
[lilypond.git] / lily / axis-group-interface.cc
1 /*
2   axis-group-interface.cc -- implement Axis_group_interface
3
4   source file of the GNU LilyPond music typesetter
5
6   (c) 2000--2009 Han-Wen Nienhuys <hanwen@xs4all.nl>
7 */
8
9 #include "axis-group-interface.hh"
10
11 #include "align-interface.hh"
12 #include "directional-element-interface.hh"
13 #include "pointer-group-interface.hh"
14 #include "grob-array.hh"
15 #include "hara-kiri-group-spanner.hh"
16 #include "international.hh"
17 #include "lookup.hh"
18 #include "paper-column.hh"
19 #include "paper-score.hh"
20 #include "separation-item.hh"
21 #include "skyline-pair.hh"
22 #include "stencil.hh"
23 #include "system.hh"
24 #include "warn.hh"
25
26 void
27 Axis_group_interface::add_element (Grob *me, Grob *e)
28 {
29   SCM axes = me->get_property ("axes");
30   if (!scm_is_pair (axes))
31     programming_error ("axes should be nonempty");
32
33   for (SCM ax = axes; scm_is_pair (ax); ax = scm_cdr (ax))
34     {
35       Axis a = (Axis) scm_to_int (scm_car (ax));
36
37       if (!e->get_parent (a))
38         e->set_parent (me, a);
39
40       e->set_object ((a == X_AXIS)
41                      ? ly_symbol2scm ("axis-group-parent-X")
42                      : ly_symbol2scm ("axis-group-parent-Y"),
43                      me->self_scm ());
44     }
45
46   /* must be ordered, because Align_interface also uses
47      Axis_group_interface  */
48   Pointer_group_interface::add_grob (me, ly_symbol2scm ("elements"), e);
49 }
50
51 bool
52 Axis_group_interface::has_axis (Grob *me, Axis a)
53 {
54   SCM axes = me->get_property ("axes");
55
56   return (SCM_BOOL_F != scm_memq (scm_from_int (a), axes));
57 }
58
59 Interval
60 Axis_group_interface::relative_group_extent (vector<Grob*> const &elts,
61                                              Grob *common, Axis a)
62 {
63   Interval r;
64   for (vsize i = 0; i < elts.size (); i++)
65     {
66       Grob *se = elts[i];
67       if (!to_boolean (se->get_property ("cross-staff")))
68         {
69           Interval dims = se->extent (common, a);
70           if (!dims.is_empty ())
71             r.unite (dims);
72         }
73     }
74   return r;
75 }
76
77 Interval
78 Axis_group_interface::cached_pure_height (Grob *me, int start, int end)
79 {
80   SCM adjacent_pure_heights = me->get_property ("adjacent-pure-heights");
81
82   if (!scm_is_pair (adjacent_pure_heights)
83       || !scm_is_vector (scm_cdr (adjacent_pure_heights)))
84     return Interval (0, 0);
85
86   return combine_pure_heights (me, scm_cdr (adjacent_pure_heights), start, end);
87 }
88
89 Interval
90 Axis_group_interface::begin_of_line_pure_height (Grob *me, int start)
91 {
92   SCM adjacent_pure_heights = me->get_property ("adjacent-pure-heights");
93
94   if (!scm_is_pair (adjacent_pure_heights)
95       || !scm_is_vector (scm_car (adjacent_pure_heights)))
96     return Interval (0, 0);
97
98   return combine_pure_heights (me, scm_car (adjacent_pure_heights), start, start+1);
99 }
100
101 Interval
102 Axis_group_interface::combine_pure_heights (Grob *me, SCM measure_extents, int start, int end)
103 {
104   Paper_score *ps = get_root_system (me)->paper_score ();
105   vector<vsize> breaks = ps->get_break_indices ();
106   vector<Grob*> cols = ps->get_columns ();
107
108   Interval ext;
109   for (vsize i = 0; i + 1 < breaks.size (); i++)
110     {
111       int r = Paper_column::get_rank (cols[breaks[i]]);
112       if (r >= end)
113         break;
114
115       if (r >= start)
116         ext.unite (ly_scm2interval (scm_c_vector_ref (measure_extents, i)));
117     }
118
119   return ext;
120 }
121
122 // adjacent-pure-heights is a pair of vectors, each of which has one element
123 // for every measure in the score. The first vector stores, for each measure,
124 // the combined height of the elements that are present only when the bar
125 // is at the beginning of a line. The second vector stores, for each measure,
126 // the combined height of the elements that are present only when the bar
127 // is not at the beginning of a line.
128
129 MAKE_SCHEME_CALLBACK (Axis_group_interface, adjacent_pure_heights, 1)
130 SCM
131 Axis_group_interface::adjacent_pure_heights (SCM smob)
132 {
133   Grob *me = unsmob_grob (smob);
134
135   Grob *common = calc_pure_elts_and_common (me);
136   extract_grob_set (me, "pure-relevant-items", items);
137   extract_grob_set (me, "pure-relevant-spanners", spanners);
138
139   Paper_score *ps = get_root_system (me)->paper_score ();
140   vector<vsize> breaks = ps->get_break_indices ();
141   vector<Grob*> cols = ps->get_columns ();
142
143   SCM begin_line_heights = scm_c_make_vector (breaks.size () - 1, SCM_EOL);
144   SCM mid_line_heights = scm_c_make_vector (breaks.size () - 1, SCM_EOL);
145
146   vsize it_index = 0;
147   for (vsize i = 0; i + 1 < breaks.size (); i++)
148     {
149       int start = Paper_column::get_rank (cols[breaks[i]]);
150       int end = Paper_column::get_rank (cols[breaks[i+1]]);
151       Interval begin_line_iv;
152       Interval mid_line_iv;
153
154       for (vsize j = it_index; j < items.size (); j++)
155         {
156           Item *it = dynamic_cast<Item*> (items[j]);
157           int rank = it->get_column ()->get_rank ();
158
159           if (rank <= end && it->pure_is_visible (start, end)
160               && !to_boolean (it->get_property ("cross-staff")))
161             {
162               Interval dims = items[j]->pure_height (common, start, end);
163               Interval &target_iv = start == it->get_column ()->get_rank () ? begin_line_iv : mid_line_iv;
164
165               if (!dims.is_empty ())
166                 target_iv.unite (dims);
167             }
168
169           if (rank < end)
170             it_index++;
171           else if (rank > end)
172             break;
173         }
174
175       for (vsize j = 0; j < spanners.size (); j++)
176         {
177           Interval_t<int> rank_span = spanners[j]->spanned_rank_interval ();
178           if (rank_span[LEFT] <= end && rank_span[RIGHT] >= start
179               && !to_boolean (spanners[j]->get_property ("cross-staff")))
180             {
181               Interval dims = spanners[j]->pure_height (common, start, end);
182
183               if (!dims.is_empty ())
184                 mid_line_iv.unite (dims);
185             }
186         }
187
188       scm_vector_set_x (begin_line_heights, scm_from_int (i), ly_interval2scm (begin_line_iv));
189       scm_vector_set_x (mid_line_heights, scm_from_int (i), ly_interval2scm (mid_line_iv));
190     }
191   return scm_cons (begin_line_heights, mid_line_heights);
192 }
193
194 Interval
195 Axis_group_interface::relative_pure_height (Grob *me, int start, int end)
196 {
197   /* It saves a _lot_ of time if we assume a VerticalAxisGroup is additive
198      (ie. height (i, k) = max (height (i, j) height (j, k)) for all i <= j <= k).
199      Unfortunately, it isn't always true, particularly if there is a
200      VerticalAlignment somewhere in the descendants.
201
202      Apart from PianoStaff, which has a fixed VerticalAlignment so it doesn't
203      count, the only VerticalAlignment comes from Score. This makes it
204      reasonably safe to assume that if our parent is a VerticalAlignment,
205      we can assume additivity and cache things nicely. */
206   Grob *p = me->get_parent (Y_AXIS);
207   if (p && Align_interface::has_interface (p))
208     return Axis_group_interface::cached_pure_height (me, start, end);
209
210   Grob *common = calc_pure_elts_and_common (me);
211   extract_grob_set (me, "pure-relevant-items", items);
212   extract_grob_set (me, "pure-relevant-spanners", spanners);
213
214   Interval r;
215
216   for (vsize i = 0; i < items.size (); i++)
217     {
218       Item *it = dynamic_cast<Item*> (items[i]);
219       int rank = it->get_column ()->get_rank ();
220
221       if (rank > end)
222         break;
223       else if (rank >= start && it->pure_is_visible (start, end)
224                && !to_boolean (it->get_property ("cross-staff")))
225         {
226           Interval dims = it->pure_height (common, start, end);
227           if (!dims.is_empty ())
228             r.unite (dims);
229         }
230     }
231
232   for (vsize i = 0; i < spanners.size (); i++)
233     {
234       Interval_t<int> rank_span = spanners[i]->spanned_rank_interval ();
235       if (rank_span[LEFT] <= end && rank_span[RIGHT] >= start
236           && !to_boolean (spanners[i]->get_property ("cross-staff")))
237         {
238           Interval dims = spanners[i]->pure_height (common, start, end);
239           if (!dims.is_empty ())
240             r.unite (dims);
241         }
242     }
243   return r;
244 }
245
246 MAKE_SCHEME_CALLBACK (Axis_group_interface, width, 1);
247 SCM
248 Axis_group_interface::width (SCM smob)
249 {
250   Grob *me = unsmob_grob (smob);
251   return generic_group_extent (me, X_AXIS);
252 }
253
254 MAKE_SCHEME_CALLBACK (Axis_group_interface, height, 1);
255 SCM
256 Axis_group_interface::height (SCM smob)
257 {
258   Grob *me = unsmob_grob (smob);
259   return generic_group_extent (me, Y_AXIS);
260 }
261
262 MAKE_SCHEME_CALLBACK (Axis_group_interface, pure_height, 3);
263 SCM
264 Axis_group_interface::pure_height (SCM smob, SCM start_scm, SCM end_scm)
265 {
266   int start = robust_scm2int (start_scm, 0);
267   int end = robust_scm2int (end_scm, INT_MAX);
268   Grob *me = unsmob_grob (smob);
269
270   /* Maybe we are in the second pass of a two-pass spacing run. In that
271      case, the Y-extent of a system is already given to us */
272   System *system = dynamic_cast<System*> (me);
273   if (system)
274     {
275       SCM line_break_details = system->column (start)->get_property ("line-break-system-details");
276       SCM system_y_extent = scm_assq (ly_symbol2scm ("system-Y-extent"), line_break_details);
277       if (scm_is_pair (system_y_extent))
278         return scm_cdr (system_y_extent);
279     }
280
281   return ly_interval2scm (pure_group_height (me, start, end));
282 }
283
284 MAKE_SCHEME_CALLBACK (Axis_group_interface, calc_skylines, 1);
285 SCM
286 Axis_group_interface::calc_skylines (SCM smob)
287 {
288   Grob *me = unsmob_grob (smob);
289   extract_grob_set (me, "elements", elts);
290   Skyline_pair skylines = skyline_spacing (me, elts);
291
292   return skylines.smobbed_copy ();
293 }
294
295 /* whereas calc_skylines calculates skylines for axis-groups with a lot of
296    visible children, combine_skylines is designed for axis-groups whose only
297    children are other axis-groups (ie. VerticalAlignment). Rather than
298    calculating all the skylines from scratch, we just merge the skylines
299    of the children.
300 */
301 MAKE_SCHEME_CALLBACK (Axis_group_interface, combine_skylines, 1);
302 SCM
303 Axis_group_interface::combine_skylines (SCM smob)
304 {
305   Grob *me = unsmob_grob (smob);
306   extract_grob_set (me, "elements", elements);
307   Grob *y_common = common_refpoint_of_array (elements, me, Y_AXIS);
308   Grob *x_common = common_refpoint_of_array (elements, me, X_AXIS);
309
310   if (y_common != me)
311     programming_error ("combining skylines that don't belong to me");
312
313   Skyline_pair ret;
314   for (vsize i = 0; i < elements.size (); i++)
315     {
316       SCM skyline_scm = elements[i]->get_property ("vertical-skylines");
317       if (Skyline_pair::unsmob (skyline_scm))
318         {
319           Real offset = elements[i]->relative_coordinate (y_common, Y_AXIS);
320           Skyline_pair other = *Skyline_pair::unsmob (skyline_scm);
321           other.raise (offset);
322           other.shift (elements[i]->relative_coordinate (x_common, X_AXIS));
323           ret.merge (other);
324         }
325     }
326   return ret.smobbed_copy ();
327 }
328   
329 SCM
330 Axis_group_interface::generic_group_extent (Grob *me, Axis a)
331 {
332   /* trigger the callback to do skyline-spacing on the children */
333   if (a == Y_AXIS)
334     (void) me->get_property ("vertical-skylines");
335
336   extract_grob_set (me, "elements", elts);
337   Grob *common = common_refpoint_of_array (elts, me, a);
338
339   Real my_coord = me->relative_coordinate (common, a);
340   Interval r (relative_group_extent (elts, common, a));
341
342   return ly_interval2scm (r - my_coord);
343 }
344
345 /* This is like generic_group_extent, but it only counts the grobs that
346    are children of some other axis-group. This is uncached; if it becomes
347    commonly used, it may be necessary to cache it somehow. */
348 Interval
349 Axis_group_interface::staff_extent (Grob *me, Grob *refp, Axis ext_a, Grob *staff, Axis parent_a)
350 {
351   extract_grob_set (me, "elements", elts);
352   vector<Grob*> new_elts;
353
354   for (vsize i = 0; i < elts.size (); i++)
355     if (elts[i]->common_refpoint (staff, parent_a) == staff)
356       new_elts.push_back (elts[i]);
357
358   return relative_group_extent (new_elts, refp, ext_a);
359 }
360
361
362 Grob *
363 Axis_group_interface::calc_pure_elts_and_common (Grob *me)
364 {
365   if (Grob *c = unsmob_grob (me->get_object ("pure-Y-common")))
366     return c;
367   
368   extract_grob_set (me, "elements", elts);
369
370   vector<Grob*> relevant_items;
371   vector<Grob*> relevant_spanners;
372   SCM pure_relevant_p = ly_lily_module_constant ("pure-relevant?");
373
374   for (vsize i = 0; i < elts.size (); i++)
375     {
376       if (to_boolean (scm_apply_1 (pure_relevant_p, elts[i]->self_scm (), SCM_EOL)))
377         {
378           if (dynamic_cast<Item*> (elts[i]))
379             relevant_items.push_back (elts[i]);
380           else if (dynamic_cast<Spanner*> (elts[i]))
381             relevant_spanners.push_back (elts[i]);
382         }
383             
384
385       Item *it = dynamic_cast<Item*> (elts[i]);
386       Direction d = LEFT;
387       if (it)
388         do
389           {
390             Item *piece = it->find_prebroken_piece (d);
391             if (piece && to_boolean (scm_apply_1 (pure_relevant_p, piece->self_scm (), SCM_EOL)))
392               relevant_items.push_back (piece);
393           }
394         while (flip (&d) != LEFT);
395     }
396   vector_sort (relevant_items, Item::less);
397
398   Grob *common = common_refpoint_of_array (relevant_items, me, Y_AXIS);
399   common = common_refpoint_of_array (relevant_spanners, common, Y_AXIS);
400
401   me->set_object ("pure-Y-common", common->self_scm ());
402   
403   SCM items_scm = Grob_array::make_array ();
404   SCM spanners_scm = Grob_array::make_array ();
405
406   unsmob_grob_array (items_scm)->set_array (relevant_items);
407   unsmob_grob_array (spanners_scm)->set_array (relevant_spanners);
408   me->set_object ("pure-relevant-items", items_scm);
409   me->set_object ("pure-relevant-spanners", spanners_scm);
410
411   return common;
412 }
413
414 SCM
415 Axis_group_interface::calc_common (Grob *me, Axis axis)
416 {
417   extract_grob_set (me, "elements", elts);
418   Grob *common = common_refpoint_of_array (elts, me, axis);
419   if (!common)
420     {
421       me->programming_error ("No common parent found in calc_common axis.");
422       return SCM_EOL;
423     }
424   
425   return common->self_scm ();
426 }
427
428
429 MAKE_SCHEME_CALLBACK (Axis_group_interface, calc_x_common, 1);
430 SCM
431 Axis_group_interface::calc_x_common (SCM grob)
432 {
433   return calc_common (unsmob_grob (grob), X_AXIS);
434 }
435
436 MAKE_SCHEME_CALLBACK (Axis_group_interface, calc_y_common, 1);
437 SCM
438 Axis_group_interface::calc_y_common (SCM grob)
439 {
440   return calc_common (unsmob_grob (grob), Y_AXIS);
441 }
442
443 Interval
444 Axis_group_interface::pure_group_height (Grob *me, int start, int end)
445 {
446   Grob *common = calc_pure_elts_and_common (me);
447         
448   Real my_coord = me->relative_coordinate (common, Y_AXIS);
449   Interval r (relative_pure_height (me, start, end));
450
451   return r - my_coord;
452 }
453
454 void
455 Axis_group_interface::get_children (Grob *me, vector<Grob*> *found)
456 {
457   found->push_back (me);
458
459   if (!has_interface (me))
460     return;
461
462   extract_grob_set (me, "elements", elements);
463   for (vsize i = 0; i < elements.size (); i++)
464     {
465       Grob *e = elements[i];
466       Axis_group_interface::get_children (e, found);
467     }
468 }
469
470 bool
471 staff_priority_less (Grob * const &g1, Grob * const &g2)
472 {
473   Real priority_1 = robust_scm2double (g1->get_property ("outside-staff-priority"), -infinity_f);
474   Real priority_2 = robust_scm2double (g2->get_property ("outside-staff-priority"), -infinity_f);
475
476   if (priority_1 < priority_2)
477     return true;
478   else if (priority_1 > priority_2)
479     return false;
480
481   /* if neither grob has an outside-staff priority, the ordering will have no
482      effect -- we just need to choose a consistent ordering. We do this to
483      avoid the side-effect of calculating extents. */
484   if (isinf (priority_1))
485     return g1 < g2;
486
487   /* if there is no preference in staff priority, choose the left-most one */
488   Grob *common = g1->common_refpoint (g2, X_AXIS);
489   Real start_1 = g1->extent (common, X_AXIS)[LEFT];
490   Real start_2 = g2->extent (common, X_AXIS)[LEFT];
491   return start_1 < start_2;
492 }
493
494 static void
495 add_boxes (Grob *me, Grob *x_common, Grob *y_common, vector<Box> *const boxes, Skyline_pair *skylines)
496 {
497   /* if a child has skylines, use them instead of the extent box */
498   if (Skyline_pair *pair = Skyline_pair::unsmob (me->get_property ("vertical-skylines")))
499     {
500       Skyline_pair s = *pair;
501       s.shift (me->relative_coordinate (x_common, X_AXIS));
502       s.raise (me->relative_coordinate (y_common, Y_AXIS));
503       skylines->merge (s);
504     }
505   else if (Grob_array *elements = unsmob_grob_array (me->get_object ("elements")))
506     {
507       for (vsize i = 0; i < elements->size (); i++)
508         add_boxes (elements->grob (i), x_common, y_common, boxes, skylines);
509     }
510   else if (!scm_is_number (me->get_property ("outside-staff-priority"))
511            && !to_boolean (me->get_property ("cross-staff")))
512     {
513       boxes->push_back (Box (me->extent (x_common, X_AXIS),
514                              me->extent (y_common, Y_AXIS)));
515     }
516 }
517
518 /* We want to avoid situations like this:
519            still more text
520       more text
521    text
522    -------------------
523    staff
524    -------------------
525
526    The point is that "still more text" should be positioned under
527    "more text".  In order to achieve this, we place the grobs in several
528    passes.  We keep track of the right-most horizontal position that has been
529    affected by the current pass so far (actually we keep track of 2
530    positions, one for above the staff, one for below).
531
532    In each pass, we loop through the unplaced grobs from left to right.
533    If the grob doesn't overlap the right-most affected position, we place it
534    (and then update the right-most affected position to point to the right
535    edge of the just-placed grob).  Otherwise, we skip it until the next pass.
536 */
537 static void
538 add_grobs_of_one_priority (Skyline_pair *const skylines,
539                            vector<Grob*> elements,
540                            Grob *x_common,
541                            Grob *y_common)
542 {
543   vector<Box> boxes;
544   Drul_array<Real> last_affected_position;
545
546   reverse (elements);
547   while (!elements.empty ())
548     {
549       last_affected_position[UP] = -infinity_f;
550       last_affected_position[DOWN] = -infinity_f;
551       /* do one pass */
552       for (vsize i = elements.size (); i--;)
553         {
554           Direction dir = get_grob_direction (elements[i]);
555           if (dir == CENTER)
556             {
557               warning (_ ("an outside-staff object should have a direction, defaulting to up"));
558               dir = UP;
559             }
560
561           Box b (elements[i]->extent (x_common, X_AXIS),
562                  elements[i]->extent (y_common, Y_AXIS));
563           SCM horizon_padding_scm = elements[i]->get_property ("outside-staff-horizontal-padding");
564           Real horizon_padding = robust_scm2double (horizon_padding_scm, 0.0);
565
566           if (b[X_AXIS][LEFT] - 2*horizon_padding < last_affected_position[dir])
567             continue;
568
569           if (!b[X_AXIS].is_empty () && !b[Y_AXIS].is_empty ())
570             {
571               boxes.clear ();
572               boxes.push_back (b);
573               Skyline other = Skyline (boxes, horizon_padding, X_AXIS, -dir);
574               Real padding = robust_scm2double (elements[i]->get_property ("outside-staff-padding"), 0.5);
575               Real dist = (*skylines)[dir].distance (other) + padding;
576
577               if (dist > 0)
578                 {
579                   b.translate (Offset (0, dir*dist));
580                   elements[i]->translate_axis (dir*dist, Y_AXIS);
581                 }
582               (*skylines)[dir].insert (b, 0, X_AXIS);
583               elements[i]->set_property ("outside-staff-priority", SCM_BOOL_F);
584               last_affected_position[dir] = b[X_AXIS][RIGHT];
585             }
586
587           /*
588             Ugh: quadratic. --hwn
589            */
590           elements.erase (elements.begin () + i);
591         }
592     }
593 }
594
595 // TODO: it is tricky to correctly handle skyline placement of cross-staff grobs.
596 // For example, cross-staff beams cannot be formatted until the distance between
597 // staves is known and therefore any grobs that depend on the beam cannot be placed
598 // until the skylines are known. On the other hand, the distance between staves should
599 // really depend on position of the cross-staff grobs that lie between them.
600 // Currently, we just leave cross-staff grobs out of the
601 // skyline altogether, but this could mean that staves are placed so close together
602 // that there is no room for the cross-staff grob. It also means, of course, that
603 // we don't get the benefits of skyline placement for cross-staff grobs.
604 Skyline_pair
605 Axis_group_interface::skyline_spacing (Grob *me, vector<Grob*> elements)
606 {
607   /* For grobs with an outside-staff-priority, the sorting function might
608      call extent and cause suicide. This breaks the contract that is required
609      for the STL sort function. To avoid this, we make sure that any suicides
610      are triggered beforehand.
611   */
612   for (vsize i = 0; i < elements.size (); i++)
613     if (scm_is_number (elements[i]->get_property ("outside-staff-priority")))
614       elements[i]->extent (elements[i], X_AXIS);
615
616   vector_sort (elements, staff_priority_less);
617   Grob *x_common = common_refpoint_of_array (elements, me, X_AXIS);
618   Grob *y_common = common_refpoint_of_array (elements, me, Y_AXIS);
619
620   assert (y_common == me);
621
622   vsize i = 0;
623   vector<Box> boxes;
624
625   Skyline_pair skylines;
626   for (i = 0; i < elements.size ()
627          && !scm_is_number (elements[i]->get_property ("outside-staff-priority")); i++)
628     if (!to_boolean (elements[i]->get_property ("cross-staff")))
629       add_boxes (elements[i], x_common, y_common, &boxes, &skylines);
630
631   SCM padding_scm = me->get_property ("skyline-horizontal-padding");
632   Real padding = robust_scm2double (padding_scm, 0.1);
633   skylines.merge (Skyline_pair (boxes, padding, X_AXIS));
634   for (; i < elements.size (); i++)
635     {
636       if (to_boolean (elements[i]->get_property ("cross-staff")))
637         continue;
638
639       SCM priority = elements[i]->get_property ("outside-staff-priority");
640       vector<Grob*> current_elts;
641       current_elts.push_back (elements[i]);
642       while (i + 1 < elements.size () 
643              && scm_eq_p (elements[i+1]->get_property ("outside-staff-priority"), priority))
644         current_elts.push_back (elements[++i]);
645
646       add_grobs_of_one_priority (&skylines, current_elts, x_common, y_common);
647     }
648   skylines.shift (-me->relative_coordinate (x_common, X_AXIS));
649   return skylines;
650 }
651
652 MAKE_SCHEME_CALLBACK (Axis_group_interface, calc_max_stretch, 1)
653 SCM
654 Axis_group_interface::calc_max_stretch (SCM smob)
655 {
656   Grob *me = unsmob_grob (smob);
657   Real ret = 0;
658   extract_grob_set (me, "elements", elts);
659
660   for (vsize i = 0; i < elts.size (); i++)
661     if (Axis_group_interface::has_interface (elts[i]))
662       ret += robust_scm2double (elts[i]->get_property ("max-stretch"), 0.0);
663
664   return scm_from_double (ret);
665 }
666
667 MAKE_SCHEME_CALLBACK (Axis_group_interface, print, 1)
668 SCM
669 Axis_group_interface::print (SCM smob)
670 {
671   if (!debug_skylines)
672     return SCM_BOOL_F;
673
674   Grob *me = unsmob_grob (smob);
675   Stencil ret;
676   if (Skyline_pair *s = Skyline_pair::unsmob (me->get_property ("vertical-skylines")))
677     {
678       ret.add_stencil (Lookup::points_to_line_stencil (0.1, (*s)[UP].to_points (X_AXIS))
679                        .in_color (255, 0, 255));
680       ret.add_stencil (Lookup::points_to_line_stencil (0.1, (*s)[DOWN].to_points (X_AXIS))
681                        .in_color (0, 255, 255));
682     }
683   return ret.smobbed_copy ();
684 }
685
686 ADD_INTERFACE (Axis_group_interface,
687                "An object that groups other layout objects.",
688
689                /* properties */
690                "X-common "
691                "Y-common "
692                "adjacent-pure-heights "
693                "axes "
694                "elements "
695                "keep-fixed-while-stretching "
696                "max-stretch "
697                "no-alignment "
698                "pure-Y-common "
699                "pure-relevant-items "
700                "pure-relevant-spanners "
701                "vertical-skylines "
702                );