Skip to content

Small stem layout refactor #27048

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

Merged
merged 1 commit into from
Apr 9, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
330 changes: 0 additions & 330 deletions src/engraving/dom/chord.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1138,336 +1138,6 @@ void Chord::setScore(Score* s)
processSiblings([s](EngravingItem* e) { e->setScore(s); }, true);
}

// all values are in quarter spaces
int Chord::calcMinStemLength()
{
int minStemLength = 0; // in quarter spaces
double _spatium = spatium();

if (tremoloSingleChord()) {
// buzz roll's height is actually half of the visual height,
// so we need to multiply it by 2 to get the actual height
int buzzRollMultiplier = tremoloSingleChord()->isBuzzRoll() ? 2 : 1;
minStemLength += ceil(tremoloSingleChord()->minHeight() / intrinsicMag() * 4.0 * buzzRollMultiplier);
int outSidePadding = style().styleMM(Sid::tremoloOutSidePadding).val() / _spatium * 4.0;
int noteSidePadding = style().styleMM(Sid::tremoloNoteSidePadding).val() / _spatium * 4.0;

int outsideStaffOffset = 0;
if (!staff()->isTabStaff(tick())) {
Note* lineNote = ldata()->up ? upNote() : downNote();
if (lineNote->line() == INVALID_LINE) {
lineNote->updateLine();
}

int line = lineNote->line();
line *= 2; // convert to quarter spaces

if (!ldata()->up && line < -2) {
outsideStaffOffset = -line;
} else if (ldata()->up && line > staff()->lines(tick()) * 4) {
outsideStaffOffset = line - (staff()->lines(tick()) * 4) + 4;
}
}
minStemLength += (outSidePadding + std::max(noteSidePadding, outsideStaffOffset));

if (m_hook) {
bool straightFlags = style().styleB(Sid::useStraightNoteFlags);
double smuflAnchor = m_hook->smuflAnchor().y() * (ldata()->up ? 1 : -1);
int hookOffset = floor((m_hook->height() / intrinsicMag() + smuflAnchor) / _spatium * 4) - (straightFlags ? 0 : 2);
// some fonts have hooks that extend very far down (making the height of the hook very large)
// so we constrain to a reasonable maximum for hook length
hookOffset = std::min(hookOffset, 11);
// TODO: when the SMuFL metadata includes a cutout for flags, replace this with that metadata
// https://github.com/w3c/smufl/issues/203
int cutout = up() ? 5 : 7;
if (straightFlags) {
// don't need cutout for straight flags (they are similar to beams)
cutout = 0;
} else if (beams() >= 2) {
// beams greater than two extend outwards and thus don't factor into the cutout
cutout -= 2;
}

minStemLength += hookOffset - cutout;

// hooks with trems inside them no longer ceil (snap) to nearest 0.5sp.
// if we want to add that back in, here is the place to do it:
// minStemLength = ceil(minStemLength / 2.0) * 2;
}
}
if (m_beam || tremoloTwoChord()) {
int beamCount = (m_beam ? beams() : 0) + (tremoloTwoChord() ? tremoloTwoChord()->lines() : 0);
static const int minInnerStemLengths[4] = { 10, 9, 8, 7 };
int innerStemLength = minInnerStemLengths[std::min(beamCount, 3)];
int beamsHeight = beamCount * (style().styleB(Sid::useWideBeams) ? 4 : 3) - 1;
int newMinStemLength = std::max(minStemLength, innerStemLength);
newMinStemLength += beamsHeight;
// for 4+ beams, there are a few situations where we need to lengthen the stem by 1
int noteLine = line();
int staffLines = staff()->lines(tick());
bool noteInStaff = (ldata()->up && noteLine > 0) || (!ldata()->up && noteLine < (staffLines - 1) * 2);
if (beamCount >= 4 && noteInStaff) {
newMinStemLength++;
}
minStemLength = std::max(minStemLength, newMinStemLength);
}
return minStemLength;
}

// all values are in quarter spaces
int Chord::stemLengthBeamAddition() const
{
if (m_hook) {
return 0;
}
int beamCount = (m_beam ? beams() : 0) + (tremoloTwoChord() ? tremoloTwoChord()->lines() : 0);
switch (beamCount) {
case 0:
case 1:
case 2:
return 0;
case 3:
return 2;
default:
return (beamCount - 3) * (style().styleB(Sid::useWideBeams) ? 4 : 3);
}
}

