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