]> git.donarmstrong.com Git - lilypond.git/blob - lily/slur.cc
Better approximations for cross-staff slurs
[lilypond.git] / lily / slur.cc
1 /*
2   This file is part of LilyPond, the GNU music typesetter.
3
4   Copyright (C) 1996--2012 Han-Wen Nienhuys <hanwen@xs4all.nl>
5   Jan Nieuwenhuizen <janneke@gnu.org>
6
7   LilyPond is free software: you can redistribute it and/or modify
8   it under the terms of the GNU General Public License as published by
9   the Free Software Foundation, either version 3 of the License, or
10   (at your option) any later version.
11
12   LilyPond is distributed in the hope that it will be useful,
13   but WITHOUT ANY WARRANTY; without even the implied warranty of
14   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   GNU General Public License for more details.
16
17   You should have received a copy of the GNU General Public License
18   along with LilyPond.  If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "slur.hh"
22 #include "grob-info.hh"
23 #include "grob-array.hh"
24 #include "beam.hh"
25 #include "bezier.hh"
26 #include "directional-element-interface.hh"
27 #include "font-interface.hh"
28 #include "item.hh"
29 #include "pointer-group-interface.hh"
30 #include "lookup.hh"
31 #include "main.hh"              // DEBUG_SLUR_SCORING
32 #include "note-column.hh"
33 #include "output-def.hh"
34 #include "skyline-pair.hh"
35 #include "spanner.hh"
36 #include "staff-symbol-referencer.hh"
37 #include "stem.hh"
38 #include "text-interface.hh"
39 #include "tie.hh"
40 #include "warn.hh"
41 #include "slur-scoring.hh"
42 #include "separation-item.hh"
43 #include "international.hh"
44
45 MAKE_SCHEME_CALLBACK (Slur, calc_direction, 1)
46 SCM
47 Slur::calc_direction (SCM smob)
48 {
49   Grob *me = unsmob_grob (smob);
50   extract_grob_set (me, "note-columns", encompasses);
51
52   if (encompasses.empty ())
53     {
54       me->suicide ();
55       return SCM_BOOL_F;
56     }
57
58   Direction d = DOWN;
59   for (vsize i = 0; i < encompasses.size (); i++)
60     {
61       if (Note_column::dir (encompasses[i]) < 0)
62         {
63           d = UP;
64           break;
65         }
66     }
67   return scm_from_int (d);
68 }
69
70 MAKE_SCHEME_CALLBACK (Slur, pure_height, 3);
71 SCM
72 Slur::pure_height (SCM smob, SCM start_scm, SCM end_scm)
73 {
74   /*
75     Note that this estimation uses a rote add-on of 0.5 to the
76     highest encompassed note-head for a slur estimate.  This is,
77     in most cases, shorter than the actual slur.
78
79     Ways to improve this could include:
80     -- adding extra height for scripts that avoid slurs on the inside
81     -- adding extra height for the "bulge" in a slur above a note head
82   */
83   Grob *me = unsmob_grob (smob);
84   int start = scm_to_int (start_scm);
85   int end = scm_to_int (end_scm);
86   Direction dir = get_grob_direction (me);
87
88   extract_grob_set (me, "note-columns", encompasses);
89   Interval ret;
90   ret.set_empty ();
91
92   Grob *parent = me->get_parent (Y_AXIS);
93   Drul_array<Real> extremal_heights (infinity_f, -infinity_f);
94   if (common_refpoint_of_array (encompasses, me, Y_AXIS) != parent)
95     /* this could happen if, for example, we are a cross-staff slur.
96        in this case, we want to be ignored */
97     return ly_interval2scm (Interval ());
98
99   for (vsize i = 0; i < encompasses.size (); i++)
100     {
101       Interval d = encompasses[i]->pure_height (parent, start, end);
102       if (!d.is_empty ())
103         {
104           for (DOWN_and_UP (downup))
105             ret.add_point (d[dir]);
106
107           if (extremal_heights[LEFT] == infinity_f)
108             extremal_heights[LEFT] = d[dir];
109           extremal_heights[RIGHT] = d[dir];
110         }
111     }
112
113   if (ret.is_empty ())
114     return ly_interval2scm (Interval ());
115
116   Interval extremal_span;
117   extremal_span.set_empty ();
118   for (LEFT_and_RIGHT (d))
119     extremal_span.add_point (extremal_heights[d]);
120   ret[-dir] = minmax (dir, extremal_span[-dir], ret[-dir]);
121
122   /*
123     The +0.5 comes from the fact that we try to place a slur
124     0.5 staff spaces from the note-head.
125     (see Slur_score_state.get_base_attachments ())
126   */
127   ret += 0.5 * dir;
128   return ly_interval2scm (ret);
129 }
130
131 MAKE_SCHEME_CALLBACK (Slur, height, 1);
132 SCM
133 Slur::height (SCM smob)
134 {
135   Grob *me = unsmob_grob (smob);
136
137   // FIXME uncached
138   Stencil *m = me->get_stencil ();
139   return m ? ly_interval2scm (m->extent (Y_AXIS))
140          : ly_interval2scm (Interval ());
141 }
142
143 MAKE_SCHEME_CALLBACK (Slur, print, 1);
144 SCM
145 Slur::print (SCM smob)
146 {
147   Grob *me = unsmob_grob (smob);
148
149   extract_grob_set (me, "note-columns", encompasses);
150   if (encompasses.empty ())
151     {
152       me->suicide ();
153       return SCM_EOL;
154     }
155
156   Real staff_thick = Staff_symbol_referencer::line_thickness (me);
157   Real base_thick = staff_thick
158                     * robust_scm2double (me->get_property ("thickness"), 1);
159   Real line_thick = staff_thick
160                     * robust_scm2double (me->get_property ("line-thickness"), 1);
161
162   Bezier one = get_curve (me);
163   Stencil a;
164
165   SCM dash_definition = me->get_property ("dash-definition");
166   a = Lookup::slur (one,
167                     get_grob_direction (me) * base_thick,
168                     line_thick,
169                     dash_definition);
170
171 #if DEBUG_SLUR_SCORING
172   SCM annotation = me->get_property ("annotation");
173   if (scm_is_string (annotation))
174     {
175       string str;
176       SCM properties = Font_interface::text_font_alist_chain (me);
177
178       if (!scm_is_number (me->get_property ("font-size")))
179         properties = scm_cons (scm_acons (ly_symbol2scm ("font-size"), scm_from_int (-6), SCM_EOL),
180                                properties);
181
182       Stencil tm = *unsmob_stencil (Text_interface::interpret_markup
183                                     (me->layout ()->self_scm (), properties,
184                                      annotation));
185       a.add_at_edge (Y_AXIS, get_grob_direction (me), tm, 1.0);
186     }
187 #endif
188
189   return a.smobbed_copy ();
190 }
191
192 /*
193   it would be better to do this at engraver level, but that is
194   fragile, as the breakable items are generated on staff level, at
195   which point slur starts and ends have to be tracked
196 */
197 void
198 Slur::replace_breakable_encompass_objects (Grob *me)
199 {
200   extract_grob_set (me, "encompass-objects", extra_objects);
201   vector<Grob *> new_encompasses;
202
203   for (vsize i = 0; i < extra_objects.size (); i++)
204     {
205       Grob *g = extra_objects[i];
206
207       if (Separation_item::has_interface (g))
208         {
209           extract_grob_set (g, "elements", breakables);
210           for (vsize j = 0; j < breakables.size (); j++)
211             /* if we encompass a separation-item that spans multiple staves,
212                we filter out the grobs that don't belong to our staff */
213             if (me->common_refpoint (breakables[j], Y_AXIS) == me->get_parent (Y_AXIS)
214                 && breakables[j]->get_property ("avoid-slur") == ly_symbol2scm ("inside"))
215               new_encompasses.push_back (breakables[j]);
216         }
217       else
218         new_encompasses.push_back (g);
219     }
220
221   SCM encompass_scm = me->get_object ("encompass-objects");
222   if (Grob_array::unsmob (encompass_scm))
223     {
224       vector<Grob *> &arr
225         = unsmob_grob_array (encompass_scm)->array_reference ();
226       arr = new_encompasses;
227     }
228 }
229
230 Bezier
231 Slur::get_curve (Grob *me)
232 {
233   Bezier b;
234   int i = 0;
235   for (SCM s = me->get_property ("control-points"); scm_is_pair (s);
236        s = scm_cdr (s))
237     b.control_[i++] = ly_scm2offset (scm_car (s));
238
239   return b;
240 }
241
242 void
243 Slur::add_column (Grob *me, Grob *n)
244 {
245   Pointer_group_interface::add_grob (me, ly_symbol2scm ("note-columns"), n);
246   add_bound_item (dynamic_cast<Spanner *> (me), n);
247 }
248
249 void
250 Slur::add_extra_encompass (Grob *me, Grob *n)
251 {
252   Pointer_group_interface::add_grob (me, ly_symbol2scm ("encompass-objects"), n);
253 }
254
255 void
256 Slur::main_to_stub (Grob *main, Grob *stub)
257 {
258   extract_grob_set (main, "note-columns", nc);
259   for (vsize i = 0; i < nc.size (); i++)
260     add_column (stub, nc[i]);
261
262   extract_grob_set (main, "encompass-objects", eo);
263   for (vsize i = 0; i < eo.size (); i++)
264     add_extra_encompass (stub, eo[i]);
265
266   stub->set_object ("surrogate", main->self_scm ());
267
268   dynamic_cast<Spanner *> (stub)->set_bound
269     (LEFT, dynamic_cast<Spanner *> (main)->get_bound (LEFT));
270   dynamic_cast<Spanner *> (stub)->set_bound
271     (RIGHT, dynamic_cast<Spanner *> (main)->get_bound (RIGHT));
272 }
273
274 MAKE_SCHEME_CALLBACK_WITH_OPTARGS (Slur, pure_outside_slur_callback, 4, 1, "");
275 SCM
276 Slur::pure_outside_slur_callback (SCM grob, SCM start_scm, SCM end_scm, SCM offset_scm)
277 {
278   int start = robust_scm2int (start_scm, 0);
279   int end = robust_scm2int (end_scm, 0);
280   Grob *script = unsmob_grob (grob);
281   Grob *slur = unsmob_grob (script->get_object ("slur"));
282   if (!slur)
283     return offset_scm;
284
285   SCM avoid = script->get_property ("avoid-slur");
286   if (avoid != ly_symbol2scm ("outside") && avoid != ly_symbol2scm ("around"))
287     return offset_scm;
288
289   Real offset = robust_scm2double (offset_scm, 0.0);
290   Direction dir = get_grob_direction (script);
291   return scm_from_double (offset + dir * slur->pure_height (slur, start, end).length () / 4);
292 }
293
294 MAKE_SCHEME_CALLBACK_WITH_OPTARGS (Slur, outside_slur_callback, 2, 1, "");
295 SCM
296 Slur::outside_slur_callback (SCM grob, SCM offset_scm)
297 {
298   Grob *script = unsmob_grob (grob);
299   Grob *slur = unsmob_grob (script->get_object ("slur"));
300
301   if (!slur)
302     return offset_scm;
303
304   SCM avoid = script->get_property ("avoid-slur");
305   if (avoid != ly_symbol2scm ("outside")
306       && avoid != ly_symbol2scm ("around"))
307     return offset_scm;
308
309   Direction dir = get_grob_direction (script);
310   if (dir == CENTER)
311     return offset_scm;
312
313   Grob *cx = script->common_refpoint (slur, X_AXIS);
314   Grob *cy = script->common_refpoint (slur, Y_AXIS);
315
316   Bezier curve = Slur::get_curve (slur);
317
318   curve.translate (Offset (slur->relative_coordinate (cx, X_AXIS),
319                            slur->relative_coordinate (cy, Y_AXIS)));
320
321   Interval yext = robust_relative_extent (script, cy, Y_AXIS);
322   Interval xext = robust_relative_extent (script, cx, X_AXIS);
323   Interval slur_wid (curve.control_[0][X_AXIS], curve.control_[3][X_AXIS]);
324
325   /*
326     cannot use is_empty because some 0-extent scripts
327     come up with TabStaffs.
328   */
329   if (xext.length () <= 0 || yext.length () <= 0)
330     return offset_scm;
331
332   bool contains = false;
333   for (LEFT_and_RIGHT (d))
334     contains |= slur_wid.contains (xext[d]);
335
336   if (!contains)
337     return offset_scm;
338
339   Real offset = robust_scm2double (offset_scm, 0);
340   yext.translate (offset);
341
342   /* FIXME: slur property, script property?  */
343   Real slur_padding = robust_scm2double (script->get_property ("slur-padding"),
344                                          0.0);
345   yext.widen (slur_padding);
346
347   Interval exts[] = {xext, yext};
348   bool do_shift = false;
349   Real EPS = 1.0e-5;
350   if (avoid == ly_symbol2scm ("outside"))
351     {
352       for (LEFT_and_RIGHT (d))
353         {
354           Real x = minmax (-d, xext[d], curve.control_[d == LEFT ? 0 : 3][X_AXIS] + -d * EPS);
355           Real y = curve.get_other_coordinate (X_AXIS, x);
356           do_shift = y == minmax (dir, yext[-dir], y);
357           if (do_shift)
358             break;
359         }
360     }
361   else
362     {
363       for (int a = X_AXIS; a < NO_AXES; a++)
364         {
365           for (LEFT_and_RIGHT (d))
366             {
367               vector<Real> coords = curve.get_other_coordinates (Axis (a), exts[a][d]);
368               for (vsize i = 0; i < coords.size (); i++)
369                 {
370                   do_shift = exts[(a + 1) % NO_AXES].contains (coords[i]);
371                   if (do_shift)
372                     break;
373                 }
374               if (do_shift)
375                 break;
376             }
377           if (do_shift)
378             break;
379         }
380     }
381
382   Real avoidance_offset = do_shift ? curve.minmax (X_AXIS, max (xext[LEFT], curve.control_[0][X_AXIS] + EPS), min (xext[RIGHT], curve.control_[3][X_AXIS] - EPS), dir) - yext[-dir] : 0.0;
383
384   return scm_from_double (offset + avoidance_offset);
385 }
386
387 MAKE_SCHEME_CALLBACK (Slur, vertical_skylines, 1);
388 SCM
389 Slur::vertical_skylines (SCM smob)
390 {
391   Grob *me = unsmob_grob (smob);
392   vector<Box> boxes;
393
394   if (!me)
395     return Skyline_pair (boxes, X_AXIS).smobbed_copy ();
396
397   Bezier curve = Slur::get_curve (me);
398   vsize box_count = robust_scm2vsize (me->get_property ("skyline-quantizing"), 10);
399   for (vsize i = 0; i < box_count; i++)
400     {
401       Box b;
402       b.add_point (curve.curve_point (i * 1.0 / box_count));
403       b.add_point (curve.curve_point ((i + 1) * 1.0 / box_count));
404       boxes.push_back (b);
405     }
406
407   return Skyline_pair (boxes, X_AXIS).smobbed_copy ();
408 }
409
410 /*
411  * USE ME ONLY FOR CROSS STAFF SLURS!
412  * We only want to keep the topmost skyline of the topmost axis group(s)
413  * and the bottommost skyline of the bottommost axis group(s). Otherwise,
414  * the VerticalAxisGroups will be spaced very far apart to accommodate the
415  * slur, which we don't want, as it is cross staff.
416  *
417  * TODO: Currently, the code below keeps the topmost and bottommost axis
418  * groups and gets rid of the rest.  This should be more nuanced for
419  * cases like ossias where the topmost staff changes over the course of
420  * the slur.  Ditto for the bottommost staff.
421  */
422
423 MAKE_SCHEME_CALLBACK (Slur, extremal_stub_vertical_skylines, 1);
424 SCM
425 Slur::extremal_stub_vertical_skylines (SCM smob)
426 {
427   Grob *me = unsmob_grob (smob);
428   Grob *my_vag = Grob::get_vertical_axis_group (me);
429   extract_grob_set (me, "note-columns", ro_note_columns);
430   vector<Grob *> note_columns (ro_note_columns);
431   vector_sort (note_columns, Grob::vertical_less);
432   bool highest = my_vag == Grob::get_vertical_axis_group (note_columns[0]);
433   bool lowest = my_vag == Grob::get_vertical_axis_group (note_columns.back ());
434   if (!highest && !lowest)
435     return Skyline_pair ().smobbed_copy ();
436
437   Skyline_pair sky = *Skyline_pair::unsmob (vertical_skylines (smob));
438
439   if (highest)
440     sky[DOWN] = Skyline (DOWN);
441   else
442     sky[UP] = Skyline (UP);
443
444   return sky.smobbed_copy ();
445 }
446
447 /*
448  * Used by Slur_engraver:: and Phrasing_slur_engraver::
449  */
450 void
451 Slur::auxiliary_acknowledge_extra_object (Grob_info const &info,
452                                           vector<Slur_info> &slur_infos,
453                                           vector<Slur_info> &end_slur_infos)
454 {
455   if (slur_infos.empty () && end_slur_infos.empty ())
456     return;
457
458   Grob *e = info.grob ();
459   SCM avoid = e->get_property ("avoid-slur");
460   Grob *slur;
461   if (end_slur_infos.size () && !slur_infos.size ())
462     slur = end_slur_infos[0].slur_;
463   else
464     slur = slur_infos[0].slur_;
465
466   if (Tie::has_interface (e)
467       || avoid == ly_symbol2scm ("inside"))
468     {
469       for (vsize i = slur_infos.size (); i--;)
470         add_extra_encompass (slur_infos[i].slur_, e);
471       for (vsize i = end_slur_infos.size (); i--;)
472         add_extra_encompass (end_slur_infos[i].slur_, e);
473       if (slur)
474         e->set_object ("slur", slur->self_scm ());
475     }
476   else if (avoid == ly_symbol2scm ("outside")
477            || avoid == ly_symbol2scm ("around"))
478     {
479       if (slur)
480         {
481           chain_offset_callback (e, outside_slur_callback_proc, Y_AXIS);
482           chain_callback (e, outside_slur_cross_staff_proc, ly_symbol2scm ("cross-staff"));
483           e->set_object ("slur", slur->self_scm ());
484         }
485     }
486   else if (avoid != ly_symbol2scm ("ignore"))
487     e->warning (_f ("Ignoring grob for slur: %s.  avoid-slur not set?",
488                     e->name ().c_str ()));
489 }
490
491 /*
492   A callback that will be chained together with the original cross-staff
493   value of a grob that is placed 'outside or 'around a slur. This just says
494   that any grob becomes cross-staff if it is placed 'outside or 'around a
495   cross-staff slur.
496 */
497 MAKE_SCHEME_CALLBACK (Slur, outside_slur_cross_staff, 2)
498 SCM
499 Slur::outside_slur_cross_staff (SCM smob, SCM previous)
500 {
501   if (previous == SCM_BOOL_T)
502     return previous;
503
504   Grob *me = unsmob_grob (smob);
505   Grob *slur = unsmob_grob (me->get_object ("slur"));
506
507   if (!slur)
508     return SCM_BOOL_F;
509   return slur->get_property ("cross-staff");
510 }
511
512 MAKE_SCHEME_CALLBACK (Slur, calc_cross_staff, 1)
513 SCM
514 Slur::calc_cross_staff (SCM smob)
515 {
516   Grob *me = unsmob_grob (smob);
517
518   extract_grob_set (me, "note-columns", cols);
519   extract_grob_set (me, "encompass-objects", extras);
520
521   for (vsize i = 0; i < cols.size (); i++)
522     {
523       if (Grob *s = Note_column::get_stem (cols[i]))
524         if (to_boolean (s->get_property ("cross-staff")))
525           return SCM_BOOL_T;
526     }
527
528   /* the separation items are dealt with in replace_breakable_encompass_objects
529      so we can ignore them here */
530   vector<Grob *> non_sep_extras;
531   for (vsize i = 0; i < extras.size (); i++)
532     if (!Separation_item::has_interface (extras[i]))
533       non_sep_extras.push_back (extras[i]);
534
535   Grob *common = common_refpoint_of_array (cols, me, Y_AXIS);
536   common = common_refpoint_of_array (non_sep_extras, common, Y_AXIS);
537
538   return scm_from_bool (common != me->get_parent (Y_AXIS));
539 }
540
541 ADD_INTERFACE (Slur,
542                "A slur."
543                "\n"
544                "The following properties may be set in the @code{details}"
545                " list.\n"
546                "\n"
547                "@table @code\n"
548                "@item region-size\n"
549                "Size of region (in staff spaces) for determining"
550                " potential endpoints in the Y direction.\n"
551                "@item head-encompass-penalty\n"
552                "Demerit to apply when note heads collide with a slur.\n"
553                "@item stem-encompass-penalty\n"
554                "Demerit to apply when stems collide with a slur.\n"
555                "@item edge-attraction-factor\n"
556                "Factor used to calculate the demerit for distances"
557                " between slur endpoints and their corresponding base"
558                " attachments.\n"
559                "@item same-slope-penalty\n"
560                "Demerit for slurs with attachment points that are"
561                " horizontally aligned.\n"
562                "@item steeper-slope-factor\n"
563                "Factor used to calculate demerit only if this slur is"
564                " not broken.\n"
565                "@item non-horizontal-penalty\n"
566                "Demerit for slurs with attachment points that are not"
567                " horizontally aligned.\n"
568                "@item max-slope\n"
569                "The maximum slope allowed for this slur.\n"
570                "@item max-slope-factor\n"
571                "Factor that calculates demerit based on the max slope.\n"
572                "@item free-head-distance\n"
573                "The amount of vertical free space that must exist"
574                " between a slur and note heads.\n"
575                "@item absolute-closeness-measure\n"
576                "Factor to calculate demerit for variance between a note"
577                " head and slur.\n"
578                "@item extra-object-collision-penalty\n"
579                "Factor to calculate demerit for extra objects that the"
580                " slur encompasses, including accidentals, fingerings, and"
581                " tuplet numbers.\n"
582                "@item accidental-collision\n"
583                "Factor to calculate demerit for @code{Accidental} objects"
584                " that the slur encompasses.  This property value replaces"
585                " the value of @code{extra-object-collision-penalty}.\n"
586                "@item extra-encompass-free-distance\n"
587                "The amount of vertical free space that must exist"
588                " between a slur and various objects it encompasses,"
589                " including accidentals, fingerings, and tuplet numbers.\n"
590                "@item extra-encompass-collision-distance\n"
591                "This detail is currently unused.\n"
592                "@item head-slur-distance-factor\n"
593                "Factor to calculate demerit for variance between a note"
594                " head and slur.\n"
595                "@item head-slur-distance-max-ratio\n"
596                "The maximum value for the ratio of distance between a"
597                " note head and slur.\n"
598                "@item free-slur-distance\n"
599                "The amount of vertical free space that must exist"
600                " between adjacent slurs.  This subproperty only works"
601                " for @code{PhrasingSlur}.\n"
602                "@item edge-slope-exponent\n"
603                "Factor used to calculate the demerit for the slope of"
604                " a slur near its endpoints; a larger value yields a"
605                " larger demerit.\n"
606                "@end table\n",
607
608                /* properties */
609                "annotation "
610                "avoid-slur "  /* UGH. */
611                "control-points "
612                "dash-definition "
613                "details "
614                "direction "
615                "eccentricity "
616                "encompass-objects "
617                "height-limit "
618                "inspect-quants "
619                "inspect-index "
620                "line-thickness "
621                "note-columns "
622                "positions "
623                "ratio "
624                "thickness "
625               );
626