]> git.donarmstrong.com Git - lilypond.git/blob - lily/tuplet-bracket.cc
Show TupletBrackets also on single notes
[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   /* Don't print a tuplet bracket if no control-points were calculated or the tuplet 
284      does not span any time, i.e. a single-note tuplet */
285   SCM cpoints =  me->get_property ("control-points");
286   if (scm_ilength (cpoints) < 2 ||
287       robust_scm2moment (me->get_bound (LEFT)->get_column ()->get_property ("when"), Moment (0))
288       == robust_scm2moment (me->get_bound (RIGHT)->get_column ()->get_property ("when"), Moment (0)))
289     {
290       me->suicide ();
291       return SCM_EOL;
292     }
293   
294   Drul_array<Offset> points;
295   points[LEFT] = ly_scm2offset (scm_car (cpoints));
296   points[RIGHT] = ly_scm2offset (scm_cadr (cpoints));
297   
298   Interval x_span (points[LEFT][X_AXIS], points[RIGHT][X_AXIS]);
299   Drul_array<Real> positions (points[LEFT][Y_AXIS], points[RIGHT][Y_AXIS]);
300
301   Output_def *pap = me->layout ();
302
303   Grob *number_grob = unsmob_grob (me->get_object ("tuplet-number"));
304   
305   /*
306     No bracket when it would be smaller than the number.
307   */
308   Real gap = 0.;
309   if (bracket_visibility && number_grob)
310     {
311       Interval ext = number_grob->extent (number_grob, X_AXIS);
312       if (!ext.is_empty ())
313         {
314           gap = ext.length () + 1.0;
315       
316           if (0.75 * x_span.length () < gap)
317             bracket_visibility = false;
318         }
319     }
320
321   if (bracket_visibility)
322     {
323       Drul_array<Real> zero (0, 0);
324       Real ss = Staff_symbol_referencer::staff_space (me);
325       Drul_array<Real> height
326         = robust_scm2drul (me->get_property ("edge-height"), zero);
327       Drul_array<Real> flare
328         = robust_scm2drul (me->get_property ("bracket-flare"), zero);
329       Drul_array<Real> shorten
330         = robust_scm2drul (me->get_property ("shorten-pair"), zero);
331       Drul_array<Stencil> edge_stencils;
332
333       Direction dir = get_grob_direction (me);
334       
335       scale_drul (&height, -ss * dir);
336       scale_drul (&flare, ss);
337       scale_drul (&shorten, ss);
338
339       Drul_array<bool> connect_to_other =
340         robust_scm2booldrul (me->get_property ("connect-to-neighbor"),
341                              Drul_array<bool> (false, false));
342
343       Direction d = LEFT;
344       do
345         {
346           if (connect_to_other[d])
347             {
348               height[d] = 0.0;
349               flare[d] = 0.0;
350               shorten[d] = 0.0;
351
352               SCM edge_text = me->get_property ("edge-text");
353
354               if (scm_is_pair (edge_text))
355                 {
356                   SCM properties = Font_interface::text_font_alist_chain (me);
357                   SCM text = index_get_cell (edge_text, d);
358                   if (Text_interface::is_markup (text))
359                     {
360                       SCM t = Text_interface::interpret_markup (pap->self_scm (),
361                                                                 properties, text);
362
363                       Stencil *edge_text = unsmob_stencil (t);
364                       edge_text->translate_axis (x_span[d] - x_span[LEFT], X_AXIS);
365                       edge_stencils[d] = *edge_text;
366                     }
367                 }
368             }
369         }
370       while (flip (&d) != LEFT);
371
372       Stencil brack = make_bracket (me, Y_AXIS,
373                                     points[RIGHT] - points[LEFT],
374                                     height,
375                                     /*
376                                       0.1 = more space at right due to italics
377                                       TODO: use italic correction of font.
378                                     */
379                                     Interval (-0.5, 0.5) * gap + 0.1,
380                                     flare, shorten);
381
382       do
383         {
384           if (!edge_stencils[d].is_empty ())
385             brack.add_stencil (edge_stencils[d]);
386         }
387       while (flip (&d) != LEFT);
388
389       mol.add_stencil (brack);
390     }
391
392   mol.translate (points[LEFT]);
393   return mol.smobbed_copy ();
394 }
395
396 /*
397   should move to lookup?
398
399   TODO: this will fail for very short (shorter than the flare)
400   brackets.
401 */
402 Stencil
403 Tuplet_bracket::make_bracket (Grob *me, // for line properties.
404                               Axis protusion_axis,
405                               Offset dz,
406                               Drul_array<Real> height,
407                               Interval gap,
408                               Drul_array<Real> flare,
409                               Drul_array<Real> shorten)
410 {
411   Drul_array<Offset> corners (Offset (0, 0), dz);
412
413   Real length = dz.length ();
414   Drul_array<Offset> gap_corners;
415
416   Axis bracket_axis = other_axis (protusion_axis);
417
418   Drul_array<Offset> straight_corners = corners;
419
420   Direction d = LEFT;
421   do
422     straight_corners[d] += -d * shorten[d] / length * dz;
423   while (flip (&d) != LEFT);
424
425   if (!gap.is_empty ())
426     {
427       do
428         gap_corners[d] = (dz * 0.5) + gap[d] / length * dz;
429       while (flip (&d) != LEFT);
430     }
431
432   Drul_array<Offset> flare_corners = straight_corners;
433   do
434     {
435       flare_corners[d][bracket_axis] = straight_corners[d][bracket_axis];
436       flare_corners[d][protusion_axis] += height[d];
437       straight_corners[d][bracket_axis] += -d * flare[d];
438     }
439   while (flip (&d) != LEFT);
440
441   Stencil m;
442   do
443     {
444       if (!gap.is_empty ())
445         m.add_stencil (Line_interface::line (me, straight_corners[d],
446                                              gap_corners[d]));
447
448       m.add_stencil (Line_interface::line (me, straight_corners[d],
449                                            flare_corners[d]));
450     }
451
452   while (flip (&d) != LEFT);
453
454   if (gap.is_empty ())
455     m.add_stencil (Line_interface::line (me, straight_corners[LEFT],
456                                          straight_corners[RIGHT]));
457   
458   return m;
459 }
460
461 void
462 Tuplet_bracket::get_bounds (Grob *me, Grob **left, Grob **right)
463 {
464   extract_grob_set (me, "note-columns", columns);
465   vsize l = 0;
466   while (l < columns.size () && Note_column::has_rests (columns[l]))
467     l++;
468
469   vsize r = columns.size ();
470   while (r > l && Note_column::has_rests (columns[r-1]))
471     r--;
472
473   *left = *right = 0;
474
475   if (l < r)
476     {
477       *left = columns[l];
478       *right = columns[r-1];
479     }
480 }
481
482 /*
483   use first -> last note for slope, and then correct for disturbing
484   notes in between.  */
485 void
486 Tuplet_bracket::calc_position_and_height (Grob *me_grob, Real *offset, Real *dy)
487 {
488   Spanner *me = dynamic_cast<Spanner *> (me_grob);
489
490   extract_grob_set (me, "note-columns", columns);
491   extract_grob_set (me, "tuplets", tuplets);
492
493   Grob *commony = common_refpoint_of_array (columns, me, Y_AXIS);
494   commony = common_refpoint_of_array (tuplets, commony, Y_AXIS);
495   if (Grob *st = Staff_symbol_referencer::get_staff_symbol (me))
496     commony = st->common_refpoint (commony, Y_AXIS);
497   Real my_offset = me->relative_coordinate (commony, Y_AXIS);
498
499   Grob *commonx = get_common_x (me);
500   commonx = common_refpoint_of_array (tuplets, commonx, Y_AXIS);
501
502   Interval staff;
503   Grob *st = Staff_symbol_referencer::get_staff_symbol (me);
504
505   /* staff-padding doesn't work correctly on cross-staff tuplets
506      because it only considers one staff symbol. Until this works,
507      disable it. */
508   if (st && !to_boolean (me->get_property ("cross-staff")))
509     {
510       Real pad = robust_scm2double (me->get_property ("staff-padding"), -1.0);
511       if  (pad >= 0.0)
512         {
513           staff = st->extent (commony, Y_AXIS) - my_offset;
514           staff.widen (pad);
515         }
516     }
517   
518   Direction dir = get_grob_direction (me);
519
520   bool equally_long = false;
521   Grob *par_beam = parallel_beam (me, columns, &equally_long);
522   
523   Item *lgr = get_x_bound_item (me, LEFT, dir);
524   Item *rgr = get_x_bound_item (me, RIGHT, dir);
525   Real x0 = robust_relative_extent (lgr, commonx, X_AXIS)[LEFT];
526   Real x1 = robust_relative_extent (rgr, commonx, X_AXIS)[RIGHT];
527   bool follow_beam = par_beam
528     && get_grob_direction (par_beam) == dir
529     && ! to_boolean (par_beam->get_property ("knee"));
530
531   vector<Offset> points;
532   if (columns.size ()
533       && follow_beam
534       && Note_column::get_stem (columns[0])
535       && Note_column::get_stem (columns.back ()))
536     {
537       /*
538         trigger set_stem_ends
539        */
540       (void) par_beam->get_property ("quantized-positions");
541
542       Drul_array<Grob *> stems (Note_column::get_stem (columns[0]),
543                                 Note_column::get_stem (columns.back ()));
544
545       Real ss = 0.5 * Staff_symbol_referencer::staff_space (me);
546       Real lp = ss * robust_scm2double (stems[LEFT]->get_property ("stem-end-position"), 0.0)
547         + stems[LEFT]->get_parent (Y_AXIS)->relative_coordinate (commony, Y_AXIS);
548       Real rp = ss * robust_scm2double (stems[RIGHT]->get_property ("stem-end-position"), 0.0)
549         + stems[RIGHT]->get_parent (Y_AXIS)->relative_coordinate (commony, Y_AXIS);
550
551       *dy = rp - lp;
552       points.push_back (Offset (stems[LEFT]->relative_coordinate (commonx, X_AXIS) - x0, lp));
553       points.push_back (Offset (stems[RIGHT]->relative_coordinate (commonx, X_AXIS) - x0, rp));
554     }
555   else
556     {
557       /*
558         Use outer non-rest columns to determine slope
559       */
560       Grob *left_col = 0;
561       Grob *right_col = 0;
562       get_bounds (me, &left_col, &right_col);
563       if (left_col && right_col)
564         {
565           Interval rv = Note_column::cross_staff_extent (right_col, commony);
566           Interval lv = Note_column::cross_staff_extent (left_col, commony);
567           rv.unite (staff);
568           lv.unite (staff);
569
570           Real graphical_dy = rv[dir] - lv[dir];
571
572           Slice ls = Note_column::head_positions_interval (left_col);
573           Slice rs = Note_column::head_positions_interval (right_col);
574
575           Interval musical_dy;
576           musical_dy[UP] = rs[UP] - ls[UP];
577           musical_dy[DOWN] = rs[DOWN] - ls[DOWN];
578           if (sign (musical_dy[UP]) != sign (musical_dy[DOWN]))
579             *dy = 0.0;
580           else if (sign (graphical_dy) != sign (musical_dy[DOWN]))
581             *dy = 0.0;
582           else
583             *dy = graphical_dy;
584         }
585       else
586         *dy = 0;
587
588       for (vsize i = 0; i < columns.size (); i++)
589         {
590           Interval note_ext = Note_column::cross_staff_extent (columns[i], commony);
591           Real x = columns[i]->relative_coordinate (commonx, X_AXIS) - x0;
592
593           points.push_back (Offset (x, note_ext[dir]));
594         }
595     }
596
597   if (!follow_beam)
598     {
599       points.push_back (Offset (x0 - x0, staff[dir]));
600       points.push_back (Offset (x1 - x0, staff[dir]));
601     }
602   
603   /*
604     This is a slight hack. We compute two encompass points from the
605     bbox of the smaller tuplets.
606
607     We assume that the smaller bracket is 1.0 space high.
608   */
609   Real ss = Staff_symbol_referencer::staff_space (me);
610   for (vsize i = 0; i < tuplets.size (); i++)
611     {
612       Interval tuplet_x (tuplets[i]->extent (commonx, X_AXIS));
613       Interval tuplet_y (tuplets[i]->extent (commony, Y_AXIS));
614
615       if (!tuplets[i]->is_live ())
616         continue;
617       
618       Direction d = LEFT;
619       Drul_array<Real> positions = robust_scm2interval (tuplets[i]->get_property ("positions"),
620                                                         Interval (0,0));
621
622       
623       Real other_dy = positions[RIGHT] - positions[LEFT];
624
625       do
626         {
627           Real y
628             = tuplet_y.linear_combination (d * sign (other_dy));
629
630           /*
631             We don't take padding into account for nested tuplets.
632             the edges can come very close to the stems, likewise for
633             nested tuplets?
634           */
635
636           points.push_back (Offset (tuplet_x[d] - x0, y));
637         }
638       while (flip (&d) != LEFT);
639     }
640
641   *offset = -dir * infinity_f;
642   Real factor = (columns.size () > 1) ? 1 / (x1 - x0) : 1.0;
643   for (vsize i = 0; i < points.size (); i++)
644     {
645       Real x = points[i][X_AXIS];
646       Real tuplety = (*dy) * x * factor + my_offset;
647
648       if (points[i][Y_AXIS] * dir > (*offset + tuplety) * dir)
649         *offset = points[i][Y_AXIS] - tuplety;
650     }
651
652   *offset += scm_to_double (me->get_property ("padding")) * dir;
653
654   /*
655     horizontal brackets should not collide with staff lines.
656
657     Kind of pointless since we put them outside the staff anyway, but
658     let's leave code for the future when possibly allow them to move
659     into the staff once again.
660   
661     This doesn't seem to support cross-staff tuplets atm.
662   */
663   if (*dy == 0
664       && fabs (*offset) < ss * Staff_symbol_referencer::staff_radius (me))
665     {
666       // quantize, then do collision check.
667       *offset *= 2 / ss;
668
669       *offset = rint (*offset);
670       if (Staff_symbol_referencer::on_line (me, (int) rint (*offset)))
671         *offset += dir;
672
673       *offset *= 0.5 * ss;
674     }
675 }
676
677
678 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_direction, 1);
679 SCM
680 Tuplet_bracket::calc_direction (SCM smob)
681 {
682   Grob *me = unsmob_grob (smob);
683   Direction dir = Tuplet_bracket::get_default_dir (me);
684   return scm_from_int (dir);
685 }
686
687 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_positions, 1);
688 SCM
689 Tuplet_bracket::calc_positions (SCM smob)
690 {
691   Spanner *me = unsmob_spanner (smob);
692
693   Real dy = 0.0;
694   Real offset = 0.0;
695   calc_position_and_height (me, &offset, &dy);
696   
697   SCM x = scm_cons (scm_from_double (offset),
698                     scm_from_double (offset + dy));
699   
700   return x;
701 }
702
703 /*
704   similar to beam ?
705 */
706 Direction
707 Tuplet_bracket::get_default_dir (Grob *me)
708 {
709   Drul_array<int> dirs (0, 0);
710   extract_grob_set (me, "note-columns", columns);
711   for (vsize i = 0; i < columns.size (); i++)
712     {
713       Grob *nc = columns[i];
714       Direction d = Note_column::dir (nc);
715       if (d)
716         dirs[d]++;
717     }
718
719   return dirs[UP] >= dirs[DOWN] ? UP : DOWN;
720 }
721
722 void
723 Tuplet_bracket::add_column (Grob *me, Item *n)
724 {
725   Pointer_group_interface::add_grob (me, ly_symbol2scm ("note-columns"), n);
726   add_bound_item (dynamic_cast<Spanner *> (me), n);
727 }
728
729 void
730 Tuplet_bracket::add_tuplet_bracket (Grob *me, Grob *bracket)
731 {
732   Pointer_group_interface::add_grob (me, ly_symbol2scm ("tuplets"), bracket);
733 }
734
735 MAKE_SCHEME_CALLBACK (Tuplet_bracket, calc_cross_staff, 1);
736 SCM
737 Tuplet_bracket::calc_cross_staff (SCM smob)
738 {
739   Grob *me = unsmob_grob (smob);
740   Grob *staff_symbol = 0;
741   extract_grob_set (me, "note-columns", cols);
742   bool equally_long = false;
743   Grob *par_beam = parallel_beam (me, cols, &equally_long);
744
745   if (par_beam)
746     return par_beam->get_property ("cross-staff");
747
748   for (vsize i = 0; i < cols.size (); i++)
749     {
750       Grob *stem = unsmob_grob (cols[i]->get_object ("stem"));
751       if (!stem)
752         continue;
753       
754       if (to_boolean (stem->get_property ("cross-staff")))
755         return SCM_BOOL_T;
756
757       Grob *stem_staff = Staff_symbol_referencer::get_staff_symbol (stem);
758       if (staff_symbol && (stem_staff != staff_symbol))
759         return SCM_BOOL_T;
760       staff_symbol = stem_staff;
761     }
762   return SCM_BOOL_F;
763 }
764
765 ADD_INTERFACE (Tuplet_bracket,
766                "A bracket with a number in the middle, used for tuplets. "
767                "When the bracket spans  a line break, the value of "
768                "@code{break-overshoot} determines how far it extends "
769                "beyond the staff. "
770                "At a line break, the markups in the @code{edge-text} are printed "
771                "at the edges. ",
772
773
774                /* properties */
775                "bracket-flare "
776                "bracket-visibility "
777                "break-overshoot "
778                "connect-to-neighbor "
779                "control-points "
780                "direction "
781                "edge-height "
782                "edge-text "
783                "gap "
784                "positions "
785                "note-columns "
786                "padding "
787                "tuplet-number "
788                "shorten-pair "
789                "staff-padding "
790                "thickness "
791                "tuplets ");
792
793