From 3bcfd69d91f52f70598f73719fbed7aa6eea8ad3 Mon Sep 17 00:00:00 2001 From: Mike Solomon Date: Thu, 15 Dec 2011 16:49:40 +0100 Subject: [PATCH] Prevents dynamics from colliding with cross-staff stems. Creates an Interval_minefield class that tracks intersections of intevals and returns the closest "safe spots" to the original interval on either side. Dynamics are then moved to one of these two safe spots depending on the collision-bias property (with negative values forcing the decision towards the left and positive towards the right). --- .../dynamics-avoid-cross-staff-stem.ly | 16 ++++ lily/beam-quanting.cc | 39 ++------- lily/include/interval-minefield.hh | 42 ++++++++++ lily/include/self-alignment-interface.hh | 5 +- lily/interval-minefield.cc | 82 +++++++++++++++++++ lily/new-dynamic-engraver.cc | 4 + lily/self-alignment-interface.cc | 78 ++++++++++++++++++ scm/define-grob-properties.scm | 11 +++ scm/define-grobs.scm | 3 + scm/output-lib.scm | 1 - 10 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 input/regression/dynamics-avoid-cross-staff-stem.ly create mode 100644 lily/include/interval-minefield.hh create mode 100644 lily/interval-minefield.cc diff --git a/input/regression/dynamics-avoid-cross-staff-stem.ly b/input/regression/dynamics-avoid-cross-staff-stem.ly new file mode 100644 index 0000000000..fdf1a778e0 --- /dev/null +++ b/input/regression/dynamics-avoid-cross-staff-stem.ly @@ -0,0 +1,16 @@ +\version "2.15.22" + +\header { + texidoc = "LilyPond automatically shifts dynamics that collide with +cross-staff stems when manual beams are used." +} + +\new GrandStaff << + \new Staff = "PnRH" { + \relative g { + \stemDown gis8 \p [ \change Staff = "PnLH" \stemUp a, \fff ] + \change Staff = "PnRH" r4 + } + } + \new Staff = "PnLH" { \clef "F" { s4 r4 } } +>> diff --git a/lily/beam-quanting.cc b/lily/beam-quanting.cc index 57cf25853d..e971d6ee9d 100644 --- a/lily/beam-quanting.cc +++ b/lily/beam-quanting.cc @@ -33,6 +33,7 @@ using namespace std; #include "grob-array.hh" #include "item.hh" #include "international.hh" +#include "interval-minefield.hh" #include "least-squares.hh" #include "libc-extension.hh" #include "main.hh" @@ -811,42 +812,14 @@ Beam_scoring_problem::shift_region_to_valid () } vector_sort (forbidden_intervals, Interval::left_less); - Real epsilon = 1.0e-10; Real beam_left_y = unquanted_y_[LEFT]; Interval feasible_beam_placements (beam_left_y, beam_left_y); - /* - forbidden_intervals contains a vector of intervals in which - the beam cannot start. it iterates through these intervals, - pushing feasible_beam_placements epsilon over or epsilon under a - collision. when this type of change happens, the loop is marked - as "dirty" and re-iterated. - - TODO: figure out a faster ways that this loop can happen via - a better search algorithm and/or OOP. - */ - - bool dirty = false; - do - { - dirty = false; - for (vsize i = 0; i < forbidden_intervals.size (); i++) - { - Direction d = DOWN; - do - { - if (forbidden_intervals[i][d] == d * infinity_f) - feasible_beam_placements[d] = d * infinity_f; - else if (forbidden_intervals[i].contains (feasible_beam_placements[d])) - { - feasible_beam_placements[d] = d * epsilon + forbidden_intervals[i][d]; - dirty = true; - } - } - while (flip (&d) != DOWN); - } - } - while (dirty); + Interval_minefield minefield (feasible_beam_placements, 0.0); + for (vsize i = 0; i < forbidden_intervals.size (); i++) + minefield.add_forbidden_interval (forbidden_intervals[i]); + minefield.solve (); + feasible_beam_placements = minefield.feasible_placements (); // if the beam placement falls out of the feasible region, we push it // to infinity so that it can never be a feasible candidate below diff --git a/lily/include/interval-minefield.hh b/lily/include/interval-minefield.hh new file mode 100644 index 0000000000..62136b046b --- /dev/null +++ b/lily/include/interval-minefield.hh @@ -0,0 +1,42 @@ +/* + This file is part of LilyPond, the GNU music typesetter. + + Copyright (C) 2011 Mike Solomon + Jan Nieuwenhuizen + + LilyPond is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + LilyPond is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with LilyPond. If not, see . +*/ + +#ifndef INTERVAL_MINEFIELD_HH +#define INTERVAL_MINEFIELD_HH + +#include "lily-proto.hh" +#include "std-vector.hh" +#include "interval.hh" + +class Interval_minefield +{ +public : + Interval_minefield (Interval, Real); + void add_forbidden_interval (Interval forbidden); + Interval feasible_placements (); + void solve (); + +private : + vector forbidden_intervals_; + Interval feasible_placements_; + Real bulk_; +}; + +#endif // INTERVAL_MINEFIELD_HH \ No newline at end of file diff --git a/lily/include/self-alignment-interface.hh b/lily/include/self-alignment-interface.hh index 8ecde08a3e..80a7c0d37a 100644 --- a/lily/include/self-alignment-interface.hh +++ b/lily/include/self-alignment-interface.hh @@ -30,8 +30,10 @@ struct Self_alignment_interface static SCM aligned_on_self (Grob *me, Axis a, bool pure, int start, int end); static SCM centered_on_object (Grob *me, Axis a); static SCM aligned_on_parent (Grob *me, Axis a); + static SCM avoid_colliding_grobs (Grob *me, Axis a, Real offset); static void set_center_parent (Grob *me, Axis a); static void set_align_self (Grob *me, Axis a); + static void avoid_x_collisions (Grob *me); DECLARE_SCHEME_CALLBACK (x_aligned_on_self, (SCM element)); DECLARE_SCHEME_CALLBACK (y_aligned_on_self, (SCM element)); @@ -41,7 +43,8 @@ struct Self_alignment_interface DECLARE_SCHEME_CALLBACK (centered_on_x_parent, (SCM element)); DECLARE_SCHEME_CALLBACK (centered_on_y_parent, (SCM element)); DECLARE_SCHEME_CALLBACK (x_centered_on_y_parent, (SCM element)); - + DECLARE_SCHEME_CALLBACK (avoid_x_colliding_grobs, (SCM element, SCM offset)); + DECLARE_SCHEME_CALLBACK (x_colliding_grobs, (SCM element)); DECLARE_SCHEME_CALLBACK (aligned_on_x_parent, (SCM element)); DECLARE_SCHEME_CALLBACK (aligned_on_y_parent, (SCM element)); }; diff --git a/lily/interval-minefield.cc b/lily/interval-minefield.cc new file mode 100644 index 0000000000..5fe3414993 --- /dev/null +++ b/lily/interval-minefield.cc @@ -0,0 +1,82 @@ +/* + This file is part of LilyPond, the GNU music typesetter. + + Copyright (C) 2011 Mike Solomon + Jan Nieuwenhuizen + + LilyPond is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + LilyPond is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with LilyPond. If not, see . +*/ + +#include "interval-minefield.hh" +#include "grob.hh" +Interval_minefield::Interval_minefield (Interval feasible_placements, Real bulk) +{ + feasible_placements_ = feasible_placements; + bulk_ = bulk; +} + +void +Interval_minefield::add_forbidden_interval (Interval forbidden) +{ + forbidden_intervals_.push_back (forbidden); +} + +Interval +Interval_minefield::feasible_placements () +{ + return feasible_placements_; +} + + /* + forbidden_intervals_ contains a vector of intervals in which + the beam cannot start. it iterates through these intervals, + pushing feasible_placements_ epsilon over or epsilon under a + collision. when this type of change happens, the loop is marked + as "dirty" and re-iterated. + + TODO: figure out a faster ways that this loop can happen via + a better search algorithm. + */ +void +Interval_minefield::solve() +{ + Real epsilon = 1.0e-10; + bool dirty = false; + do + { + dirty = false; + for (vsize i = 0; i < forbidden_intervals_.size (); i++) + { + Direction d = DOWN; + do + { + Interval feasible_widened = Interval (feasible_placements_[d], feasible_placements_[d]); + feasible_widened.widen (bulk_ / 2.); + + if (forbidden_intervals_[i][d] == d * infinity_f) + feasible_placements_[d] = d * infinity_f; + else if (forbidden_intervals_[i].contains (feasible_widened[d]) + || forbidden_intervals_[i].contains (feasible_widened[-d]) + || feasible_widened.contains (forbidden_intervals_[i][d]) + || feasible_widened.contains (forbidden_intervals_[i][-d])) + { + feasible_placements_[d] = forbidden_intervals_[i][d] + d * (epsilon + (bulk_ / 2)); + dirty = true; + } + } + while (flip (&d) != DOWN); + } + } + while (dirty); +} \ No newline at end of file diff --git a/lily/new-dynamic-engraver.cc b/lily/new-dynamic-engraver.cc index 3554b22e9e..1418e563f5 100644 --- a/lily/new-dynamic-engraver.cc +++ b/lily/new-dynamic-engraver.cc @@ -260,6 +260,7 @@ New_dynamic_engraver::acknowledge_note_column (Grob_info info) if (script_ && !script_->get_parent (X_AXIS)) { extract_grob_set (info.grob (), "note-heads", heads); + Grob *stem = unsmob_grob (info.grob ()->get_object ("stem")); /* Spacing constraints may require dynamics to be aligned on rests, so check for a rest if this note column has no note heads. @@ -271,7 +272,10 @@ New_dynamic_engraver::acknowledge_note_column (Grob_info info) { script_->set_parent (x_parent, X_AXIS); Self_alignment_interface::set_center_parent (script_, X_AXIS); + Self_alignment_interface::avoid_x_collisions (script_); } + if (stem) + Pointer_group_interface::add_grob (script_, ly_symbol2scm ("potential-X-colliding-grobs"), stem); } if (current_spanner_ && !current_spanner_->get_bound (LEFT)) diff --git a/lily/self-alignment-interface.cc b/lily/self-alignment-interface.cc index 0c72152333..a6fde153e0 100644 --- a/lily/self-alignment-interface.cc +++ b/lily/self-alignment-interface.cc @@ -19,8 +19,12 @@ #include "self-alignment-interface.hh" +#include "directional-element-interface.hh" #include "grob.hh" +#include "grob-array.hh" +#include "interval-minefield.hh" #include "paper-column.hh" +#include "pointer-group-interface.hh" #include "warn.hh" MAKE_SCHEME_CALLBACK (Self_alignment_interface, y_aligned_on_self, 1); @@ -134,6 +138,69 @@ Self_alignment_interface::aligned_on_parent (Grob *me, Axis a) return scm_from_double (x); } +MAKE_SCHEME_CALLBACK (Self_alignment_interface, avoid_x_colliding_grobs, 2); +SCM +Self_alignment_interface::avoid_x_colliding_grobs (SCM smob, SCM o) +{ + SCM avoided = avoid_colliding_grobs (unsmob_grob (smob), X_AXIS, robust_scm2double (o, 0.0)); + return scm_is_null (avoided) ? o : avoided; +} + +MAKE_SCHEME_CALLBACK (Self_alignment_interface, x_colliding_grobs, 1); +SCM +Self_alignment_interface::x_colliding_grobs (SCM smob) +{ + Grob *me = unsmob_grob (smob); + extract_grob_set (me, "potential-X-colliding-grobs", pot); + vector act; + Direction d = get_grob_direction (me->get_parent (Y_AXIS)); + for (vsize i = 0; i < pot.size (); i++) + if (d == get_grob_direction (pot[i]) + && to_boolean (pot[i]->get_property ("cross-staff"))) + act.push_back (pot[i]); + + SCM grobs_scm = Grob_array::make_array (); + unsmob_grob_array (grobs_scm)->set_array (act); + + return grobs_scm; +} + +SCM +Self_alignment_interface::avoid_colliding_grobs (Grob *me, Axis a, Real offset) +{ + extract_grob_set (me, a == X_AXIS ? "X-colliding-grobs" : "Y-colliding-grobs", colls); + if (!colls.size ()) + return SCM_EOL; + vector ivs; + + Item *refp = dynamic_cast (common_refpoint_of_array (colls, me, a)); + if (!refp) + return SCM_EOL; + + Interval iv = me->extent (me, a) + offset; + for (vsize i = 0; i < colls.size (); i++) + ivs.push_back (colls[i]->extent (refp, a)); + + Interval_minefield minefield (Interval (iv.center (), iv.center ()), iv.length ()); + for (vsize i = 0; i < ivs.size (); i++) + minefield.add_forbidden_interval (ivs[i]); + minefield.solve (); + Interval pos = minefield.feasible_placements (); + + if (pos[LEFT] == pos[RIGHT]) + return SCM_EOL; + + Direction col_dir = ((abs (pos[LEFT] - iv.center ()) + + robust_scm2double (me->get_property ("collision-bias"), 0.0)) + > abs (pos[RIGHT] - iv.center ())) + ? RIGHT + : LEFT; + + return scm_from_double ((pos[col_dir] - (iv.length () / 2) + + col_dir + * robust_scm2double (me->get_property ("collision-padding"), 0.0))); +} + void Self_alignment_interface::set_center_parent (Grob *me, Axis a) { @@ -142,6 +209,12 @@ Self_alignment_interface::set_center_parent (Grob *me, Axis a) a); } +void +Self_alignment_interface::avoid_x_collisions (Grob *me) +{ + chain_offset_callback (me, avoid_x_colliding_grobs_proc, X_AXIS); +} + void Self_alignment_interface::set_align_self (Grob *me, Axis a) { @@ -165,6 +238,11 @@ ADD_INTERFACE (Self_alignment_interface, "@end table\n", /* properties */ + "collision-bias " + "collision-padding " + "potential-X-colliding-grobs " "self-alignment-X " "self-alignment-Y " + "X-colliding-grobs " + "Y-colliding-grobs " ); diff --git a/scm/define-grob-properties.scm b/scm/define-grob-properties.scm index c26bd3cae1..f209fe649a 100644 --- a/scm/define-grob-properties.scm +++ b/scm/define-grob-properties.scm @@ -174,8 +174,13 @@ hairpins (al/@/del niente).") edges of beams?") (collapse-height ,ly:dimension? "Minimum height of system start delimiter. If equal or smaller, the bracket/@/brace/@/line is removed.") + (collision-bias ,number? "Number determining how much to favor the +left (negative) or right (positive). Larger absolute values in either +direction will push a collision in this direction.") (collision-interfaces ,list? "A list of interfaces for which automatic beam-collision resolution is run.") + (collision-padding ,number? "Amount of padding to apply after +a collision is detected via the self-alignment-interface.") (collision-voice-only ,boolean? "Does automatic beam collsion apply only to the voice in which the beam was created?") (color ,color? "The color of this grob.") @@ -1070,6 +1075,8 @@ pure-from-neighbor-interface to determine various grob heights.") (note-heads ,ly:grob-array? "An array of note head grobs.") (pedal-text ,ly:grob? "A pointer to the text of a mixed-style piano pedal.") + (potential-X-colliding-grobs ,ly:grob-array? "Grobs that can potentially +collide with a self-aligned grob on the X-axis.") (pure-relevant-grobs ,ly:grob-array? "All the grobs (items and spanners) that are relevant for finding the @code{pure-Y-extent}") (pure-relevant-items ,ly:grob-array? "A subset of elements that are @@ -1110,6 +1117,10 @@ results, use @code{LEFT} and @code{RIGHT}.") (tremolo-flag ,ly:grob? "The tremolo object on a stem.") (tuplet-number ,ly:grob? "The number for a bracket.") (tuplets ,ly:grob-array? "An array of smaller tuplet brackets.") + (X-colliding-grobs ,ly:grob-array? "Grobs that can collide +with a self-aligned grob on the X-axis.") + (Y-colliding-grobs ,ly:grob-array? "Grobs that can collide +with a self-aligned grob on the Y-axis.") (X-common ,ly:grob? "Common reference point for axis group.") (Y-common ,ly:grob? "See @code{X-common}.") diff --git a/scm/define-grobs.scm b/scm/define-grobs.scm index 653bc5e227..806f720697 100644 --- a/scm/define-grobs.scm +++ b/scm/define-grobs.scm @@ -766,6 +766,8 @@ ;; todo. + (collision-bias . -2.0) + (collision-padding . 0.5) (direction . ,ly:script-interface::calc-direction) (extra-spacing-width . (+inf.0 . -inf.0)) (font-encoding . fetaText) @@ -780,6 +782,7 @@ (X-offset . ,ly:self-alignment-interface::x-aligned-on-self) (Y-offset . ,ly:self-alignment-interface::y-aligned-on-self) (meta . ((class . Item) + (object-callbacks . ((X-colliding-grobs . ,ly:self-alignment-interface::x-colliding-grobs))) (interfaces . (dynamic-interface dynamic-text-interface font-interface diff --git a/scm/output-lib.scm b/scm/output-lib.scm index 3e2d5ff520..9137d5be17 100644 --- a/scm/output-lib.scm +++ b/scm/output-lib.scm @@ -58,7 +58,6 @@ (ly:text-interface::interpret-markup layout props text))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; beam slope -- 2.39.2