]> git.donarmstrong.com Git - lilypond.git/blob - lily/tuplet-bracket.cc
Merge branch 'master' of ssh+git://hanwen@git.sv.gnu.org/srv/git/lilypond
[lilypond.git] / lily / tuplet-bracket.cc
1 /*
2   tuplet-bracket.cc -- implement Tuplet_bracket
3
4   source file of the GNU LilyPond music typesetter
5
6   (c) 1997--2007 Jan Nieuwenhuizen <janneke@gnu.org>
7   Han-Wen Nienhuys <hanwen@xs4all.nl>
8 */
9
10 /*
11   TODO:
12
13   - tuplet bracket should probably be subject to the same rules as
14   beam sloping/quanting.
15
16   - There is no support for kneed brackets, or nested brackets.
17
18   - number placement for parallel beams should be much more advanced:
19   for sloped beams some extra horizontal offset must be introduced.
20
21   - number placement is usually done over the center note, not the
22   graphical center.
23 */
24
25 /*
26   TODO: quantise, we don't want to collide with staff lines.
27   (or should we be above staff?)
28
29   todo: handle breaking elegantly.
30 */
31
32
33 #include "tuplet-bracket.hh"
34 #include "line-interface.hh"
35 #include "beam.hh"
36 #include "warn.hh"
37 #include "output-def.hh"
38 #include "font-interface.hh"
39 #include "text-interface.hh"
40 #include "stem.hh"
41 #include "note-column.hh"
42 #include "pointer-group-interface.hh"
43 #include "directional-element-interface.hh"
44 #include "spanner.hh"
45 #include "staff-symbol-referencer.hh"
46 #include "lookup.hh"
47 #include "paper-column.hh"
48 #include "moment.hh"
49
50 static Item *
51 get_x_bound_item (Grob *me_grob, Direction hdir, Direction my_dir)
52 {
53   Spanner *me = dynamic_cast<Spanner *> (me_grob);
54   Item *g = me->get_bound (hdir);
55   if (Note_column::has_interface (g)
56       && Note_column::get_stem (g)
57       && Note_column::dir (g) == my_dir)
58     g = Note_column::get_stem (g);
59
60   return g;
61 }
62
63
64 void
65 flatten_number_pair_property (Grob *me,
66                               Direction xdir,  SCM sym)
67 {
68   Drul_array<Real> zero (0, 0);
69   Drul_array<Real> pair
70     = robust_scm2drul (me->internal_get_property (sym), zero);
71   pair[xdir] = 0.0;
72   
73   me->set_property (sym, ly_interval2scm (pair));
74 }
75
76
77 /*
78   Return beam that encompasses the span of the tuplet bracket.
79 */
80 Grob *
81 Tuplet_bracket::parallel_beam (Grob *me_grob, vector<Grob*> const &cols,
82                                bool *equally_long)
83 {
84   Spanner *me = dynamic_cast<Spanner *> (me_grob);
85
86   if (me->get_bound (LEFT)->break_status_dir ()
87       || me->get_bound (RIGHT)->break_status_dir ())
88     return 0;
89
90   Drul_array<Grob*> stems (Note_column::get_stem (cols[0]),
91                            Note_column::get_stem (cols.back ()));
92
93   if (!stems[RIGHT]
94       || !stems[LEFT]
95       || (dynamic_cast<Item*> (stems[RIGHT])->get_column ()
96           != me->get_bound (RIGHT)->get_column ()))
97     return 0;
98
99   Drul_array<Grob*> beams;
100   Direction d = LEFT;
101   do {
102     beams[d] = stems[d] ? Stem::get_beam (stems[d]) : 0;
103   } while (flip (&d) != LEFT);
104   
105   *equally_long = false;
106   if (! (beams[LEFT] && (beams[LEFT] == beams[RIGHT]) && !me->is_broken ()))
107     return 0;
108
109   extract_grob_set (beams[LEFT], "stems", beam_stems);
110   if (beam_stems.size () == 0)
111     {
112       programming_error ("beam under tuplet bracket has no stems");
113       *equally_long = 0;
114       return 0;
115     }
116
117   *equally_long =
118     (beam_stems[0] == stems[LEFT]
119      && beam_stems.back () == stems[RIGHT]);
120   return beams[LEFT];
121 }
122
123
124 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_connect_to_neighbors,1);
125 SCM
126 Tuplet_bracket::calc_connect_to_neighbors (SCM smob)
127 {
128   Spanner *me = unsmob_spanner (smob);
129
130   Direction dir = get_grob_direction (me); 
131   Drul_array<Item *> bounds (get_x_bound_item (me, LEFT, dir),
132                              get_x_bound_item (me, RIGHT, dir));
133   
134   Drul_array<bool> connect_to_other (false, false);
135   Direction d = LEFT;
136   do
137     {
138       Direction break_dir = bounds[d]->break_status_dir ();
139       Spanner *orig_spanner = dynamic_cast<Spanner*> (me->original ());
140       vsize neighbor_idx = me->get_break_index () - break_dir;
141       if (break_dir
142           && d == RIGHT
143           && neighbor_idx < orig_spanner->broken_intos_.size ())
144         {
145           Grob *neighbor = orig_spanner->broken_intos_[neighbor_idx];
146
147           /* trigger possible suicide*/
148           (void) neighbor->get_property ("positions");
149         }
150
151       connect_to_other[d]
152         = (break_dir
153            && neighbor_idx < orig_spanner->broken_intos_.size ()
154            && orig_spanner->broken_intos_[neighbor_idx]->is_live ());
155     }
156   while (flip (&d) != LEFT);
157
158
159   if (connect_to_other[LEFT] || connect_to_other[RIGHT])
160     return scm_cons (scm_from_bool (connect_to_other[LEFT]),
161                      scm_from_bool (connect_to_other[RIGHT]));
162                      
163   return SCM_EOL;
164 }
165
166 Grob* 
167 Tuplet_bracket::get_common_x (Spanner *me)
168 {
169   extract_grob_set (me, "note-columns", columns);
170
171   Grob * commonx = common_refpoint_of_array (columns, me, X_AXIS);
172   commonx = commonx->common_refpoint (me->get_bound (LEFT), X_AXIS);
173   commonx = commonx->common_refpoint (me->get_bound (RIGHT), X_AXIS);
174
175   return commonx;
176 }
177   
178 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_control_points,1)
179 SCM
180 Tuplet_bracket::calc_control_points (SCM smob)
181 {
182   Spanner *me = unsmob_spanner (smob);
183
184   extract_grob_set (me, "note-columns", columns);
185
186   SCM scm_positions = me->get_property ("positions");
187   if (!me->is_live ())
188     return SCM_EOL;
189   
190   if (!scm_is_pair (scm_positions))
191     programming_error ("Positions should be number pair");
192     
193   Drul_array<Real> positions
194     = robust_scm2drul (scm_positions, Drul_array<Real> (0,0));
195
196   Grob *commonx = get_common_x (me);
197   Direction dir = get_grob_direction (me);
198
199   Drul_array<Item *> bounds;
200   bounds[LEFT] = get_x_bound_item (me, LEFT, dir);
201   bounds[RIGHT] = get_x_bound_item (me, RIGHT, dir);
202
203   Drul_array<bool> connect_to_other =
204     robust_scm2booldrul (me->get_property ("connect-to-neighbor"),
205                          Drul_array<bool> (false, false));
206   
207     
208   Interval x_span;
209   Direction d = LEFT;
210   do
211     {
212       x_span[d] = robust_relative_extent (bounds[d], commonx, X_AXIS)[d];
213
214       if (connect_to_other[d])
215         {
216           Interval overshoot (robust_scm2drul (me->get_property ("break-overshoot"),
217                                                Interval (-0.5, 0.0)));
218
219           if (d == RIGHT)
220             x_span[d] += d * overshoot[d];
221           else
222             x_span[d] = robust_relative_extent (bounds[d], commonx, X_AXIS)[RIGHT]
223               - overshoot[LEFT];
224         }
225       
226       else if (d == RIGHT
227                && (columns.empty ()
228                    || (bounds[d]->get_column ()
229                        != dynamic_cast<Item *> (columns.back ())->get_column ())))
230         {
231           /*
232             We're connecting to a column, for the last bit of a broken
233             fullLength bracket.
234             
235             TODO: make padding tunable?
236           */
237           Real padding = 1.0;
238
239           if (bounds[d]->break_status_dir ())
240             padding = 0.0;
241           
242           x_span[d]
243             = robust_relative_extent (bounds[d], commonx, X_AXIS) [LEFT]
244             - padding;
245         }
246     }
247   while (flip (&d) != LEFT);
248
249   
250   
251   x_span -= me->get_bound (LEFT)->relative_coordinate (commonx, X_AXIS);
252   return scm_list_2 (ly_offset2scm (Offset (x_span[LEFT], positions[LEFT])),
253                      ly_offset2scm (Offset (x_span[RIGHT], positions[RIGHT])));
254 }
255
256 /*
257   TODO:
258
259   in the case that there is no bracket, but there is a (single) beam,
260   follow beam precisely for determining tuplet number location.
261 */
262 MAKE_SCHEME_CALLBACK (Tuplet_bracket, print, 1);
263 SCM
264 Tuplet_bracket::print (SCM smob)
265 {
266   Spanner *me = unsmob_spanner (smob);
267   Stencil mol;
268
269   extract_grob_set (me, "note-columns", columns);
270   bool equally_long = false;
271   Grob *par_beam = parallel_beam (me, columns, &equally_long);
272   
273   bool bracket_visibility = !(par_beam && equally_long);
274   /*
275     Fixme: the type of this prop is sucky.
276   */
277   SCM bracket = me->get_property ("bracket-visibility");
278   if (scm_is_bool (bracket))
279     bracket_visibility = ly_scm2bool (bracket);
280   else if (bracket == ly_symbol2scm ("if-no-beam"))
281     bracket_visibility = !par_beam;
282   
283   SCM cpoints =  me->get_property ("control-points");
284   if (scm_ilength (cpoints) < 2)
285     {
286       me->suicide ();
287       return SCM_EOL;
288     }
289   
290   Drul_array<Offset> points;
291   points[LEFT] = ly_scm2offset (scm_car (cpoints));
292   points[RIGHT] = ly_scm2offset (scm_cadr (cpoints));
293   
294   Interval x_span (points[LEFT][X_AXIS], points[RIGHT][X_AXIS]);
295   Drul_array<Real> positions (points[LEFT][Y_AXIS], points[RIGHT][Y_AXIS]);
296
297   Output_def *pap = me->layout ();
298
299   Grob *number_grob = unsmob_grob (me->get_object ("tuplet-number"));
300   
301   /*
302     No bracket when it would be smaller than the number.
303   */
304   Real gap = 0.;
305   if (bracket_visibility && number_grob)
306     {
307       Interval ext = number_grob->extent (number_grob, X_AXIS);
308       if (!ext.is_empty ())
309         {
310           gap = ext.length () + 1.0;
311       
312           if (0.75 * x_span.length () < gap)
313             bracket_visibility = false;
314         }
315     }
316
317   if (bracket_visibility)
318     {
319       Drul_array<Real> zero (0, 0);
320       Real ss = Staff_symbol_referencer::staff_space (me);
321       Drul_array<Real> height
322         = robust_scm2drul (me->get_property ("edge-height"), zero);
323       Drul_array<Real> flare
324         = robust_scm2drul (me->get_property ("bracket-flare"), zero);
325       Drul_array<Real> shorten
326         = robust_scm2drul (me->get_property ("shorten-pair"), zero);
327       Drul_array<Stencil> edge_stencils;
328
329       Direction dir = get_grob_direction (me);
330       
331       scale_drul (&height, -ss * dir);
332       scale_drul (&flare, ss);
333       scale_drul (&shorten, ss);
334
335       Drul_array<bool> connect_to_other =
336         robust_scm2booldrul (me->get_property ("connect-to-neighbor"),
337                              Drul_array<bool> (false, false));
338
339       Direction d = LEFT;
340       do
341         {
342           if (connect_to_other[d])
343             {
344               height[d] = 0.0;
345               flare[d] = 0.0;
346               shorten[d] = 0.0;
347
348               SCM edge_text = me->get_property ("edge-text");
349
350               if (scm_is_pair (edge_text))
351                 {
352                   SCM properties = Font_interface::text_font_alist_chain (me);
353                   SCM text = index_get_cell (edge_text, d);
354                   if (Text_interface::is_markup (text))
355                     {
356                       SCM t = Text_interface::interpret_markup (pap->self_scm (),
357                                                                 properties, text);
358
359                       Stencil *edge_text = unsmob_stencil (t);
360                       edge_text->translate_axis (x_span[d] - x_span[LEFT], X_AXIS);
361                       edge_stencils[d] = *edge_text;
362                     }
363                 }
364             }
365         }
366       while (flip (&d) != LEFT);
367
368       Stencil brack = make_bracket (me, Y_AXIS,
369                                     points[RIGHT] - points[LEFT],
370                                     height,
371                                     /*
372                                       0.1 = more space at right due to italics
373                                       TODO: use italic correction of font.
374                                     */
375                                     Interval (-0.5, 0.5) * gap + 0.1,
376                                     flare, shorten);
377
378       do
379         {
380           if (!edge_stencils[d].is_empty ())
381             brack.add_stencil (edge_stencils[d]);
382         }
383       while (flip (&d) != LEFT);
384
385       mol.add_stencil (brack);
386     }
387
388   mol.translate (points[LEFT]);
389   return mol.smobbed_copy ();
390 }
391
392 /*
393   should move to lookup?
394
395   TODO: this will fail for very short (shorter than the flare)
396   brackets.
397 */
398 Stencil
399 Tuplet_bracket::make_bracket (Grob *me, // for line properties.
400                               Axis protusion_axis,
401                               Offset dz,
402                               Drul_array<Real> height,
403                               Interval gap,
404                               Drul_array<Real> flare,
405                               Drul_array<Real> shorten)
406 {
407   Drul_array<Offset> corners (Offset (0, 0), dz);
408
409   Real length = dz.length ();
410   Drul_array<Offset> gap_corners;
411
412   Axis bracket_axis = other_axis (protusion_axis);
413
414   Drul_array<Offset> straight_corners = corners;
415
416   Direction d = LEFT;
417   do
418     straight_corners[d] += -d * shorten[d] / length * dz;
419   while (flip (&d) != LEFT);
420
421   if (!gap.is_empty ())
422     {
423       do
424         gap_corners[d] = (dz * 0.5) + gap[d] / length * dz;
425       while (flip (&d) != LEFT);
426     }
427
428   Drul_array<Offset> flare_corners = straight_corners;
429   do
430     {
431       flare_corners[d][bracket_axis] = straight_corners[d][bracket_axis];
432       flare_corners[d][protusion_axis] += height[d];
433       straight_corners[d][bracket_axis] += -d * flare[d];
434     }
435   while (flip (&d) != LEFT);
436
437   Stencil m;
438   do
439     {
440       if (!gap.is_empty ())
441         m.add_stencil (Line_interface::line (me, straight_corners[d],
442                                              gap_corners[d]));
443
444       m.add_stencil (Line_interface::line (me, straight_corners[d],
445                                            flare_corners[d]));
446     }
447
448   while (flip (&d) != LEFT);
449
450   if (gap.is_empty ())
451     m.add_stencil (Line_interface::line (me, straight_corners[LEFT],
452                                          straight_corners[RIGHT]));
453   
454   return m;
455 }
456
457 void
458 Tuplet_bracket::get_bounds (Grob *me, Grob **left, Grob **right)
459 {
460   extract_grob_set (me, "note-columns", columns);
461   vsize l = 0;
462   while (l < columns.size () && Note_column::has_rests (columns[l]))
463     l++;
464
465   vsize r = columns.size ();
466   while (r > l && Note_column::has_rests (columns[r-1]))
467     r--;
468
469   *left = *right = 0;
470
471   if (l < r)
472     {
473       *left = columns[l];
474       *right = columns[r-1];
475     }
476 }
477
478 /*
479   use first -> last note for slope, and then correct for disturbing
480   notes in between.  */
481 void
482 Tuplet_bracket::calc_position_and_height (Grob *me_grob, Real *offset, Real *dy)
483 {
484   Spanner *me = dynamic_cast<Spanner *> (me_grob);
485
486   extract_grob_set (me, "note-columns", columns);
487   extract_grob_set (me, "tuplets", tuplets);
488
489   Grob *commony = common_refpoint_of_array (columns, me, Y_AXIS);
490   commony = common_refpoint_of_array (tuplets, commony, Y_AXIS);
491   if (Grob *st = Staff_symbol_referencer::get_staff_symbol (me))
492     commony = st->common_refpoint (commony, Y_AXIS);
493   Real my_offset = me->relative_coordinate (commony, Y_AXIS);
494
495   Grob *commonx = get_common_x (me);
496   commonx = common_refpoint_of_array (tuplets, commonx, Y_AXIS);
497
498   Interval staff;
499   Grob *st = Staff_symbol_referencer::get_staff_symbol (me);
500
501   /* staff-padding doesn't work correctly on cross-staff tuplets
502      because it only considers one staff symbol. Until this works,
503      disable it. */
504   if (st && !to_boolean (me->get_property ("cross-staff")))
505     {
506       Real pad = robust_scm2double (me->get_property ("staff-padding"), -1.0);
507       if  (pad >= 0.0)
508         {
509           staff = st->extent (commony, Y_AXIS) - my_offset;
510           staff.widen (pad);
511         }
512     }
513   
514   Direction dir = get_grob_direction (me);
515
516   bool equally_long = false;
517   Grob *par_beam = parallel_beam (me, columns, &equally_long);
518   
519   Item *lgr = get_x_bound_item (me, LEFT, dir);
520   Item *rgr = get_x_bound_item (me, RIGHT, dir);
521   Real x0 = robust_relative_extent (lgr, commonx, X_AXIS)[LEFT];
522   Real x1 = robust_relative_extent (rgr, commonx, X_AXIS)[RIGHT];
523   bool follow_beam = par_beam
524     && get_grob_direction (par_beam) == dir
525     && ! to_boolean (par_beam->get_property ("knee"));
526
527   vector<Offset> points;
528   if (columns.size ()
529       && follow_beam
530       && Note_column::get_stem (columns[0])
531       && Note_column::get_stem (columns.back ()))
532     {
533       /*
534         trigger set_stem_ends
535        */
536       (void) par_beam->get_property ("quantized-positions");
537
538       Drul_array<Grob *> stems (Note_column::get_stem (columns[0]),
539                                 Note_column::get_stem (columns.back ()));
540
541       Real ss = 0.5 * Staff_symbol_referencer::staff_space (me);
542       Real lp = ss * robust_scm2double (stems[LEFT]->get_property ("stem-end-position"), 0.0)
543         + stems[LEFT]->get_parent (Y_AXIS)->relative_coordinate (commony, Y_AXIS);
544       Real rp = ss * robust_scm2double (stems[RIGHT]->get_property ("stem-end-position"), 0.0)
545         + stems[RIGHT]->get_parent (Y_AXIS)->relative_coordinate (commony, Y_AXIS);
546
547       *dy = rp - lp;
548       points.push_back (Offset (stems[LEFT]->relative_coordinate (commonx, X_AXIS) - x0, lp));
549       points.push_back (Offset (stems[RIGHT]->relative_coordinate (commonx, X_AXIS) - x0, rp));
550     }
551   else
552     {
553       /*
554         Use outer non-rest columns to determine slope
555       */
556       Grob *left_col = 0;
557       Grob *right_col = 0;
558       get_bounds (me, &left_col, &right_col);
559       if (left_col && right_col)
560         {
561           Interval rv = Note_column::cross_staff_extent (right_col, commony);
562           Interval lv = Note_column::cross_staff_extent (left_col, commony);
563           rv.unite (staff);
564           lv.unite (staff);
565
566           Real graphical_dy = rv[dir] - lv[dir];
567
568           Slice ls = Note_column::head_positions_interval (left_col);
569           Slice rs = Note_column::head_positions_interval (right_col);
570
571           Interval musical_dy;
572           musical_dy[UP] = rs[UP] - ls[UP];
573           musical_dy[DOWN] = rs[DOWN] - ls[DOWN];
574           if (sign (musical_dy[UP]) != sign (musical_dy[DOWN]))
575             *dy = 0.0;
576           else if (sign (graphical_dy) != sign (musical_dy[DOWN]))
577             *dy = 0.0;
578           else
579             *dy = graphical_dy;
580         }
581       else
582         *dy = 0;
583
584       for (vsize i = 0; i < columns.size (); i++)
585         {
586           Interval note_ext = Note_column::cross_staff_extent (columns[i], commony);
587           Real x = columns[i]->relative_coordinate (commonx, X_AXIS) - x0;
588
589           points.push_back (Offset (x, note_ext[dir]));
590         }
591     }
592
593   if (!follow_beam)
594     {
595       points.push_back (Offset (x0 - x0, staff[dir]));
596       points.push_back (Offset (x1 - x0, staff[dir]));
597     }
598   
599   /*
600     This is a slight hack. We compute two encompass points from the
601     bbox of the smaller tuplets.
602
603     We assume that the smaller bracket is 1.0 space high.
604   */
605   Real ss = Staff_symbol_referencer::staff_space (me);
606   for (vsize i = 0; i < tuplets.size (); i++)
607     {
608       Interval tuplet_x (tuplets[i]->extent (commonx, X_AXIS));
609       Interval tuplet_y (tuplets[i]->extent (commony, Y_AXIS));
610
611       if (!tuplets[i]->is_live ())
612         continue;
613       
614       Direction d = LEFT;
615       Drul_array<Real> positions = robust_scm2interval (tuplets[i]->get_property ("positions"),
616                                                         Interval (0,0));
617
618       
619       Real other_dy = positions[RIGHT] - positions[LEFT];
620
621       do
622         {
623           Real y
624             = tuplet_y.linear_combination (d * sign (other_dy));
625
626           /*
627             We don't take padding into account for nested tuplets.
628             the edges can come very close to the stems, likewise for
629             nested tuplets?
630           */
631
632           points.push_back (Offset (tuplet_x[d] - x0, y));
633         }
634       while (flip (&d) != LEFT);
635     }
636
637   *offset = -dir * infinity_f;
638   Real factor = (columns.size () > 1) ? 1 / (x1 - x0) : 1.0;
639   for (vsize i = 0; i < points.size (); i++)
640     {
641       Real x = points[i][X_AXIS];
642       Real tuplety = (*dy) * x * factor + my_offset;
643
644       if (points[i][Y_AXIS] * dir > (*offset + tuplety) * dir)
645         *offset = points[i][Y_AXIS] - tuplety;
646     }
647
648   *offset += scm_to_double (me->get_property ("padding")) * dir;
649
650   /*
651     horizontal brackets should not collide with staff lines.
652
653     Kind of pointless since we put them outside the staff anyway, but
654     let's leave code for the future when possibly allow them to move
655     into the staff once again.
656   
657     This doesn't seem to support cross-staff tuplets atm.
658   */
659   if (*dy == 0
660       && fabs (*offset) < ss * Staff_symbol_referencer::staff_radius (me))
661     {
662       // quantize, then do collision check.
663       *offset *= 2 / ss;
664
665       *offset = rint (*offset);
666       if (Staff_symbol_referencer::on_line (me, (int) rint (*offset)))
667         *offset += dir;
668
669       *offset *= 0.5 * ss;
670     }
671 }
672
673
674 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_direction, 1);
675 SCM
676 Tuplet_bracket::calc_direction (SCM smob)
677 {
678   Grob *me = unsmob_grob (smob);
679   Direction dir = Tuplet_bracket::get_default_dir (me);
680   return scm_from_int (dir);
681 }
682
683 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_positions, 1);
684 SCM
685 Tuplet_bracket::calc_positions (SCM smob)
686 {
687   Spanner *me = unsmob_spanner (smob);
688
689   /*
690     Don't print if it doesn't span time.
691    */
692   if (robust_scm2moment (me->get_bound (LEFT)->get_column ()->get_property ("when"), Moment (0))
693       == robust_scm2moment (me->get_bound (RIGHT)->get_column ()->get_property ("when"), Moment (0)))
694     {
695       me->suicide ();
696       return SCM_EOL;
697     }
698
699   Real dy = 0.0;
700   Real offset = 0.0;
701   calc_position_and_height (me, &offset, &dy);
702   
703   SCM x = scm_cons (scm_from_double (offset),
704                     scm_from_double (offset + dy));
705   
706   return x;
707 }
708
709 /*
710   similar to beam ?
711 */
712 Direction
713 Tuplet_bracket::get_default_dir (Grob *me)
714 {
715   Drul_array<int> dirs (0, 0);
716   extract_grob_set (me, "note-columns", columns);
717   for (vsize i = 0; i < columns.size (); i++)
718     {
719       Grob *nc = columns[i];
720       Direction d = Note_column::dir (nc);
721       if (d)
722         dirs[d]++;
723     }
724
725   return dirs[UP] >= dirs[DOWN] ? UP : DOWN;
726 }
727
728 void
729 Tuplet_bracket::add_column (Grob *me, Item *n)
730 {
731   Pointer_group_interface::add_grob (me, ly_symbol2scm ("note-columns"), n);
732   add_bound_item (dynamic_cast<Spanner *> (me), n);
733 }
734
735 void
736 Tuplet_bracket::add_tuplet_bracket (Grob *me, Grob *bracket)
737 {
738   Pointer_group_interface::add_grob (me, ly_symbol2scm ("tuplets"), bracket);
739 }
740
741 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_cross_staff, 1);
742 SCM
743 Tuplet_bracket::calc_cross_staff (SCM smob)
744 {
745   Grob *me = unsmob_grob (smob);
746   Grob *staff_symbol = 0;
747   extract_grob_set (me, "note-columns", cols);
748   bool equally_long = false;
749   Grob *par_beam = parallel_beam (me, cols, &equally_long);
750
751   if (par_beam)
752     return par_beam->get_property ("cross-staff");
753
754   for (vsize i = 0; i < cols.size (); i++)
755     {
756       Grob *stem = unsmob_grob (cols[i]->get_object ("stem"));
757       if (!stem)
758         continue;
759       
760       if (to_boolean (stem->get_property ("cross-staff")))
761         return SCM_BOOL_T;
762
763       Grob *stem_staff = Staff_symbol_referencer::get_staff_symbol (stem);
764       if (staff_symbol && (stem_staff != staff_symbol))
765         return SCM_BOOL_T;
766       staff_symbol = stem_staff;
767     }
768   return SCM_BOOL_F;
769 }
770
771 ADD_INTERFACE (Tuplet_bracket,
772                "A bracket with a number in the middle, used for tuplets. "
773                "When the bracket spans  a line break, the value of "
774                "@code{break-overshoot} determines how far it extends "
775                "beyond the staff. "
776                "At a line break, the markups in the @code{edge-text} are printed "
777                "at the edges. ",
778
779
780                /* properties */
781                "bracket-flare "
782                "bracket-visibility "
783                "break-overshoot "
784                "connect-to-neighbor "
785                "control-points "
786                "direction "
787                "edge-height "
788                "edge-text "
789                "gap "
790                "positions "
791                "note-columns "
792                "padding "
793                "tuplet-number "
794                "shorten-pair "
795                "staff-padding "
796                "thickness "
797                "tuplets ");
798
799