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