MuseScore/src/engraving/rendering/dev/slurtielayout.cpp

1787 lines
69 KiB
C++

/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2023 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "slurtielayout.h"
#include "iengravingfont.h"
#include "compat/dummyelement.h"
#include "dom/slur.h"
#include "dom/chord.h"
#include "dom/system.h"
#include "dom/staff.h"
#include "dom/stafftype.h"
#include "dom/ledgerline.h"
#include "dom/note.h"
#include "dom/hook.h"
#include "dom/stem.h"
#include "dom/tremolo.h"
#include "dom/fretcircle.h"
#include "dom/tie.h"
#include "dom/engravingitem.h"
#include "dom/measure.h"
#include "dom/guitarbend.h"
#include "tlayout.h"
#include "chordlayout.h"
#include "tremololayout.h"
#include "../engraving/types/symnames.h"
using namespace mu::engraving;
using namespace mu::engraving::rendering::dev;
void SlurTieLayout::layout(Slur* item, LayoutContext& ctx)
{
if (item->track2() == mu::nidx) {
item->setTrack2(item->track());
}
double _spatium = item->spatium();
if (ctx.conf().isPaletteMode() || item->tick() == Fraction(-1, 1)) {
//
// when used in a palette, slur has no parent and
// tick and tick2 has no meaning so no layout is
// possible and needed
//
SlurSegment* s;
if (item->spannerSegments().empty()) {
s = new SlurSegment(ctx.mutDom().dummyParent()->system());
s->setTrack(item->track());
item->add(s);
} else {
s = item->frontSegment();
}
s->setSpannerSegmentType(SpannerSegmentType::SINGLE);
layoutSegment(s, ctx, PointF(0, 0), PointF(_spatium * 6, 0));
item->setbbox(item->frontSegment()->ldata()->bbox());
return;
}
if (item->startCR() == 0 || item->startCR()->measure() == 0) {
LOGD("track %zu-%zu %p - %p tick %d-%d null start anchor",
item->track(), item->track2(), item->startCR(), item->endCR(), item->tick().ticks(), item->tick2().ticks());
return;
}
if (item->endCR() == 0) { // sanity check
LOGD("no end CR for %d", (item->tick() + item->ticks()).ticks());
item->setEndElement(item->startCR());
item->setTick2(item->tick());
}
switch (item->slurDirection()) {
case DirectionV::UP:
item->setUp(true);
break;
case DirectionV::DOWN:
item->setUp(false);
break;
case DirectionV::AUTO:
{
//
// assumption:
// slurs have only chords or rests as start/end elements
//
if (item->startCR() == 0 || item->endCR() == 0) {
item->setUp(true);
break;
}
Measure* m1 = item->startCR()->measure();
Chord* c1 = item->startCR()->isChord() ? toChord(item->startCR()) : 0;
Chord* c2 = item->endCR()->isChord() ? toChord(item->endCR()) : 0;
item->setUp(!(item->startCR()->up()));
if ((item->endCR()->tick() - item->startCR()->tick()) > m1->ticks()) {
// long slurs are always above
item->setUp(true);
} else {
item->setUp(!(item->startCR()->up()));
}
if (c1 && c2 && Slur::isDirectionMixture(c1, c2) && (c1->noteType() == NoteType::NORMAL)) {
// slurs go above if start and end note have different stem directions,
// but grace notes are exceptions
item->setUp(true);
} else if (m1->hasVoices(item->startCR()->staffIdx(), item->tick(), item->ticks()) && c1 && c1->noteType() == NoteType::NORMAL) {
// in polyphonic passage, slurs go on the stem side
item->setUp(item->startCR()->up());
}
}
break;
}
SlurTiePos sPos;
slurPos(item, &sPos, ctx);
const std::vector<System*>& sl = ctx.dom().systems();
ciSystem is = sl.begin();
while (is != sl.end()) {
if (*is == sPos.system1) {
break;
}
++is;
}
if (is == sl.end()) {
LOGD("Slur::layout first system not found");
}
item->setPos(0, 0);
//---------------------------------------------------------
// count number of segments, if no change, all
// user offsets (drags) are retained
//---------------------------------------------------------
unsigned nsegs = 1;
for (ciSystem iis = is; iis != sl.end(); ++iis) {
if ((*iis)->vbox()) {
continue;
}
if (*iis == sPos.system2) {
break;
}
++nsegs;
}
item->fixupSegments(nsegs);
for (int i = 0; is != sl.end(); ++i, ++is) {
System* system = *is;
if (system->vbox()) {
--i;
continue;
}
SlurSegment* segment = item->segmentAt(i);
segment->setSystem(system);
// case 1: one segment
if (sPos.system1 == sPos.system2) {
segment->setSpannerSegmentType(SpannerSegmentType::SINGLE);
layoutSegment(segment, ctx, sPos.p1, sPos.p2);
}
// case 2: start segment
else if (i == 0) {
segment->setSpannerSegmentType(SpannerSegmentType::BEGIN);
double x = system->ldata()->bbox().width();
layoutSegment(segment, ctx, sPos.p1, PointF(x, sPos.p1.y()));
}
// case 3: middle segment
else if (i != 0 && system != sPos.system2) {
segment->setSpannerSegmentType(SpannerSegmentType::MIDDLE);
double x1 = system->firstNoteRestSegmentX(true);
double x2 = system->ldata()->bbox().width();
double y = item->staffIdx() > system->staves().size() ? system->y() : system->staff(item->staffIdx())->y();
layoutSegment(segment, ctx, PointF(x1, y), PointF(x2, y));
}
// case 4: end segment
else {
segment->setSpannerSegmentType(SpannerSegmentType::END);
double x = system->firstNoteRestSegmentX(true);
layoutSegment(segment, ctx, PointF(x, sPos.p2.y()), sPos.p2);
}
if (system == sPos.system2) {
break;
}
}
item->setbbox(item->spannerSegments().empty() ? RectF() : item->frontSegment()->ldata()->bbox());
}
SpannerSegment* SlurTieLayout::layoutSystem(Slur* item, System* system, LayoutContext& ctx)
{
const double horizontalTieClearance = 0.35 * item->spatium();
const double tieClearance = 0.65 * item->spatium();
const double continuedSlurOffsetY = item->spatium() * .4;
const double continuedSlurMaxDiff = 2.5 * item->spatium();
Fraction stick = system->firstMeasure()->tick();
Fraction etick = system->lastMeasure()->endTick();
SlurSegment* slurSegment = toSlurSegment(TLayout::getNextLayoutSystemSegment(item, system, [](System* parent) {
return new SlurSegment(parent);
}));
SpannerSegmentType sst;
if (item->tick() >= stick) {
//
// this is the first call to layoutSystem,
// processing the first line segment
//
if (item->track2() == mu::nidx) {
item->setTrack2(item->track());
}
if (item->startCR() == 0 || item->startCR()->measure() == 0) {
LOGD("Slur::layout(): track %zu-%zu %p - %p tick %d-%d null start anchor",
item->track(), item->track2(), item->startCR(), item->endCR(), item->tick().ticks(), item->tick2().ticks());
return slurSegment;
}
if (item->endCR() == 0) { // sanity check
item->setEndElement(item->startCR());
item->setTick2(item->tick());
}
computeUp(item, ctx);
if (item->sourceStemArrangement() != -1) {
if (item->sourceStemArrangement() != item->calcStemArrangement(item->startCR(), item->endCR())) {
// copy & paste from incompatible stem arrangement, so reset bezier points
for (int g = 0; g < (int)Grip::GRIPS; ++g) {
slurSegment->ups((Grip)g) = UP();
}
}
}
sst = item->tick2() < etick ? SpannerSegmentType::SINGLE : SpannerSegmentType::BEGIN;
} else if (item->tick() < stick && item->tick2() >= etick) {
sst = SpannerSegmentType::MIDDLE;
} else {
sst = SpannerSegmentType::END;
}
slurSegment->setSpannerSegmentType(sst);
SlurTiePos sPos;
slurPos(item, &sPos, ctx);
PointF p1, p2;
// adjust for ties
p1 = sPos.p1;
p2 = sPos.p2;
bool constrainLeftAnchor = false;
// start anchor, either on the start chordrest or at the beginning of the system
if (sst == SpannerSegmentType::SINGLE || sst == SpannerSegmentType::BEGIN) {
Chord* sc = item->startCR()->isChord() ? toChord(item->startCR()) : nullptr;
// on chord
if (sc) {
Tie* tie = (item->up() ? sc->upNote() : sc->downNote())->tieFor();
PointF endPoint = PointF();
if (tie && (tie->isInside() || tie->up() != item->up())) {
// there is a tie that starts on this chordrest
tie = nullptr;
}
if (tie && !tie->segmentsEmpty()) {
endPoint = tie->segmentAt(0)->ups(Grip::START).pos();
}
bool adjustedVertically = false;
if (tie) {
if (item->up() && tie->up()) {
if (endPoint.y() - p1.y() < tieClearance) {
p1.ry() = endPoint.y() - tieClearance;
adjustedVertically = true;
}
} else if (!item->up() && !tie->up()) {
if (p1.y() - endPoint.y() < tieClearance) {
p1.ry() = endPoint.y() + tieClearance;
adjustedVertically = true;
}
}
}
if (!adjustedVertically && sc->notes()[0]->tieBack() && !sc->notes()[0]->tieBack()->isInside()
&& sc->notes()[0]->tieBack()->up() == item->up()) {
// there is a tie that ends on this chordrest
tie = sc->notes()[0]->tieBack();
if (!tie->segmentsEmpty()) {
endPoint = tie->segmentAt(static_cast<int>(tie->nsegments()) - 1)->ups(Grip::END).pos();
if (abs(endPoint.y() - p1.y()) < tieClearance) {
p1.rx() += horizontalTieClearance;
}
}
}
}
} else if (sst == SpannerSegmentType::END || sst == SpannerSegmentType::MIDDLE) {
// beginning of system
ChordRest* firstCr = system->firstChordRest(item->track());
double y = p1.y();
if (firstCr && firstCr == item->endCR()) {
constrainLeftAnchor = true;
}
if (firstCr && firstCr->isChord()) {
Chord* chord = toChord(firstCr);
if (chord) {
// if both up or both down, deal with avoiding stems and beams
Note* upNote = chord->upNote();
Note* downNote = chord->downNote();
// account for only the stem length that is above the top note (or below the bottom note)
double stemLength = chord->stem() ? chord->stem()->length() - (downNote->pos().y() - upNote->pos().y()) : 0.0;
if (item->up()) {
y = chord->upNote()->pos().y() - (chord->upNote()->height() / 2);
if (chord->up() && chord->stem() && firstCr != item->endCR()) {
y -= stemLength;
}
} else {
y = chord->downNote()->pos().y() + (chord->downNote()->height() / 2);
if (!chord->up() && chord->stem() && firstCr != item->endCR()) {
y += stemLength;
}
}
y += continuedSlurOffsetY * (item->up() ? -1 : 1);
}
}
p1 = PointF(system->firstNoteRestSegmentX(true), y);
// adjust for ties at the end of the system
ChordRest* cr = system->firstChordRest(item->track());
if (cr && cr->isChord() && cr->tick() >= stick && cr->tick() <= etick) {
// TODO: can ties go to or from rests?
Chord* c = toChord(cr);
Tie* tie = nullptr;
PointF endPoint;
Tie* tieBack = c->notes()[0]->tieBack();
if (tieBack && !tieBack->isInside() && tieBack->up() == item->up()) {
// there is a tie that ends on this chordrest
if (!tieBack->segmentsEmpty()) { //Checks for spanner segment esxists
tie = tieBack;
endPoint = tie->backSegment()->ups(Grip::START).pos();
}
}
if (tie) {
if (item->up() && tie->up()) {
if (endPoint.y() - p1.y() < tieClearance) {
p1.ry() = endPoint.y() - tieClearance;
}
} else if (!item->up() && !tie->up()) {
if (p1.y() - endPoint.y() < tieClearance) {
p1.ry() = endPoint.y() + tieClearance;
}
}
}
}
}
// end anchor
if (sst == SpannerSegmentType::SINGLE || sst == SpannerSegmentType::END) {
Chord* ec = item->endCR()->isChord() ? toChord(item->endCR()) : nullptr;
// on chord
if (ec) {
Tie* tie = (item->up() ? ec->upNote() : ec->downNote())->tieBack();
PointF endPoint;
if (tie && (tie->isInside() || tie->up() != item->up())) {
tie = nullptr;
}
bool adjustedVertically = false;
if (tie && !tie->segmentsEmpty()) {
endPoint = tie->segmentAt(0)->ups(Grip::END).pos();
if (item->up() && tie->up()) {
if (endPoint.y() - p2.y() < tieClearance) {
p2.ry() = endPoint.y() - tieClearance;
adjustedVertically = true;
}
} else if (!item->up() && !tie->up()) {
if (p2.y() - endPoint.y() < tieClearance) {
p2.ry() = endPoint.y() + tieClearance;
adjustedVertically = true;
}
}
}
Tie* tieFor = ec->notes()[0]->tieFor();
if (!adjustedVertically && tieFor && !tieFor->isInside() && tieFor->up() == item->up()) {
// there is a tie that starts on this chordrest
if (!tieFor->segmentsEmpty() && std::abs(tieFor->frontSegment()->ups(Grip::START).pos().y() - p2.y()) < tieClearance) {
p2.rx() -= horizontalTieClearance;
}
}
}
} else {
// at end of system
ChordRest* lastCr = system->lastChordRest(item->track());
double y = p1.y();
if (lastCr && lastCr == item->startCR()) {
y += 0.25 * item->spatium() * (item->up() ? -1 : 1);
} else if (lastCr && lastCr->isChord()) {
Chord* chord = toChord(lastCr);
if (chord) {
Note* upNote = chord->upNote();
Note* downNote = chord->downNote();
// account for only the stem length that is above the top note (or below the bottom note)
double stemLength = chord->stem() ? chord->stem()->length() - (downNote->pos().y() - upNote->pos().y()) : 0.0;
if (item->up()) {
y = chord->upNote()->pos().y() - (chord->upNote()->height() / 2);
if (chord->up() && chord->stem()) {
y -= stemLength;
}
} else {
y = chord->downNote()->pos().y() + (chord->downNote()->height() / 2);
if (!chord->up() && chord->stem()) {
y += stemLength;
}
}
y += continuedSlurOffsetY * (item->up() ? -1 : 1);
}
double diff = item->up() ? y - p1.y() : p1.y() - y;
if (diff > continuedSlurMaxDiff) {
y = p1.y() + (y > p1.y() ? continuedSlurMaxDiff : -continuedSlurMaxDiff);
}
}
p2 = PointF(system->endingXForOpenEndedLines(), y);
// adjust for ties at the end of the system
ChordRest* cr = system->lastChordRest(item->track());
if (cr && cr->isChord() && cr->tick() >= stick && cr->tick() <= etick) {
// TODO: can ties go to or from rests?
Chord* c = toChord(cr);
Tie* tie = nullptr;
PointF endPoint;
Tie* tieFor = c->notes()[0]->tieFor();
if (tieFor && !tieFor->isInside() && tieFor->up() == item->up()) {
// there is a tie that starts on this chordrest
if (!tieFor->segmentsEmpty()) { //Checks is spanner segment exists
tie = tieFor;
endPoint = tie->segmentAt(0)->ups(Grip::END).pos();
}
}
if (tie) {
if (item->up() && tie->up()) {
if (endPoint.y() - p2.y() < tieClearance) {
p2.ry() = endPoint.y() - tieClearance;
}
} else if (!item->up() && !tie->up()) {
if (p2.y() - endPoint.y() < tieClearance) {
p2.ry() = endPoint.y() + tieClearance;
}
}
}
}
}
if (constrainLeftAnchor) {
p1.ry() = p2.y() + (0.25 * item->spatium() * (item->up() ? -1 : 1));
}
layoutSegment(slurSegment, ctx, p1, p2);
return slurSegment;
}
//---------------------------------------------------------
// slurPos
// calculate position of start- and endpoint of slur
// relative to System() position
//---------------------------------------------------------
void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx)
{
item->stemFloated().reset();
double _spatium = (item->staffType() ? item->staffType()->lineDistance().val() : 1.0) * item->spatium();
const double stemSideInset = 0.5;
const double stemOffsetX = 0.35;
const double beamClearance = 0.35;
const double beamAnchorInset = 0.15;
const double straightStemXOffset = 0.5; // how far down a straight stem a slur attaches (percent)
const double minOffset = 0.2;
// hack alert!! -- fakeCutout
// The fakeCutout const describes the slope of a line from the top of the stem to the full width of the hook.
// this is necessary because hooks don't have SMuFL cutouts
// Gonville and MuseJazz have really weirdly-shaped hooks compared to Leland and Bravura and Emmentaler,
// so we need to adjust the slope of our hook-avoidance line. this will be unnecessary when hooks have
// SMuFL anchors
bool bulkyHook = ctx.engravingFont()->family() == "Gonville" || ctx.engravingFont()->family() == "MuseJazz";
const double fakeCutoutSlope = bulkyHook ? 1.5 : 1.0;
if (item->endCR() == 0) {
sp->p1 = item->startCR()->pagePos();
sp->p1.rx() += item->startCR()->width();
sp->p2 = sp->p1;
sp->p2.rx() += 5 * _spatium;
sp->system1 = item->startCR()->measure()->system();
sp->system2 = sp->system1;
return;
}
bool useTablature = item->staff() && item->staff()->isTabStaff(item->endCR()->tick());
bool staffHasStems = true; // assume staff uses stems
const StaffType* stt = 0;
if (useTablature) {
stt = item->staff()->staffType(item->tick());
staffHasStems = stt->stemThrough(); // if tab with stems beside, stems do not count for slur pos
}
// start and end cr, chord, and note
ChordRest* scr = item->startCR();
ChordRest* ecr = item->endCR();
Chord* sc = 0;
Note* note1 = 0;
if (scr->isChord()) {
sc = toChord(scr);
note1 = item->up() ? sc->upNote() : sc->downNote();
}
Chord* ec = 0;
Note* note2 = 0;
if (ecr->isChord()) {
ec = toChord(ecr);
note2 = item->up() ? ec->upNote() : ec->downNote();
}
sp->system1 = scr->measure()->system();
sp->system2 = ecr->measure()->system();
if (sp->system1 == 0) {
LOGD("no system1");
return;
}
sp->p1 = scr->pos() + scr->segment()->pos() + scr->measure()->pos();
sp->p2 = ecr->pos() + ecr->segment()->pos() + ecr->measure()->pos();
// adjust for cross-staff
if (scr->vStaffIdx() != item->vStaffIdx() && sp->system1) {
double diff = sp->system1->staff(scr->vStaffIdx())->y() - sp->system1->staff(item->vStaffIdx())->y();
sp->p1.ry() += diff;
}
if (ecr->vStaffIdx() != item->vStaffIdx() && sp->system2) {
double diff = sp->system2->staff(ecr->vStaffIdx())->y() - sp->system2->staff(item->vStaffIdx())->y();
sp->p2.ry() += diff;
}
// account for centering or other adjustments (other than mirroring)
if (note1 && !note1->ldata()->mirror.value()) {
sp->p1.rx() += note1->x();
}
if (note2 && !note2->ldata()->mirror.value()) {
sp->p2.rx() += note2->x();
}
PointF po = PointF();
Stem* stem1 = sc && staffHasStems ? sc->stem() : 0;
Stem* stem2 = ec && staffHasStems ? ec->stem() : 0;
enum class SlurAnchor : char {
NONE, STEM
};
SlurAnchor sa1 = SlurAnchor::NONE;
SlurAnchor sa2 = SlurAnchor::NONE;
if (staffHasStems) {
if (sc && sc->hook() && sc->up() == item->up()) {
sa1 = SlurAnchor::STEM;
}
if (scr->up() == ecr->up() && scr->up() == item->up()) {
if (stem1 && !item->stemSideStartForBeam()) {
sa1 = SlurAnchor::STEM;
}
if (stem2 && !item->stemSideEndForBeam()) {
sa2 = SlurAnchor::STEM;
}
} else if (ecr->segment()->system() != scr->segment()->system()) {
// in the case of continued slurs, we anchor to stem when necessary
if (scr->up() == item->up() && stem1 && !scr->beam()) {
sa1 = SlurAnchor::STEM;
}
if (ecr->up() == item->up() && stem2 && !ecr->beam()) {
sa2 = SlurAnchor::STEM;
}
}
}
double __up = item->up() ? -1.0 : 1.0;
double hw1 = note1 ? note1->tabHeadWidth(stt) : scr->width() * scr->mag(); // if stt == 0, tabHeadWidth()
double hw2 = note2 ? note2->tabHeadWidth(stt) : ecr->width() * ecr->mag(); // defaults to headWidth()
PointF pt;
switch (sa1) {
case SlurAnchor::STEM: //sc can't be null
{
// place slur starting point at stem end point
pt = sc->stemPos() - sc->pagePos() + sc->stem()->ldata()->line.p2();
if (useTablature) { // in tabs, stems are centred on note:
pt.rx() = hw1 * 0.5 + (note1 ? note1->bboxXShift() : 0.0); // skip half notehead to touch stem, anatoly-os: incorrect. half notehead width is not always the stem position
}
// clear the stem (x)
// allow slight overlap (y)
// don't allow overlap with hook if not disabling the autoplace checks against start/end segments in SlurSegment::layoutSegment()
double yadj = -stemSideInset* sc->intrinsicMag();
yadj *= _spatium * __up;
double offset = std::max(stemOffsetX * sc->intrinsicMag(), minOffset);
pt += PointF(offset * _spatium, yadj);
// account for articulations
fixArticulations(item, pt, sc, __up, true);
// adjust for hook
double fakeCutout = 0.0;
if (!ctx.conf().styleB(Sid::useStraightNoteFlags)) {
Hook* hook = sc->hook();
// regular flags
if (hook && hook->ldata()->bbox().translated(hook->pos()).contains(pt)) {
// TODO: in the utopian far future where all hooks have SMuFL cutouts, this fakeCutout business will no
// longer be used. for the time being fakeCutout describes a point on the line y=mx+b, out from the top of the stem
// where y = yadj, m = fakeCutoutSlope, and x = y/m + fakeCutout
fakeCutout = std::min(0.0, std::abs(yadj) - (hook->width() / fakeCutoutSlope));
pt.rx() = sc->stemPosX() - fakeCutout;
}
} else {
Hook* hook = sc->hook();
// straight flags
if (hook && hook->ldata()->bbox().translated(hook->pos()).contains(pt)) {
double hookWidth = hook->width() * hook->mag();
pt.rx() = (hookWidth * straightStemXOffset) + (hook->pos().x() + sc->x());
if (item->up()) {
pt.ry() = sc->downNote()->pos().y() - stem1->height() - (beamClearance * _spatium * .7);
} else {
pt.ry() = sc->upNote()->pos().y() + stem1->height() + (beamClearance * _spatium * .7);
}
}
}
sp->p1 += pt;
}
break;
case SlurAnchor::NONE:
break;
}
switch (sa2) {
case SlurAnchor::STEM: //ec can't be null
{
pt = ec->stemPos() - ec->pagePos() + ec->stem()->ldata()->line.p2();
if (useTablature) {
pt.rx() = hw2 * 0.5;
}
// don't allow overlap with beam
double yadj;
if (ec->beam() && ec->beam()->elements().front() != ec) {
yadj = 0.75;
} else if (ec->tremolo() && ec->tremolo()->twoNotes() && ec->tremolo()->chord2() == ec) {
yadj = 0.75;
} else {
yadj = -stemSideInset;
}
yadj *= _spatium * __up;
double offset = std::max(stemOffsetX * ec->intrinsicMag(), minOffset);
pt += PointF(-offset * _spatium, yadj);
// account for articulations
fixArticulations(item, pt, ec, __up, true);
sp->p2 += pt;
}
break;
case SlurAnchor::NONE:
break;
}
//
// default position:
// horizontal: middle of notehead
// vertical: _spatium * .4 above/below notehead
//
//------p1
// Compute x0, y0 and stemPos
if (sa1 == SlurAnchor::NONE || sa2 == SlurAnchor::NONE) { // need stemPos if sa2 == SlurAnchor::NONE
bool stemPos = false; // p1 starts at chord stem side
// default positions
po.rx() = hw1 * .5 + (note1 ? note1->bboxXShift() : 0.0);
if (note1) {
po.ry() = note1->pos().y();
} else if (item->up()) {
po.ry() = scr->ldata()->bbox().top();
} else {
po.ry() = scr->ldata()->bbox().top() + scr->height();
}
double offset = useTablature ? 0.75 : 0.9;
po.ry() += scr->intrinsicMag() * _spatium * offset * __up;
// adjustments for stem and/or beam
Tremolo* trem = sc ? sc->tremolo() : nullptr;
if (stem1 || (trem && trem->twoNotes())) { //sc not null
Beam* beam1 = sc->beam();
if (beam1 && (beam1->elements().back() != sc) && (sc->up() == item->up())) {
TLayout::layoutBeam(beam1, ctx);
// start chord is beamed but not the last chord of beam group
// and slur direction is same as start chord (stem side)
// in these cases, layout start of slur to stem
double beamWidthSp = ctx.conf().styleS(Sid::beamWidth).val() * beam1->magS();
double offset2 = std::max(beamClearance * sc->intrinsicMag(), minOffset) * _spatium;
double sh = stem1->length() + (beamWidthSp / 2) + offset2;
if (item->up()) {
po.ry() = sc->stemPos().y() - sc->pagePos().y() - sh;
} else {
po.ry() = sc->stemPos().y() - sc->pagePos().y() + sh;
}
po.rx() = sc->stemPosX() + (beamAnchorInset * _spatium * sc->intrinsicMag()) + (stem1->lineWidthMag() / 2 * __up);
// account for articulations
fixArticulations(item, po, sc, __up, true);
// force end of slur to layout to stem as well,
// if start and end chords have same stem direction
stemPos = true;
} else if (trem && trem->twoNotes() && trem->chord2() != sc && sc->up() == item->up()) {
TLayout::layoutTremolo(trem, ctx);
Note* note = item->up() ? sc->upNote() : sc->downNote();
double stemHeight = stem1 ? stem1->length() : defaultStemLengthStart(trem);
double offset2 = std::max(beamClearance * sc->intrinsicMag(), minOffset) * _spatium;
double sh = stemHeight + offset2;
if (item->up()) {
po.ry() = sc->stemPos().y() - sc->pagePos().y() - sh;
} else {
po.ry() = sc->stemPos().y() - sc->pagePos().y() + sh;
}
if (!stem1) {
po.rx() = note->noteheadCenterX();
} else {
po.rx() = sc->stemPosX() + (beamAnchorInset * _spatium * sc->intrinsicMag()) + (stem1->lineWidthMag() / 2. * __up);
}
fixArticulations(item, po, sc, __up, true);
stemPos = true;
} else {
// start chord is not beamed or is last chord of beam group
// or slur direction is opposite that of start chord
// at this point slur is in default position relative to note on slur side
// but we may need to make further adjustments
// if stem and slur are both up
// we need to clear stem horizontally
double stemOffsetMag = stemOffsetX * sc->intrinsicMag();
if (sc->up() && item->up()) {
// stems in tab staves come from the middle of the head, which means it's much easier
// to just subtract an offset from the notehead center (which po already is)
if (useTablature) {
po.rx() += stemOffsetMag * _spatium;
} else {
po.rx() = hw1 + _spatium * stemOffsetMag;
}
}
//
// handle case: stem up - stem down
// stem down - stem up
//
if ((sc->up() != ecr->up()) && (sc->up() == item->up())) {
item->stemFloated().left = true;
// start and end chord have opposite direction
// and slur direction is same as start chord
// (so slur starts on stem side)
// float the start point along the stem to follow direction of movement
// see for example Gould p. 111
// get position of note on slur side for start & end chords
Note* n1 = sc->up() ? sc->upNote() : sc->downNote();
Note* n2 = 0;
if (ec) {
n2 = ec->up() ? ec->upNote() : ec->downNote();
}
// differential in note positions
double yd = (n2 ? n2->pos().y() : ecr->pos().y()) - n1->pos().y();
yd *= .5;
// float along stem according to differential
double sh = stem1->height();
if (item->up() && yd < 0.0) {
po.ry() = std::max(po.y() + yd, sc->downNote()->pos().y() - sh - _spatium);
} else if (!item->up() && yd > 0.0) {
po.ry() = std::min(po.y() + yd, sc->upNote()->pos().y() + sh + _spatium);
}
// account for articulations
fixArticulations(item, po, sc, __up, true);
// we may wish to force end to align to stem as well,
// if it is in same direction
// (but it won't be, so this assignment should have no effect)
stemPos = true;
} else {
// avoid articulations
fixArticulations(item, po, sc, __up, sc->up() == item->up());
}
}
} else if (sc) {
// avoid articulations
fixArticulations(item, po, sc, __up, sc->up() == item->up());
}
// TODO: offset start position if there is another slur ending on this cr
if (sa1 == SlurAnchor::NONE) {
sp->p1 += po;
}
//------p2
if (sa2 == SlurAnchor::NONE) {
// default positions
po.rx() = hw2 * .5 + (note2 ? note2->bboxXShift() : 0.0);
if (note2) {
po.ry() = note2->pos().y();
} else if (item->up()) {
po.ry() = item->endCR()->ldata()->bbox().top();
} else {
po.ry() = item->endCR()->ldata()->bbox().top() + item->endCR()->height();
}
double offset2 = useTablature ? 0.75 : 0.9;
po.ry() += ecr->intrinsicMag() * _spatium * offset2 * __up;
// adjustments for stem and/or beam
Tremolo* trem2 = ec ? ec->tremolo() : nullptr;
if (stem2 || (trem2 && trem2->twoNotes())) { //ec can't be null
Beam* beam2 = ec->beam();
if ((stemPos && (scr->up() == ec->up()))
|| (beam2
&& (!beam2->elements().empty())
&& (beam2->elements().front() != ec)
&& (ec->up() == item->up())
&& sc && (sc->noteType() == NoteType::NORMAL)
)
|| (trem2 && trem2->twoNotes() && ec->up() == item->up())
) {
if (beam2) {
TLayout::layoutBeam(beam2, ctx);
}
if (trem2) {
TLayout::layoutTremolo(trem2, ctx);
}
// slur start was laid out to stem and start and end have same direction
// OR
// end chord is beamed but not the first chord of beam group
// and slur direction is same as end chord (stem side)
// and start chordrest is not a grace chord
// in these cases, layout end of slur to stem
double beamWidthSp = beam2 ? ctx.conf().styleS(Sid::beamWidth).val() : 0;
Note* note = item->up() ? sc->upNote() : sc->downNote();
double stemHeight = stem2 ? stem2->length() + (beamWidthSp / 2) : defaultStemLengthEnd(trem2);
double offset3 = std::max(beamClearance * ec->intrinsicMag(), minOffset) * _spatium;
double sh = stemHeight + offset3;
if (item->up()) {
po.ry() = ec->stemPos().y() - ec->pagePos().y() - sh;
} else {
po.ry() = ec->stemPos().y() - ec->pagePos().y() + sh;
}
if (!stem2) {
// tremolo whole notes
po.setX(note->noteheadCenterX());
} else {
po.setX(ec->stemPosX() + (stem2->lineWidthMag() / 2 * __up) - (beamAnchorInset * _spatium * ec->intrinsicMag()));
}
// account for articulations
fixArticulations(item, po, ec, __up, true);
} else {
// slur was not aligned to stem or start and end have different direction
// AND
// end chord is not beamed or is first chord of beam group
// or slur direction is opposite that of end chord
// if stem and slur are both down,
// we need to clear stem horizontally
double stemOffsetMag = stemOffsetX * ec->intrinsicMag();
if (!ec->up() && !item->up()) {
// stems in tab staves come from the middle of the head, which means it's much easier
// to just subtract an offset from the notehead center (which po already is)
if (useTablature) {
po.rx() -= stemOffsetMag * _spatium;
} else {
po.rx() = -_spatium * stemOffsetMag + note2->x();
}
} else if (useTablature && item->up() && ec->up()) {
// same as above
po.rx() -= _spatium * stemOffsetMag;
}
//
// handle case: stem up - stem down
// stem down - stem up
//
if ((scr->up() != ec->up()) && (ec->up() == item->up())) {
item->stemFloated().right = true;
// start and end chord have opposite direction
// and slur direction is same as end chord
// (so slur end on stem side)
// float the end point along the stem to follow direction of movement
// see for example Gould p. 111
Note* n1 = 0;
if (sc) {
n1 = sc->up() ? sc->upNote() : sc->downNote();
}
Note* n2 = ec->up() ? ec->upNote() : ec->downNote();
double yd = n2->pos().y() - (n1 ? n1->pos().y() : item->startCR()->pos().y());
yd *= .5;
double mh = stem2->height();
if (item->up() && yd > 0.0) {
po.ry() = std::max(po.y() - yd, ec->downNote()->pos().y() - mh - _spatium);
} else if (!item->up() && yd < 0.0) {
po.ry() = std::min(po.y() - yd, ec->upNote()->pos().y() + mh + _spatium);
}
// account for articulations
fixArticulations(item, po, ec, __up, true);
} else {
// avoid articulations
fixArticulations(item, po, ec, __up, ec->up() == item->up());
}
}
} else if (ec) {
// avoid articulations
fixArticulations(item, po, ec, __up, ec->up() == item->up());
}
// TODO: offset start position if there is another slur ending on this cr
sp->p2 += po;
}
}
if (item->staffType()->isTabStaff()) {
SlurTieLayout::avoidPreBendsOnTab(sc, ec, sp);
}
/// adding extra space above slurs for notes in circles
if (Slur::engravingConfiguration()->enableExperimentalFretCircle() && item->staff()->staffType()->isCommonTabStaff()) {
auto adjustSlur = [](Chord* ch, PointF& coord, bool up) {
const Fraction halfFraction = Fraction(1, 2);
if (ch && ch->ticks() >= halfFraction) {
for (EngravingItem* item : ch->el()) {
if (item && item->isFretCircle()) {
coord += PointF(0, toFretCircle(item)->ldata()->offsetFromUpNote * (up ? -1 : 1));
break;
}
}
}
};
adjustSlur(sc, sp->p1, item->up());
adjustSlur(ec, sp->p2, item->up());
}
}
void SlurTieLayout::fixArticulations(Slur* item, PointF& pt, Chord* c, double up, bool stemSide)
{
//
// handle special case of tenuto and staccato
// yo = current offset of slur from chord position
// return unchanged position, or position of outmost "close" articulation
//
double slurTipToArticVertDist = c->spatium() * 0.5 * up;
double slurTipInwardAdjust = 0.1 * item->spatium();
bool start = item->startCR() && item->startCR() == c;
bool end = item->endCR() && item->endCR() == c;
for (Articulation* a : c->articulations()) {
if (!a->layoutCloseToNote() || !a->addToSkyline()) {
continue;
}
// skip if articulation on stem side but slur is not or vice versa
if ((a->up() == c->up()) != stemSide) {
continue;
}
// Correct x-position inwards
Note* note = c->up() ? c->downNote() : c->upNote();
pt.rx() = a->x() - note->x();
if (start) {
pt.rx() += slurTipInwardAdjust;
} else if (end) {
pt.rx() -= slurTipInwardAdjust;
}
// Adjust y-position
if (a->up()) {
pt.ry() = std::min(pt.y(), a->y() + a->height() / 2 * up + slurTipToArticVertDist);
} else {
pt.ry() = std::max(pt.y(), a->y() + a->height() / 2 * up + slurTipToArticVertDist);
}
}
}
void SlurTieLayout::avoidPreBendsOnTab(const Chord* sc, const Chord* ec, SlurTiePos* sp)
{
GuitarBend* bendOnStart = nullptr;
GuitarBend* bendOnEnd = nullptr;
if (sc) {
for (Note* note : sc->notes()) {
GuitarBend* bf = note->bendFor();
GuitarBend* bb = note->bendBack();
if (bf && !bf->segmentsEmpty() && bf->type() == GuitarBendType::PRE_BEND && !bf->angledPreBend()) {
bendOnStart = bf;
} else if (bb && !bb->segmentsEmpty() && bb->type() == GuitarBendType::PRE_BEND && !bb->angledPreBend()) {
bendOnStart = bb;
}
if (bendOnStart) {
break;
}
}
}
if (ec) {
for (Note* note : ec->notes()) {
GuitarBend* bf = note->bendFor();
GuitarBend* bb = note->bendBack();
if (bf && !bf->segmentsEmpty() && bf->type() == GuitarBendType::PRE_BEND && !bf->angledPreBend()) {
bendOnEnd = bf;
} else if (bb && !bb->segmentsEmpty() && bb->type() == GuitarBendType::PRE_BEND && !bb->angledPreBend()) {
bendOnEnd = bb;
}
if (bendOnEnd) {
break;
}
}
}
if (bendOnStart) {
sp->p1.rx() = std::max(sp->p1.rx(), bendOnStart->frontSegment()->pos().x() + 0.33 * sc->spatium());
}
if (bendOnEnd) {
sp->p2.rx() = std::min(sp->p2.rx(), bendOnEnd->frontSegment()->pos().x() - 0.33 * ec->spatium());
}
}
TieSegment* SlurTieLayout::layoutTieWithNoEndNote(Tie* item)
{
StaffType* st = item->staff()->staffType(item->startNote()->tick());
Chord* c1 = item->startNote()->chord();
item->setTick(c1->tick());
if (item->slurDirection() == DirectionV::AUTO) {
bool simpleException = st && st->isSimpleTabStaff();
if (simpleException) {
item->setUp(isUpVoice(c1->voice()));
} else {
if (c1->measure()->hasVoices(c1->staffIdx(), c1->tick(), c1->actualTicks())) {
// in polyphonic passage, ties go on the stem side
item->setUp(c1->up());
} else {
item->setUp(!c1->up());
}
}
} else {
item->setUp(item->slurDirection() == DirectionV::UP ? true : false);
}
item->fixupSegments(1);
TieSegment* segment = item->segmentAt(0);
segment->setSpannerSegmentType(SpannerSegmentType::SINGLE);
segment->setSystem(item->startNote()->chord()->segment()->measure()->system());
segment->resetAdjustmentOffset();
SlurTiePos sPos;
computeStartAndEndSystem(item, sPos);
sPos.p1 = computeDefaultStartOrEndPoint(item, Grip::START);
sPos.p2 = computeDefaultStartOrEndPoint(item, Grip::END);
segment->ups(Grip::START).p = sPos.p1;
segment->ups(Grip::END).p = sPos.p2;
segment->computeBezier();
return segment;
}
static bool tieSegmentShouldBeSkipped(Tie* item)
{
Note* startNote = item->startNote();
StaffType* st = item->staff()->staffType(startNote ? startNote->tick() : Fraction(0, 1));
if (!st || !st->isTabStaff()) {
return false;
}
return !st->showBackTied() || (startNote && startNote->harmonic());
}
TieSegment* SlurTieLayout::tieLayoutFor(Tie* item, System* system)
{
item->setPos(0, 0);
if (!item->startNote()) {
LOGD("no start note");
return nullptr;
}
if (!item->endNote()) {
return layoutTieWithNoEndNote(item);
}
// do not layout ties in tablature if not showing back-tied fret marks
if (tieSegmentShouldBeSkipped(item)) {
if (!item->segmentsEmpty()) {
item->eraseSpannerSegments();
}
return nullptr;
}
item->calculateDirection();
item->calculateIsInside();
SlurTiePos sPos;
sPos.p1 = computeDefaultStartOrEndPoint(item, Grip::START);
computeStartAndEndSystem(item, sPos);
int segmentCount = sPos.system1 == sPos.system2 ? 1 : 2;
if (segmentCount == 2) {
sPos.p2 = PointF(system->endingXForOpenEndedLines(), sPos.p1.y());
} else {
sPos.p2 = computeDefaultStartOrEndPoint(item, Grip::END);
}
correctForCrossStaff(item, sPos);
forceHorizontal(item, sPos);
item->fixupSegments(segmentCount);
TieSegment* segment = item->segmentAt(0);
segment->setTrack(item->track());
segment->setSpannerSegmentType(sPos.system1 != sPos.system2 ? SpannerSegmentType::BEGIN : SpannerSegmentType::SINGLE);
segment->setSystem(system); // Needed to populate System.spannerSegments
segment->resetAdjustmentOffset();
Chord* startChord = item->startNote()->chord();
item->setTick(startChord->tick()); // Why is this here?? (M.S.)
if (segment->autoplace() && !segment->isEdited()) {
adjustX(segment, sPos, Grip::START);
if (segment->isSingleType()) {
adjustX(segment, sPos, Grip::END);
}
}
adjustYforLedgerLines(segment, sPos);
segment->ups(Grip::START).p = sPos.p1;
segment->ups(Grip::END).p = sPos.p2;
if (segment->autoplace() && !segment->isEdited()) {
adjustY(segment);
} else {
segment->computeBezier();
}
segment->addLineAttachPoints(); // add attach points to start and end note
return segment;
}
TieSegment* SlurTieLayout::tieLayoutBack(Tie* item, System* system)
{
// do not layout ties in tablature if not showing back-tied fret marks
if (tieSegmentShouldBeSkipped(item)) {
if (!item->segmentsEmpty()) {
item->eraseSpannerSegments();
}
return nullptr;
}
SlurTiePos sPos;
computeStartAndEndSystem(item, sPos);
sPos.p2 = computeDefaultStartOrEndPoint(item, Grip::END);
double x = system ? system->firstNoteRestSegmentX(true) : 0;
double y = sPos.p2.y();
sPos.p1 = PointF(x, y);
item->fixupSegments(2);
TieSegment* segment = item->segmentAt(1);
segment->setTrack(item->track());
segment->setSystem(system);
segment->resetAdjustmentOffset();
segment->adjustY(sPos.p1, sPos.p2);
segment->setSpannerSegmentType(SpannerSegmentType::END);
if (segment->autoplace() && !segment->isEdited()) {
adjustX(segment, sPos, Grip::END);
}
adjustYforLedgerLines(segment, sPos);
segment->ups(Grip::START).p = sPos.p1;
segment->ups(Grip::END).p = sPos.p2;
if (segment->autoplace() && !segment->isEdited()) {
adjustY(segment);
} else {
segment->computeBezier();
}
segment->addLineAttachPoints();
return segment;
}
void SlurTieLayout::computeStartAndEndSystem(Tie* item, SlurTiePos& slurTiePos)
{
Chord* startChord = item->startNote()->chord();
Chord* endChord = item->endNote() ? item->endNote()->chord() : nullptr;
System* startSystem = startChord->measure()->system();
if (!startSystem) {
Measure* m = startChord->measure();
LOGD("No system: measure is %d has %d count %d", m->isMMRest(), m->hasMMRest(), m->mmRestCount());
}
System* endSystem = endChord ? endChord->measure()->system() : startSystem;
slurTiePos.system1 = startSystem;
slurTiePos.system2 = endSystem;
}
PointF SlurTieLayout::computeDefaultStartOrEndPoint(const Tie* tie, Grip startOrEnd)
{
if (startOrEnd != Grip::START && startOrEnd != Grip::END) {
return PointF();
}
bool start = startOrEnd == Grip::START;
Note* note = start ? tie->startNote() : tie->endNote();
Chord* chord = note ? note->chord() : nullptr;
if (!chord) {
return PointF();
}
PointF result = note->pos() + chord->pos() + chord->segment()->pos() + chord->measure()->pos();
const bool up = tie->up();
const bool inside = tie->isInside();
const int upSign = up ? -1 : 1;
const int leftRightSign = start ? +1 : -1;
const double noteWidth = note->width();
const double noteHeight = note->height();
const double spatium = tie->spatium();
double baseX, baseY = 0.0;
if (inside) {
baseX = start ? noteWidth : 0.0;
} else {
baseX = noteOpticalCenterForTie(note, up);
baseY = upSign * noteHeight / 2;
}
result += PointF(baseX, baseY);
double visualInsetSp = 0.0;
if (inside) {
visualInsetSp = 0.2;
} else if (note->hasAnotherStraightAboveOrBelow(up)) {
visualInsetSp = 0.45;
} else {
visualInsetSp = 0.1;
}
double visualInset = visualInsetSp * spatium * leftRightSign;
const double yOffset = 0.20 * spatium * upSign; // TODO: style
result += PointF(visualInset, yOffset);
return result;
}
double SlurTieLayout::noteOpticalCenterForTie(const Note* note, bool up)
{
SymId symId = note->ldata()->cachedNoteheadSym.value();
PointF cutOutLeft = note->symSmuflAnchor(symId, up ? SmuflAnchorId::cutOutNW : SmuflAnchorId::cutOutSW);
PointF cutOutRight = note->symSmuflAnchor(symId, up ? SmuflAnchorId::cutOutNE : SmuflAnchorId::cutOutSE);
if (cutOutLeft.isNull() || cutOutRight.isNull()) {
return 0.5 * note->width();
}
return 0.5 * (cutOutLeft.x() + cutOutRight.x());
}
void SlurTieLayout::correctForCrossStaff(Tie* tie, SlurTiePos& sPos)
{
Chord* startChord = tie->startNote() ? tie->startNote()->chord() : nullptr;
Chord* endChord = tie->endNote() ? tie->endNote()->chord() : nullptr;
if (!startChord) {
return;
}
if (startChord->vStaffIdx() != tie->staffIdx() && sPos.system1) {
double yOrigin = sPos.system1->staff(tie->staffIdx())->y();
double yMoved = sPos.system1->staff(startChord->vStaffIdx())->y();
double yDiff = yMoved - yOrigin;
double curY = sPos.p1.y();
sPos.p1.setY(curY + yDiff);
}
if (!endChord) {
return;
}
if (endChord->vStaffIdx() != tie->staffIdx() && sPos.system2) {
double yOrigin = sPos.system2->staff(tie->staffIdx())->y();
double yMoved = sPos.system2->staff(endChord->vStaffIdx())->y();
double yDiff = yMoved - yOrigin;
double curY = sPos.p2.y();
sPos.p2.setY(curY + yDiff);
}
}
void SlurTieLayout::forceHorizontal(Tie* tie, SlurTiePos& sPos)
{
Note* startNote = tie->startNote();
Note* endNote = tie->endNote();
if (startNote && endNote
&& startNote->line() == endNote->line()
&& startNote->chord()->vStaffIdx() == endNote->chord()->vStaffIdx()) {
double y1 = sPos.p1.y();
double y2 = sPos.p2.y();
double outerY = tie->up() ? std::min(y1, y2) : std::max(y1, y2);
sPos.p1.setY(outerY);
sPos.p2.setY(outerY);
}
}
void SlurTieLayout::adjustX(TieSegment* tieSegment, SlurTiePos& sPos, Grip startOrEnd)
{
bool start = startOrEnd == Grip::START;
Tie* tie = tieSegment->tie();
Note* note = start ? tie->startNote() : tie->endNote();
if (!note) {
return;
}
Chord* chord = note->chord();
const double spatium = tieSegment->spatium();
PointF& tiePoint = start ? sPos.p1 : sPos.p2;
double resultingX = tiePoint.x();
bool isOuterTieOfChord = tie->isOuterTieOfChord(startOrEnd);
if (isOuterTieOfChord) {
Tie* otherTie = start ? note->tieBack() : note->tieFor();
bool avoidOtherTie = otherTie && otherTie->up() == tie->up() && !otherTie->isInside();
if (avoidOtherTie) {
resultingX += 0.1 * spatium * (start ? 1 : -1);
}
}
bool avoidStem = chord->stem() && chord->stem()->visible() && chord->up() == tie->up();
if (isOuterTieOfChord && !avoidStem) {
tieSegment->addAdjustmentOffset(PointF(resultingX - tiePoint.x(), 0.0), startOrEnd);
tiePoint.setX(resultingX);
return;
}
PointF chordSystemPos = chord->pos() + chord->segment()->pos() + chord->measure()->pos();
if (chord->vStaffIdx() != tieSegment->staffIdx()) {
System* system = tieSegment->system();
double yDiff = system->staff(chord->vStaffIdx())->y() - system->staff(tie->staffIdx())->y();
chordSystemPos += PointF(0.0, yDiff);
}
Shape chordShape = chord->shape().translate(chordSystemPos);
bool ignoreDot = start && isOuterTieOfChord;
chordShape.remove_if([&](ShapeElement& s) {
return !s.item() || (s.item() == note || s.item()->isHook() || s.item()->isLedgerLine() || (s.item()->isNoteDot() && ignoreDot));
});
const double arcSideMargin = 0.3 * spatium;
const double pointsSideMargin = 0.15 * spatium;
const double yBelow = tiePoint.y() - (tie->up() ? arcSideMargin : pointsSideMargin);
const double yAbove = tiePoint.y() + (tie->up() ? pointsSideMargin : arcSideMargin);
double pointToClear = start ? chordShape.rightMostEdgeAtHeight(yBelow, yAbove)
: chordShape.leftMostEdgeAtHeight(yBelow, yAbove);
const double padding = 0.20 * spatium * (start ? 1 : -1); // TODO: style
pointToClear += padding;
resultingX = start ? std::max(resultingX, pointToClear) : std::min(resultingX, pointToClear);
adjustXforLedgerLines(tieSegment, start, chord, note, chordSystemPos, padding, resultingX);
tieSegment->addAdjustmentOffset(PointF(resultingX - tiePoint.x(), 0.0), startOrEnd);
tiePoint.setX(resultingX);
}
void SlurTieLayout::adjustXforLedgerLines(TieSegment* tieSegment, bool start, Chord* chord, Note* note,
const PointF& chordSystemPos, double padding, double& resultingX)
{
if (tieSegment->tie()->isInside() || !chord->ledgerLines()) {
return;
}
bool isOuterNote = note == chord->upNote() || note == chord->downNote();
if (isOuterNote) {
return;
}
bool ledgersAbove = false;
bool ledgersBelow = false;
for (LedgerLine* ledger = chord->ledgerLines(); ledger; ledger = ledger->next()) {
if (ledger->y() < 0.0) {
ledgersAbove = true;
} else {
ledgersBelow = true;
}
if (ledgersAbove && ledgersBelow) {
break;
}
}
int noteLine = note->line();
bool isOddLine = noteLine % 2 != 0;
bool isAboveStaff = noteLine <= 0;
bool isBelowStaff = noteLine >= 2 * (note->staff()->lines(note->tick()) - 1);
bool isInsideStaff = !isAboveStaff && !isBelowStaff;
if (isOddLine || isInsideStaff || (isAboveStaff && !ledgersAbove) || (isBelowStaff && !ledgersBelow)) {
return;
}
Shape noteShape = note->shape().translated(note->pos() + chordSystemPos);
double xNoteEdge = (start ? noteShape.right() : -noteShape.left()) + padding;
resultingX = start ? std::max(resultingX, xNoteEdge) : std::min(resultingX, xNoteEdge);
}
void SlurTieLayout::adjustYforLedgerLines(TieSegment* tieSegment, SlurTiePos& sPos)
{
Tie* tie = tieSegment->tie();
Note* note = tieSegment->isSingleBeginType() ? tie->startNote() : tie->endNote();
if (!note) {
return;
}
Chord* chord = note->chord();
if (!chord->ledgerLines()) {
return;
}
PointF chordSystemPos = chord->pos() + chord->segment()->pos() + chord->segment()->measure()->pos();
PointF& tiePoint = tieSegment->isSingleBeginType() ? sPos.p1 : sPos.p2;
double spatium = tie->spatium();
int upSign = tie->up() ? -1 : 1;
double margin = 0.4 * spatium;
for (LedgerLine* ledger = chord->ledgerLines(); ledger; ledger = ledger->next()) {
PointF ledgerPos = ledger->pos() + chordSystemPos;
double yDiff = upSign * (ledgerPos.y() - tiePoint.y());
bool collision = yDiff > 0 && yDiff < margin;
if (collision) {
sPos.p1 += PointF(0.0, -upSign * (margin - yDiff));
sPos.p2 += PointF(0.0, -upSign * (margin - yDiff));
tieSegment->computeBezier();
break;
}
}
}
void SlurTieLayout::adjustY(TieSegment* tieSegment)
{
Staff* staff = tieSegment->staff();
if (!staff) {
return;
}
Fraction tick = tieSegment->tick();
tieSegment->computeBezier();
bool up = tieSegment->tie()->up();
int upSign = up ? -1 : 1;
const double spatium = tieSegment->spatium();
const double staffLineDist = staff->lineDistance(tick) * spatium;
const double staffLineThickness = tieSegment->style().styleMM(Sid::staffLineWidth);
// 1. Check for bad end point protrusion
const double endPointY = tieSegment->ups(Grip::START).p.y();
const int closestLineToEndpoints = up ? floor(endPointY / staffLineDist) : ceil(endPointY / staffLineDist);
const bool isEndInsideStaff = closestLineToEndpoints >= 0 && closestLineToEndpoints < staff->lines(tick);
const bool isEndInsideLedgerLines = !isEndInsideStaff && !tieSegment->tie()->isOuterTieOfChord(Grip::START);
const double halfLineThicknessCorrection = 0.5 * staffLineThickness * upSign;
const double protrusion = abs(endPointY - (closestLineToEndpoints * spatium - halfLineThicknessCorrection));
const double badIntersectionLimit = 0.20 * spatium; // TODO: style
bool badIntersection = protrusion < badIntersectionLimit && (isEndInsideStaff || isEndInsideLedgerLines);
if (badIntersection) {
double correctedY = closestLineToEndpoints * spatium + halfLineThicknessCorrection + badIntersectionLimit * upSign;
tieSegment->addAdjustmentOffset(PointF(0.0, correctedY - endPointY), Grip::START);
tieSegment->addAdjustmentOffset(PointF(0.0, correctedY - endPointY), Grip::END);
tieSegment->ups(Grip::START).p.setY(correctedY);
tieSegment->ups(Grip::END).p.setY(correctedY);
tieSegment->computeBezier();
}
// 2. Check for bad arc protrusion
RectF tieSegmentBBox = tieSegment->ldata()->bbox();
double tieLength = tieSegmentBBox.width();
double tieHeight = tieSegmentBBox.height();
double midThickness = tieSegment->midThickness() * 2;
double yOuterApogee = up ? tieSegmentBBox.top() : tieSegmentBBox.bottom();
double yInnerApogee = yOuterApogee - midThickness * upSign;
double yMidApogee = 0.5 * (yOuterApogee + yInnerApogee);
int closestLineToArc = round(yMidApogee / staffLineDist);
bool isArcInsideStaff = closestLineToArc >= 0 && closestLineToArc < staff->lines(tick);
Tie* tie = tieSegment->tie();
Note* note = tie->startNote();
if (!isArcInsideStaff) {
return;
}
double outwardMargin = -upSign * (yOuterApogee - (closestLineToArc * spatium - halfLineThicknessCorrection));
double inwardMargin = upSign * (yInnerApogee - (closestLineToArc * spatium + halfLineThicknessCorrection));
const double badArcIntersectionLimit = tieLength < 3 * spatium ? 0.1 * spatium : 0.15 * spatium;
bool increaseArc = outwardMargin - 0.5 * badArcIntersectionLimit < inwardMargin;
bool correctOutwards = inwardMargin < badArcIntersectionLimit && increaseArc;
bool correctInwards = outwardMargin < badArcIntersectionLimit && !increaseArc;
if (!correctInwards && !correctOutwards) {
return;
}
bool isSmallTie = tieLength < 2.0 * spatium && tieHeight < 0.7 * spatium;
// For ties this small, try to fit them within a staff space before changing their arc
if (isSmallTie) {
Tie* tie2 = tieSegment->tie();
bool isInside = tie2->isInside();
bool isOuterOfChord = tie2->isOuterTieOfChord(Grip::START) || tie2->isOuterTieOfChord(Grip::END);
bool hasTiedSecondInside = tie2->hasTiedSecondInside();
if (!isInside && !isOuterOfChord && !hasTiedSecondInside && !hasEndPointAboveNote(tieSegment)) {
double currentY = tieSegment->ups(Grip::START).p.y();
double yCorrection = -upSign * (badArcIntersectionLimit - outwardMargin);
tieSegment->addAdjustmentOffset(PointF(0.0, yCorrection), Grip::START);
tieSegment->addAdjustmentOffset(PointF(0.0, yCorrection), Grip::END);
tieSegment->ups(Grip::START).p.setY(currentY + yCorrection);
tieSegment->ups(Grip::END).p.setY(currentY + yCorrection);
tieSegment->computeBezier();
return;
} else {
correctOutwards = true;
}
}
double arcCorrection = correctOutwards ? (badArcIntersectionLimit - inwardMargin) : (badArcIntersectionLimit - outwardMargin);
double maxArcCorrection = 0.75 * tieHeight;
double rest = arcCorrection > maxArcCorrection ? arcCorrection - maxArcCorrection : 0.0;
arcCorrection = std::min(arcCorrection, maxArcCorrection);
if (correctOutwards) {
if (rest > 0) {
rest *= upSign;
double currentY = tieSegment->ups(Grip::START).p.y();
tieSegment->addAdjustmentOffset(PointF(0.0, rest), Grip::START);
tieSegment->addAdjustmentOffset(PointF(0.0, rest), Grip::END);
tieSegment->ups(Grip::START).p.setY(currentY + rest);
tieSegment->ups(Grip::END).p.setY(currentY + rest);
}
tieSegment->computeBezier(PointF(0.0, -arcCorrection));
} else if (correctInwards) {
tieSegment->computeBezier(PointF(0.0, arcCorrection));
}
}
bool SlurTieLayout::hasEndPointAboveNote(TieSegment* tieSegment)
{
Note* startNote = tieSegment->tie()->startNote();
Note* endNote = tieSegment->tie()->endNote();
if ((tieSegment->isSingleBeginType() && !startNote) || (tieSegment->isSingleEndType() && !endNote)) {
return false;
}
Chord* startChord = startNote->chord();
PointF startNotePos = startNote->pos() + startChord->pos() + startChord->segment()->pos() + startChord->measure()->pos();
Chord* endChord = endNote->chord();
PointF endNotePos = endNote->pos() + endChord->pos() + endChord->segment()->pos() + endChord->measure()->pos();
PointF tieStartPos = tieSegment->ups(Grip::START).pos();
PointF tieEndPos = tieSegment->ups(Grip::END).pos();
return tieStartPos.x() < startNotePos.x() + startNote->width() || tieEndPos.x() > endNotePos.x();
}
void SlurTieLayout::resolveVerticalTieCollisions(const std::vector<TieSegment*>& stackedTies)
{
if (stackedTies.size() < 2) {
return;
}
std::list<TieSegment*> downwardTies;
std::list<TieSegment*> upwardTies;
for (TieSegment* tieSegment : stackedTies) {
if (!tieSegment->tie()->up()) {
downwardTies.push_front(tieSegment);
} else {
upwardTies.push_back(tieSegment);
}
}
auto fixTieCollision = [](TieSegment* thisTie, TieSegment* nextTie) {
double spatium = thisTie->spatium();
bool up = thisTie->tie()->up();
int upSign = up ? -1 : 1;
double thisTieOuterY = up ? thisTie->ldata()->bbox().top() : thisTie->ldata()->bbox().bottom();
double nextTieInnerY = (up ? nextTie->ldata()->bbox().top() : nextTie->ldata()->bbox().bottom())
- upSign * 2 * nextTie->midThickness();
double clearanceMargin = 0.15 * spatium;
bool collision = upSign * (nextTieInnerY - thisTieOuterY) < clearanceMargin;
if (!collision) {
return;
}
Staff* staff = thisTie->staff();
Fraction tick = thisTie->tick();
double yMidPoint = 0.5 * (thisTieOuterY + nextTieInnerY);
double halfLineDist = 0.5 * staff->lineDistance(tick) * spatium;
int midLine = round(yMidPoint / halfLineDist);
bool insideStaff = midLine >= -1 && midLine <= 2 * (staff->lines(tick) - 1) + 1;
if (insideStaff) {
yMidPoint = midLine * halfLineDist;
}
double thisTieYCorrection = yMidPoint - upSign * 0.5 * clearanceMargin - thisTieOuterY;
double nextTieYCorrection = yMidPoint + upSign * 0.5 * clearanceMargin - nextTieInnerY;
double thisShoulderOff = thisTie->adjustmentOffset(Grip::BEZIER1).y();
double nextShoulderOff = nextTie->adjustmentOffset(Grip::BEZIER1).y();
// Subtract it otherwise it gets summed twice
thisTie->addAdjustmentOffset(PointF(0.0, -thisShoulderOff), Grip::BEZIER1);
thisTie->addAdjustmentOffset(PointF(0.0, -thisShoulderOff), Grip::BEZIER2);
nextTie->addAdjustmentOffset(PointF(0.0, -nextShoulderOff), Grip::BEZIER1);
nextTie->addAdjustmentOffset(PointF(0.0, -nextShoulderOff), Grip::BEZIER2);
thisTie->computeBezier(PointF(0.0, -upSign * (thisShoulderOff + thisTieYCorrection)));
nextTie->computeBezier(PointF(0.0, -upSign * (nextShoulderOff + nextTieYCorrection)));
};
if (upwardTies.size() >= 2) {
for (auto it = upwardTies.begin(); std::next(it, 1) != upwardTies.end(); ++it) {
TieSegment* thisTie = *it;
TieSegment* nextTie = *(std::next(it, 1));
fixTieCollision(thisTie, nextTie);
}
}
if (downwardTies.size() >= 2) {
for (auto it = downwardTies.begin(); std::next(it, 1) != downwardTies.end(); ++it) {
TieSegment* thisTie = *it;
TieSegment* nextTie = *(std::next(it, 1));
fixTieCollision(thisTie, nextTie);
}
}
}
void SlurTieLayout::computeUp(Slur* slur, LayoutContext& ctx)
{
switch (slur->slurDirection()) {
case DirectionV::UP:
slur->setUp(true);
break;
case DirectionV::DOWN:
slur->setUp(false);
break;
case DirectionV::AUTO:
{
//
// assumption:
// slurs have only chords or rests as start/end elements
//
ChordRest* chordRest1 = slur->startCR();
ChordRest* chordRest2 = slur->endCR();
if (chordRest1 == 0 || chordRest2 == 0) {
slur->setUp(true);
break;
}
Chord* chord1 = slur->startCR()->isChord() ? toChord(slur->startCR()) : 0;
Chord* chord2 = slur->endCR()->isChord() ? toChord(slur->endCR()) : 0;
if (chord2 && !chord2->staff()->isDrumStaff(chord2->tick())
&& slur->startCR()->measure()->system() != slur->endCR()->measure()->system()) {
// HACK: if the end chord is in a different system, it may have never been laid out yet.
// But we need to know its direction to decide slur direction, so need to compute it here.
for (Note* note : chord2->notes()) {
note->updateLine(); // because chord direction is based on note lines
}
ChordLayout::computeUp(chord2, ctx);
}
if (chord1 && chord1->beam() && chord1->beam()->cross()) {
// TODO: stem direction is not finalized, so we cannot use it here
slur->setUp(true);
break;
}
slur->setUp(!(chordRest1->up()));
// Check if multiple voices
bool multipleVoices = false;
Measure* m1 = chordRest1->measure();
while (m1 && m1->tick() <= chordRest2->tick()) {
if ((m1->hasVoices(chordRest1->staffIdx(), slur->tick(), slur->ticks() + chordRest2->ticks()))
&& chord1) {
multipleVoices = true;
break;
}
m1 = m1->nextMeasure();
}
if (multipleVoices) {
// slurs go on the stem side
if (chordRest1->voice() > 0 || chordRest2->voice() > 0) {
slur->setUp(false);
} else {
slur->setUp(true);
}
} else if (chord1 && chord2 && !chord1->isGrace() && slur->isDirectionMixture(chord1, chord2)) {
// slurs go above if there are mixed direction stems between c1 and c2
// but grace notes are exceptions
slur->setUp(true);
} else if (chord1 && chord2 && chord1->isGrace() && chord2 != chord1->parent() && slur->isDirectionMixture(chord1, chord2)) {
slur->setUp(true);
}
}
break;
}
}
double SlurTieLayout::defaultStemLengthStart(Tremolo* tremolo)
{
return TremoloLayout::extendedStemLenWithTwoNoteTremolo(tremolo,
tremolo->chord1()->defaultStemLength(),
tremolo->chord2()->defaultStemLength()).first;
}
double SlurTieLayout::defaultStemLengthEnd(Tremolo* tremolo)
{
return TremoloLayout::extendedStemLenWithTwoNoteTremolo(tremolo,
tremolo->chord1()->defaultStemLength(),
tremolo->chord2()->defaultStemLength()).second;
}
void SlurTieLayout::layoutSegment(SlurSegment* item, LayoutContext& ctx, const PointF& p1, const PointF& p2)
{
SlurSegment::LayoutData* ldata = item->mutldata();
const StaffType* stType = item->staffType();
if (stType && stType->isHiddenElementOnTab(ctx.conf().style(), Sid::slurShowTabCommon, Sid::slurShowTabSimple)) {
ldata->setIsSkipDraw(true);
return;
}
ldata->setIsSkipDraw(false);
ldata->setPos(PointF());
item->ups(Grip::START).p = p1;
item->ups(Grip::END).p = p2;
item->setExtraHeight(0.0);
//Adjust Y pos to staff type yOffset before other calculations
if (item->staffType()) {
ldata->moveY(item->staffType()->yoffset().val() * item->spatium());
}
item->computeBezier();
}