int Chord::minStaffOverlap(bool up, int staffLines, int beamCount, bool hasHook, double beamSpacing, bool useWideBeams, bool isFullSize)
{
int beamOverlap = 8;
if (isFullSize) {
if (beamCount == 3 && !hasHook) {
beamOverlap = 12;
} else if (beamCount >= 4 && !hasHook) {
beamOverlap = (beamCount - 4) * beamSpacing + (useWideBeams ? 16 : 14);
}
}

int staffLineOffset = isFullSize ? 1 : 4;
int staffOverlap = std::min(beamOverlap, (staffLines - staffLineOffset) * 4);
if (!up) {
return staffOverlap;
}
return (staffLines - 1) * 4 - staffOverlap;
}

// all values are in quarter spaces
int Chord::maxReduction(int extensionOutsideStaff) const
{
if (!style().styleB(Sid::shortenStem)) {
return 0;
}
// [extensionOutsideStaff][beamCount]
static const int maxReductions[4][5] = {
//1sp 1.5sp 2sp 2.5sp >=3sp -- extensionOutsideStaff
{ 1, 2, 3, 4, 4 }, // 0 beams
{ 0, 1, 2, 3, 3 }, // 1 beam
{ 0, 1, 1, 1, 1 }, // 2 beams
{ 0, 0, 0, 1, 1 }, // 3 beams
};
int beamCount = 0;
if (!m_hook) {
beamCount = tremoloTwoChord() ? tremoloTwoChord()->lines() + (m_beam ? beams() : 0) : beams();
}
bool hasTradHook = m_hook && !style().styleB(Sid::useStraightNoteFlags);
if (m_hook && !hasTradHook) {
beamCount = std::min(beamCount, 2); // the straight glyphs extend outwards after 2 beams
}
if (beamCount >= 4) {
return 0;
}
int extensionHalfSpaces = floor(extensionOutsideStaff / 2.0);
extensionHalfSpaces = std::min(extensionHalfSpaces, 4);
int reduction = maxReductions[beamCount][extensionHalfSpaces];
if (intrinsicMag() < 1) {
// there is an exception for grace-sized stems with hooks.
// reducing by the full amount puts the hooks too low. Limit reduction to 0.5sp
if (hasTradHook) {
reduction = std::min(reduction, 1);
}
} else {
// there are a few exceptions for normal-sized (non-grace) beams
if (beamCount == 1 && extensionHalfSpaces < 2) {
// 1) if the extension is less than 1sp above or below the staff, they've been adjusted
// already to play nicely with staff lines. Reduce by 1sp.
reduction = 2;
} else if (beamCount == 3 && extensionHalfSpaces == 3) {
// 2) if there are three beams and it extends 1.5sp above or below the staff, we need to
// *extend* the stem rather than reduce it.
reduction = 0;
}
if (hasTradHook) {
reduction = std::min(reduction, 1);
} else if (m_hook && beams() > 2) {
reduction += 1;
}
}
return reduction;
}

// all values are in quarter spaces
int Chord::stemOpticalAdjustment(int stemEndPosition) const
{
if (m_hook && !m_beam) {
return 0;
}
int beamCount = (tremoloTwoChord() ? tremoloTwoChord()->lines() : 0) + (m_beam ? beams() : 0);
if (beamCount == 0 || beamCount > 2) {
return 0;
}
bool isOnEvenLine = fmod(stemEndPosition + 4, 4) == 2;
if (isOnEvenLine) {
return 1;
}
return 0;
}

int Chord::calc4BeamsException(int stemLength) const
{
int difference = 0;
int staffLines = (staff()->lines(tick()) - 1) * 2;
if (up() && upNote()->line() > staffLines) {
difference = upNote()->line() - staffLines;
} else if (!up() && downNote()->line() < 0) {
difference = std::abs(downNote()->line());
}
switch (difference) {
case 2:
return std::max(stemLength, 21);
case 3:
case 4:
return std::max(stemLength, 23);
default:
return stemLength;
}
}

