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