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