//-----------------------------------------------------------------------------
// defaultStemLength
/// Get the default stem length for this chord
/// all internal calculation is done in quarter spaces
/// using integers to eliminate all possibilities for rounding errors
//-----------------------------------------------------------------------------

double Chord::calcDefaultStemLength()
{
// returns default length even if the chord doesn't have a stem

double _spatium = spatium();
double lineDistance = (staff() ? staff()->lineDistance(tick()) : 1.0);

const Staff* staffItem = staff();
const StaffType* staffType = staffItem ? staffItem->staffTypeForElement(this) : nullptr;
const StaffType* tab = (staffType && staffType->isTabStaff()) ? staffType : nullptr;

bool isBesideTabStaff = tab && !tab->stemless() && !tab->stemThrough();
if (isBesideTabStaff) {
return tab->chordStemLength(this) * _spatium;
}

int defaultStemLength = style().styleD(Sid::stemLength) * 4;
defaultStemLength += stemLengthBeamAddition();
if (tab) {
defaultStemLength *= 1.5;
}
// extraHeight represents the extra vertical distance between notehead and stem start
// eg. slashed noteheads etc
double extraHeight = (ldata()->up ? upNote()->stemUpSE().y() : downNote()->stemDownNW().y()) / intrinsicMag() / _spatium;
int shortestStem = style().styleB(Sid::useWideBeams) ? 12 : (style().styleD(Sid::shortestStem) + std::abs(extraHeight)) * 4;
int quarterSpacesPerLine = std::floor(lineDistance * 2);
int chordHeight = (downLine() - upLine()) * quarterSpacesPerLine; // convert to quarter spaces
int stemLength = defaultStemLength;

int minStemLengthQuarterSpaces = calcMinStemLength();
m_minStemLength = minStemLengthQuarterSpaces / 4.0 * _spatium;

int staffLineCount = staffItem ? staffItem->lines(tick()) : 5;
int shortStemStart = style().styleI(Sid::shortStemStartLocation) * quarterSpacesPerLine + 1;
bool useWideBeams = style().styleB(Sid::useWideBeams);
int beamCount = (tremoloTwoChord() ? tremoloTwoChord()->lines() : 0) + (m_beam ? beams() : 0);
int middleLine = minStaffOverlap(ldata()->up, staffLineCount,
beamCount, !!m_hook, useWideBeams ? 4 : 3,
useWideBeams, !(isGrace() || isSmall()));
if (up()) {
int stemEndPosition = upLine() * quarterSpacesPerLine - defaultStemLength;
double stemEndPositionMag = (double)upLine() * quarterSpacesPerLine - (defaultStemLength * intrinsicMag());
int idealStemLength = defaultStemLength;

if (stemEndPositionMag <= -shortStemStart) {
int reduction = maxReduction(std::abs((int)floor(stemEndPositionMag) + shortStemStart));
idealStemLength = std::max(idealStemLength - reduction, shortestStem);
} else if (stemEndPosition > middleLine) {
// this case will be taken care of below; even if we were to adjust here we'd have
// to adjust again later if the line spacing != 1.0 or if _relativeMag != 1.0
} else {
idealStemLength -= stemOpticalAdjustment(stemEndPosition);
idealStemLength = std::max(idealStemLength, shortestStem);
}
stemLength = std::max(idealStemLength, minStemLengthQuarterSpaces);
} else {
int stemEndPosition = downLine() * quarterSpacesPerLine + defaultStemLength;
double stemEndPositionMag = (double)downLine() * quarterSpacesPerLine + (defaultStemLength * intrinsicMag());
int idealStemLength = defaultStemLength;

int downShortStemStart = (staffLineCount - 1) * (2 * quarterSpacesPerLine) + shortStemStart;
if (stemEndPositionMag >= downShortStemStart) {
int reduction = maxReduction(std::abs((int)ceil(stemEndPositionMag) - downShortStemStart));
idealStemLength = std::max(idealStemLength - reduction, shortestStem);
} else if (stemEndPosition < middleLine) {
// this case will be taken care of below; even if we were to adjust here we'd have
// to adjust again later if the line spacing != 1.0 or if _relativeMag != 1.0
} else {
idealStemLength -= stemOpticalAdjustment(stemEndPosition);
idealStemLength = std::max(idealStemLength, shortestStem);
}

stemLength = std::max(idealStemLength, minStemLengthQuarterSpaces);
}
if (beamCount == 4 && !m_hook) {
stemLength = calc4BeamsException(stemLength);
}

double finalStemLength = (chordHeight / 4.0 * _spatium) + ((stemLength / 4.0 * _spatium) * intrinsicMag());
double extraLength = 0.;
Note* startNote = ldata()->up ? downNote() : upNote();
if (!startNote->fixed()) {
// when the chord's magnitude is < 1, the stem length with mag can find itself below the middle line.
// in those cases, we have to add the extra amount to it to bring it to a minimum.
double upValue = ldata()->up ? -1. : 1.;
double stemStart = startNote->ldata()->pos().y();
double stemEndMag = stemStart + (finalStemLength * upValue);
double topLine = 0.0;
lineDistance *= _spatium;
double bottomLine = lineDistance * (staffLineCount - 1.0);
double target = 0.0;
double midLine = middleLine / 4.0 * lineDistance;
if (muse::RealIsEqualOrMore(lineDistance / _spatium, 1.0)) {
// need to extend to middle line, or to opposite line if staff is < 2sp tall
if (bottomLine < 2 * _spatium) {
target = ldata()->up ? topLine : bottomLine;
} else {
double twoSpIn = ldata()->up ? bottomLine - (2 * _spatium) : topLine + (2 * _spatium);
target = muse::RealIsEqual(lineDistance / _spatium, 1.0) ? midLine : twoSpIn;
}
} else {
// need to extend to second line in staff, or to opposite line if staff has < 3 lines
if (staffLineCount < 3) {
target = ldata()->up ? topLine : bottomLine;
} else {
target = ldata()->up ? bottomLine - (2 * lineDistance) : topLine + (2 * lineDistance);
}
}
extraLength = 0.0;
if (ldata()->up && stemEndMag > target) {
extraLength = stemEndMag - target;
} else if (!ldata()->up && stemEndMag < target) {
extraLength = target - stemEndMag;
}
}
return finalStemLength + extraLength;
}

