Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Narrow spacing improvements #15174

Merged
merged 6 commits into from Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/ci_vtests.yml
Expand Up @@ -150,14 +150,14 @@ jobs:
with:
name: ${{ needs.setup.outputs.artifact_name }}
path: ./comparison
- name: Generate ref drawdata
run: |
chmod +x ./musescore_reference/app/bin/mscore4portable
xvfb-run ./vtest/gen-ref-data.sh -m ./musescore_reference/app/bin/mscore4portable
- name: DrawData VTest
run: |
chmod +x ./musescore_current/app/bin/mscore4portable
xvfb-run ./vtest/vtest.sh -m ./musescore_current/app/bin/mscore4portable
#- name: Generate ref drawdata
# run: |
# chmod +x ./musescore_reference/app/bin/mscore4portable
# xvfb-run ./vtest/gen-ref-data.sh -m ./musescore_reference/app/bin/mscore4portable
#- name: DrawData VTest
# run: |
# chmod +x ./musescore_current/app/bin/mscore4portable
# xvfb-run ./vtest/vtest.sh -m ./musescore_current/app/bin/mscore4portable
# - name: Skip failure signal if PR is labeled 'vtests'
# if: github.event_name == 'pull_request' && contains( env.VTEST_DIFF_FOUND, 'true') && contains(github.event.pull_request.labels.*.name, 'vtests')
# run: |
Expand Down
106 changes: 105 additions & 1 deletion src/engraving/layout/layoutsystem.cpp
Expand Up @@ -204,7 +204,8 @@ System* LayoutSystem::collectSystem(const LayoutOptions& options, LayoutContext&
// check if lc.curMeasure fits, remove if not
// collect at least one measure and the break
double acceptanceRange = squeezability * system->squeezableSpace();
bool doBreak = (system->measures().size() > 1) && ((curSysWidth + ww) > targetSystemWidth + acceptanceRange);
bool doBreak = (system->measures().size() > 1) && ((curSysWidth + ww) > targetSystemWidth + acceptanceRange)
&& !ctx.prevMeasure->noBreak();
/* acceptanceRange allows some systems to be initially slightly larger than the margins and be
* justified by squeezing instead of stretching. Allows to make much better choices of how many
* measures to fit per system. */
Expand Down Expand Up @@ -428,6 +429,10 @@ System* LayoutSystem::collectSystem(const LayoutOptions& options, LayoutContext&
curSysWidth += m->width() - oldWidth;
}

if (curSysWidth > targetSystemWidth) {
manageNarrowSpacing(system, curSysWidth, targetSystemWidth, minTicks, maxTicks);
}

// JUSTIFY SYSTEM
// Do not justify last system of a section if curSysWidth is < lastSystemFillLimit
if (!((ctx.curMeasure == 0 || (lm && lm->sectionBreak()))
Expand Down Expand Up @@ -1530,3 +1535,102 @@ void LayoutSystem::restoreTies(System* system)
Fraction etick = system->measures().back()->endTick();
doLayoutTies(system, segList, stick, etick);
}

void LayoutSystem::manageNarrowSpacing(System* system, double& curSysWidth, double targetSysWidth, const Fraction minTicks,
const Fraction maxTicks)
{
static constexpr double step = 0.2; // We'll try reducing the spacing in steps of 20%
// (empiric compromise between looking good and not taking too many iterations)
static constexpr double squeezeLimit = 0.3; // For some spaces, do not go below 30%

// First, try to gradually reduce the duration stretch (i.e. flatten the spacing curve)
double stretchCoeff = system->firstMeasure()->layoutStretch() - step;
while (curSysWidth > targetSysWidth && RealIsEqualOrMore(stretchCoeff, 0.0)) {
for (MeasureBase* mb : system->measures()) {
if (!mb->isMeasure()) {
continue;
}
Measure* m = toMeasure(mb);
double prevWidth = m->width();
m->computeWidth(minTicks, maxTicks, stretchCoeff, /*overrideMinMeasureWidth*/ true);
curSysWidth += m->width() - prevWidth;
}
stretchCoeff -= step;
}
if (curSysWidth < targetSysWidth) {
// Success!
return;
}

// Now we are limited by the collision checks, so try to gradually squeeze everything without collisions
staff_idx_t nstaves = system->score()->nstaves();
double squeezeFactor = 1 - step;
while (curSysWidth > targetSysWidth && RealIsEqualOrMore(squeezeFactor, 0.0)) {
for (MeasureBase* mb : system->measures()) {
if (!mb->isMeasure()) {
continue;
}

// Reduce all paddings
Measure* m = toMeasure(mb);
double prevWidth = m->width();
for (Segment& segment : m->segments()) {
for (staff_idx_t staffIdx = 0; staffIdx < nstaves; ++staffIdx) {
Shape& shape = segment.staffShape(staffIdx);
shape.setSqueezeFactor(squeezeFactor);
}
}
m->computeWidth(minTicks, maxTicks, stretchCoeff, /*overrideMinMeasureWidth*/ true);

// Reduce other distances that don't depend on paddings
Segment* first = m->firstEnabled();
double currentFirstX = first->x();
if (currentFirstX > 0 && !first->hasAccidentals()) {
first->setPosX(currentFirstX * std::max(squeezeFactor, squeezeLimit));
}
for (Segment& segment : m->segments()) {
if (!segment.header() && !segment.isTimeSigType()) {
continue;
}
Segment* nextSeg = segment.next();
if (!nextSeg || !nextSeg->isChordRestType()) {
continue;
}
double margin = segment.width() - segment.minHorizontalCollidingDistance(nextSeg);
double reducedMargin = margin * (1 - std::max(squeezeFactor, squeezeLimit));
segment.setWidth(segment.width() - reducedMargin);
}
m->respaceSegments();
curSysWidth += m->width() - prevWidth;
}
squeezeFactor -= step;
}
if (curSysWidth < targetSysWidth) {
// Success!
return;
}

// Things don't fit without collisions, so give up and allow collisions
double smallerStep = 0.25 * step;
double widthReduction = 1 - smallerStep;
while (curSysWidth > targetSysWidth && RealIsEqualOrMore(widthReduction, 0.0)) {
for (MeasureBase* mb : system->measures()) {
if (!mb->isMeasure()) {
continue;
}

Measure* m = toMeasure(mb);
double prevWidth = m->width();
for (Segment& segment : m->segments()) {
if (!segment.isChordRestType()) {
continue;
}
double curSegmentWidth = segment.width();
segment.setWidth(curSegmentWidth * widthReduction);
}
m->respaceSegments();
curSysWidth += m->width() - prevWidth;
}
widthReduction *= 1 - smallerStep;
}
}
2 changes: 2 additions & 0 deletions src/engraving/layout/layoutsystem.h
Expand Up @@ -49,6 +49,8 @@ class LayoutSystem
static void justifySystem(System* system, double curSysWidth, double targetSystemWidth);
static void updateCrossBeams(System* system, const LayoutContext& ctx);
static void restoreTies(System* system);
static void manageNarrowSpacing(System* system, double& curSysWidth, double targetSysWidth, const Fraction minTicks,
const Fraction maxTicks);
};
}

