From 35780ed7c2540b8fb102db4e32357a3f808efff7 Mon Sep 17 00:00:00 2001 From: julianz- <6255571+julianz-@users.noreply.github.com> Date: Thu, 30 Apr 2026 01:50:59 -0700 Subject: [PATCH] Fix unnecessary notehead separation for unison notes of different durations When two notes share the same pitch and spelling (unison), MuseScore was forcing them apart even when their noteheads could be shared. This change allows unison notes to share a notehead regardless of duration difference, hiding the shorter note's filled notehead in favour of the open one. Dot differences are also no longer treated as a reason to separate unison notes, since the dot can survive independently while the noteheads are combined. Notes with explicitly set notehead types are unaffected, providing a natural override for cases where separation is intentional. --- src/engraving/dom/note.h | 4 ++++ src/engraving/rendering/score/chordlayout.cpp | 19 ++++++++++++++++++- src/engraving/rendering/score/tlayout.cpp | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/engraving/dom/note.h b/src/engraving/dom/note.h index 6c12f3df4e040..477eeadfb64cc 100644 --- a/src/engraving/dom/note.h +++ b/src/engraving/dom/note.h @@ -300,6 +300,8 @@ class Note final : public EngravingItem bool hidden() const { return m_hidden; } void setHidden(bool val) { m_hidden = val; } + bool hideNotehead() const { return m_hideNotehead; } + void setHideNotehead(bool val) { m_hideNotehead = val; } bool dotsHidden() const { return m_dotsHidden; } void setDotsHidden(bool val) { m_dotsHidden = val; } @@ -494,6 +496,8 @@ class Note final : public EngravingItem bool m_hidden = false; // marks this note as the hidden one if there are // overlapping notes; hidden notes are not played // and heads + accidentals are not shown + bool m_hideNotehead = false; // hides notehead glyph only for unison sharing; + // unlike m_hidden, does not affect playback or accidentals bool m_dotsHidden = false; // dots of hidden notes are hidden too // except if only one note is dotted bool m_fretConflict = false; // used by TAB staves to mark a fretting conflict: diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 2fcd8b10808f0..e60f4bfea9d97 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1992,12 +1992,28 @@ void ChordLayout::calculateChordOffsets(Segment* segment, staff_idx_t staffIdx, // thus user can force notes to be shared despite differing number of dots or either being stemless // by setting one of the notehead types to match the other or by making one notehead invisible // TODO: consider adding a style option, staff properties, or note property to control sharing - if ((nchord->dots() != pchord->dots() || !nchord->stem() || !pchord->stem() || nHeadType != pHeadType + + // check if the notes are unison + bool unisonMatch = (n->pitch() == p->pitch() && n->tpc() == p->tpc()); + + // for unisons we allow half noteheads to merge even with different durations, + // as the stem, flag, or beam makes the duration clear. + // dotted notes can also merge with non-dotted notes at unison. + if (((!unisonMatch && nchord->dots() != pchord->dots()) || !nchord->stem() || !pchord->stem() + || (nHeadType != pHeadType && !unisonMatch) || n->isSmall() || p->isSmall()) && ((n->headType() == NoteHeadType::HEAD_AUTO && p->headType() == NoteHeadType::HEAD_AUTO) || nHeadType != pHeadType) && (n->visible() == p->visible())) { shareHeads = false; + } else if (nHeadType != pHeadType && unisonMatch) { + // ensure if a unison includes an open notehead this is shown in + // preference to a filled notehead + if (nHeadType == NoteHeadType::HEAD_QUARTER) { + n->setHideNotehead(true); + } else if (pHeadType == NoteHeadType::HEAD_QUARTER) { + p->setHideNotehead(true); + } } } @@ -2381,6 +2397,7 @@ double ChordLayout::layoutChords2(std::vector& notes, bool up, LayoutCont // by default, notes and dots are not hidden // this may be changed later to allow unisons to share noteheads note->setHidden(false); + note->setHideNotehead(false); note->setDotsHidden(false); // be sure chord position is initialized diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index 7a458497d5228..3bd702ae5832f 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -4137,7 +4137,7 @@ void TLayout::layoutNote(const Note* item, Note::LayoutData* ldata) const_cast(item)->setHeadGroup(NoteHeadGroup::HEAD_DIAMOND); } - SymId nh = item->noteHead(); + SymId nh = item->hideNotehead() ? SymId::noSym : item->noteHead(); ldata->cachedNoteheadSym.set_value(nh); if (item->isNoteName()) {