Fraction Chord::endTickIncludingTied() const
{
const Chord* lastTied = this;
Expand Down
16 changes: 1 addition & 15 deletions src/engraving/dom/chord.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#ifndef MU_ENGRAVING_CHORD_H
#define MU_ENGRAVING_CHORD_H
#pragma once

/**
\file
Expand Down Expand Up @@ -129,10 +128,7 @@ class Chord final : public ChordRest

double defaultStemLength() const { return m_defaultStemLength; }
void setDefaultStemLength(double l) { m_defaultStemLength = l; }
double minStemLength() const { return m_minStemLength; }
void setBeamExtension(double extension);
static int minStaffOverlap(bool up, int staffLines, int beamCount, bool hasHook, double beamSpacing, bool useWideBeams,
bool isFullSize);

std::vector<Note*>& notes() { return m_notes; }
const std::vector<Note*>& notes() const { return m_notes; }
Expand Down Expand Up @@ -318,8 +314,6 @@ class Chord final : public ChordRest
double downPos() const override;
double centerX() const;

double calcDefaultStemLength();

struct StartEndSlurs {
bool startUp = false;
bool startDown = false;
Expand All @@ -343,12 +337,6 @@ class Chord final : public ChordRest
Chord(Segment* parent = 0);
Chord(const Chord&, bool link = false);

int stemLengthBeamAddition() const;
int maxReduction(int extensionOutsideStaff) const;
int stemOpticalAdjustment(int stemEndPosition) const;
int calcMinStemLength();
int calc4BeamsException(int stemLength) const;

// `includeTemporarySiblings`: whether items that are deleted & recreated during every layout should also be processed
void processSiblings(std::function<void(EngravingItem*)> func, bool includeTemporarySiblings) const;

Expand Down Expand Up @@ -383,7 +371,6 @@ class Chord final : public ChordRest
double m_spaceRw = 0.0;

double m_defaultStemLength = 0.0;
double m_minStemLength = 0.0;

bool m_isUiItem = false;

Expand All @@ -402,4 +389,3 @@ class Chord final : public ChordRest
std::vector<Articulation*> m_articulations;
};
} // namespace mu::engraving
#endif
Loading