Expand Down
81 changes: 62 additions & 19 deletions src/engraving/libmscore/measure.cpp
Expand Up @@ -4154,7 +4154,8 @@ void Measure::checkTrailer()
// to compute the minimum non-collision distance between elements.
//---------------------------------------------------------

void Measure::computeWidth(Segment* s, double x, bool isSystemHeader, Fraction minTicks, Fraction maxTicks, double stretchCoeff)
void Measure::computeWidth(Segment* s, double x, bool isSystemHeader, Fraction minTicks, Fraction maxTicks, double stretchCoeff,
bool overrideMinMeasureWidth)
{
Segment* fs = firstEnabled();
if (!fs->visible()) { // first enabled could be a clef change on invisible staff
Expand All @@ -4168,6 +4169,7 @@ void Measure::computeWidth(Segment* s, double x, bool isSystemHeader, Fraction m
double usrStretch = std::max(userStretch(), double(0.1)); // Avoids stretch going to zero
usrStretch = std::min(usrStretch, double(10)); // Higher values may cause the spacing to break (10 is already ridiculously high and no user should even use that)

// PASS 1: compute the spacing of all left-aligned segments by stacking them one after the other
while (s) {
s->setWidthOffset(0.0);
s->setPosX(x);
Expand All @@ -4176,14 +4178,14 @@ void Measure::computeWidth(Segment* s, double x, bool isSystemHeader, Fraction m
// skipped in computeMinWidth() -- the only way this would be an issue here is
// if this method was called specifically with the invisible segment specified
// which I'm pretty sure doesn't happen at this point. still...
if (!s->enabled() || !s->visible() || s->allElementsInvisible()) {
if (!s->enabled() || !s->visible() || s->allElementsInvisible() || s->isRightAligned()) {
s->setWidth(0);
s = s->next();
continue;
}

Segment* ns = s->nextActive();
while (ns && ns->allElementsInvisible()) {
while (ns && (ns->allElementsInvisible() || ns->isRightAligned())) {
ns = ns->nextActive();
}
// end barline might be disabled
Expand Down Expand Up @@ -4213,14 +4215,6 @@ void Measure::computeWidth(Segment* s, double x, bool isSystemHeader, Fraction m
w = std::max(w, minStretchedWidth);
}
}
// Clefs (or breaths) are justified right-to-left. It is the clef (or breath) that needs to move left
//(if there's space), not the following segment that needs to move right.
if ((ns->isClefType() || ns->isBreathType()) && ns->next()) {
double reduction = std::max(ns->minHorizontalCollidingDistance(ns->next()),
double(score()->styleMM(Sid::clefKeyRightMargin)));
w -= reduction;
s->setWidthOffset(s->widthOffset() - reduction);
}

// Adjust spacing for cross-beam situations
s->computeCrossBeamType(ns);
Expand Down Expand Up @@ -4250,14 +4244,15 @@ void Measure::computeWidth(Segment* s, double x, bool isSystemHeader, Fraction m

int n = 1;
for (Segment* ps = s; ps; ps=ps->prevActive()) {
double ww;

assert(ps); // ps should never be nullptr but better be safe.
assert(ps); // ps should never be nullptr but better be safe.
if (!ps) {
break;
}
if (ps->isRightAligned()) {
continue;
}

ww = ps->minHorizontalCollidingDistance(ns) - (s->x() - ps->x());
double ww = ps->minHorizontalCollidingDistance(ns) - (s->x() - ps->x());
if (ps == fs) {
ww = std::max(ww, ns->minLeft(ls) - s->x());
}
Expand Down Expand Up @@ -4305,17 +4300,21 @@ void Measure::computeWidth(Segment* s, double x, bool isSystemHeader, Fraction m
_squeezableSpace = std::max(0.0, std::min(_squeezableSpace, x - score()->styleMM(Sid::minMeasureWidth)));
setLayoutStretch(stretchCoeff);
setWidth(x);

// PASS 2: now put in the right-aligned segments
spaceRightAlignedSegments();

// Check against minimum width and increase if needed (MMRest minWidth is guaranteed elsewhere)
double minWidth = computeMinMeasureWidth();
if (width() < minWidth) {
if (width() < minWidth && !overrideMinMeasureWidth) {
stretchToTargetWidth(minWidth);
setWidthLocked(true);
} else {
setWidthLocked(false);
}
}

void Measure::computeWidth(Fraction minTicks, Fraction maxTicks, double stretchCoeff)
void Measure::computeWidth(Fraction minTicks, Fraction maxTicks, double stretchCoeff, bool overrideMinMeasureWidth)
{
Segment* s;

Expand Down Expand Up @@ -4356,7 +4355,7 @@ void Measure::computeWidth(Fraction minTicks, Fraction maxTicks, double stretchC
bool isSystemHeader = s->header();

_squeezableSpace = 0;
computeWidth(s, x, isSystemHeader, minTicks, maxTicks, stretchCoeff);
computeWidth(s, x, isSystemHeader, minTicks, maxTicks, stretchCoeff, overrideMinMeasureWidth);
}

double Measure::computeMinMeasureWidth() const
Expand Down Expand Up @@ -4392,6 +4391,50 @@ double Measure::computeMinMeasureWidth() const
return minWidth;
}

void Measure::spaceRightAlignedSegments()
{
// Collect all the right-aligned segments starting from the back
std::vector<Segment*> rightAlignedSegments;
for (Segment* segment = m_segments.last(); segment; segment = segment->prev()) {
if (segment->enabled() && segment->isRightAligned()) {
rightAlignedSegments.push_back(segment);
}
}
// Compute spacing
static constexpr double arbitraryLowReal = -10000.0;
for (Segment* raSegment : rightAlignedSegments) {
// 1) right-align the segment against the following ones
double minDistAfter = arbitraryLowReal;
for (Segment* seg = raSegment->nextActive(); seg; seg = seg->nextActive()) {
double xDiff = seg->x() - raSegment->x();
double minDist = raSegment->minHorizontalCollidingDistance(seg);
minDistAfter = std::max(minDistAfter, minDist - xDiff);
}
if (minDistAfter != arbitraryLowReal && raSegment->prevActive()) {
Segment* prevSegment = raSegment->prev();
prevSegment->setWidth(prevSegment->width() - minDistAfter);
prevSegment->setWidthOffset(prevSegment->widthOffset() - minDistAfter);
raSegment->movePosX(-minDistAfter);
raSegment->setWidth(raSegment->width() + minDistAfter);
}
// 2) Make sure the segment isn't colliding with anything behind
double minDistBefore = 0.0;
for (Segment* seg = raSegment->prevActive(); seg; seg = seg->prevActive()) {
double xDiff = raSegment->x() - seg->x();
double minDist = seg->minHorizontalCollidingDistance(raSegment);
minDistBefore = std::max(minDistBefore, minDist - xDiff);
}
Segment* prevSegment = raSegment->prevActive();
if (prevSegment) {
prevSegment->setWidth(prevSegment->width() + minDistBefore);
}
for (Segment* seg = raSegment; seg; seg = seg->next()) {
seg->movePosX(minDistBefore);
}
setWidth(width() + minDistBefore);
}
}

void Measure::stretchToTargetWidth(double targetWidth)
{
if (targetWidth < width()) {
Expand Down Expand Up @@ -4422,7 +4465,7 @@ double Measure::computeFirstSegmentXPosition(Segment* segment)
Shape ls(RectF(0.0, 0.0, 0.0, spatium() * 4));

// First, try to compute first segment x-position by padding against end barline of previous measure
Measure* prevMeas = (prev() && prev()->isMeasure()) ? toMeasure(prev()) : nullptr;
Measure* prevMeas = (prev() && prev()->isMeasure() && prev()->system() == system()) ? toMeasure(prev()) : nullptr;
Segment* prevMeasEnd = prevMeas ? prevMeas->last() : nullptr;
if (prevMeasEnd && prevMeasEnd->isEndBarLineType()
&& !(segment->isBeginBarLineType() || segment->isStartRepeatBarLineType() || segment->isBarLineType())) {
Expand Down
6 changes: 4 additions & 2 deletions src/engraving/libmscore/measure.h
Expand Up @@ -347,7 +347,7 @@ class Measure final : public MeasureBase
void triggerLayout() const override;
double basicStretch() const;
double basicWidth() const;
void computeWidth(Fraction minTicks, Fraction maxTicks, double stretchCoeff);
void computeWidth(Fraction minTicks, Fraction maxTicks, double stretchCoeff, bool overrideMinMeasureWidth = false);
void stretchToTargetWidth(double targetWidth);
void checkHeader();
void checkTrailer();
Expand Down Expand Up @@ -386,8 +386,10 @@ class Measure final : public MeasureBase
void push_front(Segment* e);

void fillGap(const Fraction& pos, const Fraction& len, track_idx_t track, const Fraction& stretch, bool useGapRests = true);
void computeWidth(Segment* s, double x, bool isSystemHeader, Fraction minTicks, Fraction maxTicks, double stretchCoeff);
void computeWidth(Segment* s, double x, bool isSystemHeader, Fraction minTicks, Fraction maxTicks, double stretchCoeff,
bool overrideMinMeasureWidth = false);
double computeMinMeasureWidth() const;
void spaceRightAlignedSegments();

MStaff* mstaff(staff_idx_t staffIndex) const;

Expand Down
7 changes: 2 additions & 5 deletions src/engraving/libmscore/segment.cpp
Expand Up @@ -2258,6 +2258,7 @@ void Segment::createShapes()
void Segment::createShape(staff_idx_t staffIdx)
{
Shape& s = _shapes[staffIdx];
s.setSqueezeFactor(1);
s.clear();

if (const System* system = this->system()) {
Expand Down Expand Up @@ -2300,7 +2301,7 @@ void Segment::createShape(staff_idx_t staffIdx)
continue;
}
if (e->addToSkyline()) {
s.add(e->shape().translate(e->pos()));
s.add(e->shape().translate(e->isClef() ? e->ipos() : e->pos()));
}
}
}
Expand Down Expand Up @@ -2625,10 +2626,6 @@ double Segment::minHorizontalDistance(Segment* ns, bool systemHeaderGap) const
}
double w = std::max(ww, 0.0); // non-negative

if (isClefType() && ns && ns->isChordRestType()) {
w = std::max(w, double(score()->styleMM(Sid::clefKeyRightMargin)));
}

// Header exceptions that need additional space (more than the padding)
double absoluteMinHeaderDist = 1.5 * spatium();
if (systemHeaderGap) {
Expand Down
1 change: 1 addition & 0 deletions src/engraving/libmscore/segment.h
Expand Up @@ -318,6 +318,7 @@ class Segment final : public EngravingItem
bool isEndBarLineType() const { return _segmentType == SegmentType::EndBarLine; }
bool isKeySigAnnounceType() const { return _segmentType == SegmentType::KeySigAnnounce; }
bool isTimeSigAnnounceType() const { return _segmentType == SegmentType::TimeSigAnnounce; }
bool isRightAligned() const { return isClefType() || isBreathType(); }

Fraction shortestChordRest() const;
void computeCrossBeamType(Segment* nextSeg);
Expand Down
5 changes: 4 additions & 1 deletion src/engraving/libmscore/shape.cpp
Expand Up @@ -101,7 +101,8 @@ Shape Shape::translated(const PointF& pt) const
double Shape::minHorizontalDistance(const Shape& a) const
{
double dist = -1000000.0; // min real
double verticalClearance = 0.2 * _spatium;
double absoluteMinPadding = 0.1 * _spatium * _squeezeFactor;
double verticalClearance = 0.2 * _spatium * _squeezeFactor;
for (const ShapeElement& r2 : a) {
const EngravingItem* item2 = r2.toItem;
double by1 = r2.top();
Expand All @@ -115,6 +116,8 @@ double Shape::minHorizontalDistance(const Shape& a) const
KerningType kerningType = KerningType::NON_KERNING;
if (item1 && item2) {
padding = item1->computePadding(item2);
padding *= _squeezeFactor;
padding = std::max(padding, absoluteMinPadding);
kerningType = item1->computeKerningType(item2);
}
if ((intersection && kerningType != KerningType::ALLOW_COLLISION)
Expand Down