From 7e356bac2a1456db8f17b9e47ad31cc053262115 Mon Sep 17 00:00:00 2001 From: songzhenke Date: Mon, 18 Aug 2025 21:33:24 +1000 Subject: [PATCH 01/24] Support jianpu: up noteonly with duration line, octave dot and rest --- src/engraving/dom/chord.cpp | 47 ++++- src/engraving/dom/chord.h | 14 ++ src/engraving/dom/dom.cmake | 4 + src/engraving/dom/durationline.cpp | 80 +++++++ src/engraving/dom/durationline.h | 76 +++++++ src/engraving/dom/factory.cpp | 4 + src/engraving/dom/note.cpp | 3 +- src/engraving/dom/note.h | 4 + src/engraving/dom/octavedot.cpp | 94 +++++++++ src/engraving/dom/octavedot.h | 72 +++++++ src/engraving/dom/staff.cpp | 9 + src/engraving/dom/staff.h | 1 + src/engraving/dom/stafftype.cpp | 6 + src/engraving/dom/stafftype.h | 6 +- src/engraving/rendering/score/beamlayout.cpp | 5 + src/engraving/rendering/score/chordlayout.cpp | 198 +++++++++++++++++- src/engraving/rendering/score/chordlayout.h | 3 + src/engraving/rendering/score/restlayout.cpp | 5 +- .../rendering/score/slurtielayout.cpp | 31 ++- src/engraving/rendering/score/tdraw.cpp | 58 ++++- src/engraving/rendering/score/tdraw.h | 5 +- src/engraving/rendering/score/tlayout.cpp | 46 ++++ src/engraving/rendering/score/tlayout.h | 3 + src/engraving/types/types.h | 2 + src/engraving/types/typesconv.cpp | 6 + 25 files changed, 760 insertions(+), 22 deletions(-) create mode 100644 src/engraving/dom/durationline.cpp create mode 100644 src/engraving/dom/durationline.h create mode 100644 src/engraving/dom/octavedot.cpp create mode 100644 src/engraving/dom/octavedot.h diff --git a/src/engraving/dom/chord.cpp b/src/engraving/dom/chord.cpp index 12fd9b026adc..811ce30332f4 100644 --- a/src/engraving/dom/chord.cpp +++ b/src/engraving/dom/chord.cpp @@ -39,6 +39,7 @@ #include "chordline.h" #include "dom/parenthesis.h" #include "drumset.h" +#include "durationline.h" #include "factory.h" #include "guitarbend.h" #include "hook.h" @@ -51,6 +52,7 @@ #include "notedot.h" #include "noteevent.h" #include "noteline.h" +#include "octavedot.h" #include "ornament.h" #include "part.h" #include "rest.h" @@ -475,7 +477,9 @@ Chord::~Chord() delete m_stemSlash; delete m_stem; delete m_hook; + muse::DeleteAll(m_durationLines); muse::DeleteAll(m_ledgerLines); + muse::DeleteAll(m_octaveDots); muse::DeleteAll(m_graceNotes); muse::DeleteAll(m_notes); muse::DeleteAll(m_noteParens); @@ -1029,21 +1033,37 @@ Chord* Chord::next() const return nullptr; } -void Chord::resizeLedgerLinesTo(size_t newSize) +template +void Chord::resizeTo(std::vector& vec, size_t newSize) { - int ledgerLineCountDiff = static_cast(newSize - m_ledgerLines.size()); - if (ledgerLineCountDiff > 0) { - for (int i = 0; i < ledgerLineCountDiff; ++i) { - m_ledgerLines.push_back(new LedgerLine(score()->dummy())); + int diff = static_cast(newSize - vec.size()); + if (diff > 0) { + for (int i = 0; i < diff; ++i) { + vec.push_back(new T(score()->dummy())); } } else { - for (int i = 0; i < std::abs(ledgerLineCountDiff); ++i) { - delete m_ledgerLines.back(); - m_ledgerLines.pop_back(); + for (int i = 0; i < std::abs(diff); ++i) { + delete vec.back(); + vec.pop_back(); } } - assert(m_ledgerLines.size() == newSize); + assert(vec.size() == newSize); +} + +void Chord::resizeDurationLinesTo(size_t newSize) +{ + resizeTo(m_durationLines, newSize); +} + +void Chord::resizeLedgerLinesTo(size_t newSize) +{ + resizeTo(m_ledgerLines, newSize); +} + +void Chord::resizeOctaveDotsTo(size_t newSize) +{ + resizeTo(m_octaveDots, newSize); } void Chord::setBeamExtension(double extension) @@ -1065,7 +1085,8 @@ bool Chord::shouldHaveStem() const && !(durationType().type() == DurationType::V_HALF && staffType && staffType->isTabStaff() && staffType->minimStyle() == TablatureMinimStyle::NONE) && !(measure() && measure()->stemless(staffIdx())) - && !(staffType && staffType->isTabStaff() && staffType->stemless()); + && !(staffType && staffType->isTabStaff() && staffType->stemless()) + && !(staff && staff->isJianpuStaff(tick())); } bool Chord::shouldHaveHook() const @@ -1315,6 +1336,12 @@ void Chord::scanElements(std::function func) for (Note* note : m_notes) { note->scanElements(func); } + for (DurationLine* dl : m_durationLines) { + func(dl); + } + for (OctaveDot* dot : m_octaveDots) { + func(dot); + } for (Chord* chord : m_graceNotes) { chord->scanElements(func); } diff --git a/src/engraving/dom/chord.h b/src/engraving/dom/chord.h index 8f4fadec461c..28e028c1f29f 100644 --- a/src/engraving/dom/chord.h +++ b/src/engraving/dom/chord.h @@ -40,10 +40,12 @@ namespace mu::engraving { class AccidentalState; class Arpeggio; class Chord; +class DurationLine; class Hook; class LedgerLine; class Note; class NoteEventList; +class OctaveDot; class Stem; class StemSlash; class TremoloTwoChord; @@ -166,10 +168,18 @@ class Chord final : public ChordRest bool isUiItem() const { return m_isUiItem; } void setIsUiItem(bool val) { m_isUiItem = val; } + const std::vector& durationLines() const { return m_durationLines; } + std::vector& durationLines() { return m_durationLines; } + void resizeDurationLinesTo(size_t newSize); + const std::vector& ledgerLines() const { return m_ledgerLines; } std::vector& ledgerLines() { return m_ledgerLines; } void resizeLedgerLinesTo(size_t newSize); + const std::vector& octaveDots() const { return m_octaveDots; } + std::vector& octaveDots() { return m_octaveDots; } + void resizeOctaveDotsTo(size_t newSize); + double defaultStemLength() const { return m_defaultStemLength; } void setDefaultStemLength(double l) { m_defaultStemLength = l; } void setBeamExtension(double extension); @@ -391,8 +401,12 @@ class Chord final : public ChordRest // `includeTemporarySiblings`: whether items that are deleted & recreated during every layout should also be processed void processSiblings(std::function func, bool includeTemporarySiblings) const; + template void resizeTo(std::vector& vec, size_t newSize); + std::vector m_notes; // sorted to decreasing line step + std::vector m_durationLines; std::vector m_ledgerLines; + std::vector m_octaveDots; NoteParenthesisInfoList m_noteParens; Stem* m_stem = nullptr; diff --git a/src/engraving/dom/dom.cmake b/src/engraving/dom/dom.cmake index 42438dff83dc..145c3337df2d 100644 --- a/src/engraving/dom/dom.cmake +++ b/src/engraving/dom/dom.cmake @@ -82,6 +82,8 @@ set(DOM_SRC ${CMAKE_CURRENT_LIST_DIR}/drumset.h ${CMAKE_CURRENT_LIST_DIR}/durationelement.cpp ${CMAKE_CURRENT_LIST_DIR}/durationelement.h + ${CMAKE_CURRENT_LIST_DIR}/durationline.cpp + ${CMAKE_CURRENT_LIST_DIR}/durationline.h ${CMAKE_CURRENT_LIST_DIR}/durationtype.cpp ${CMAKE_CURRENT_LIST_DIR}/durationtype.h ${CMAKE_CURRENT_LIST_DIR}/dynamic.cpp @@ -214,6 +216,8 @@ set(DOM_SRC ${CMAKE_CURRENT_LIST_DIR}/noteline.cpp ${CMAKE_CURRENT_LIST_DIR}/noteline.h ${CMAKE_CURRENT_LIST_DIR}/notifier.h + ${CMAKE_CURRENT_LIST_DIR}/octavedot.cpp + ${CMAKE_CURRENT_LIST_DIR}/octavedot.h ${CMAKE_CURRENT_LIST_DIR}/ornament.cpp ${CMAKE_CURRENT_LIST_DIR}/ornament.h ${CMAKE_CURRENT_LIST_DIR}/ottava.cpp diff --git a/src/engraving/dom/durationline.cpp b/src/engraving/dom/durationline.cpp new file mode 100644 index 000000000000..6ee0f5a2d157 --- /dev/null +++ b/src/engraving/dom/durationline.cpp @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited + * + * 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 . + */ + +#include "durationline.h" + +#include "chord.h" +#include "measure.h" +#include "system.h" + +#include "log.h" + +using namespace mu; + +namespace mu::engraving { +//--------------------------------------------------------- +// DurationLine +//--------------------------------------------------------- + +DurationLine::DurationLine(EngravingItem* s) + : EngravingItem(ElementType::DURATION_LINE, s) +{ + setSelectable(false); + m_len = 0.; +} + +DurationLine::~DurationLine() +{ +} + +//--------------------------------------------------------- +// pagePos +//--------------------------------------------------------- + +PointF DurationLine::pagePos() const +{ + System* system = chord()->measure()->system(); + double yp = y() + system->staff(staffIdx())->y() + system->y(); + return PointF(pageX(), yp); +} + +//--------------------------------------------------------- +// measureXPos +//--------------------------------------------------------- + +double DurationLine::measureXPos() const +{ + double xp = x(); // chord relative + xp += chord()->x(); // segment relative + xp += chord()->segment()->x(); // measure relative + return xp; +} + +//--------------------------------------------------------- +// spatiumChanged +//--------------------------------------------------------- + +void DurationLine::spatiumChanged(double oldValue, double newValue) +{ + m_len = (m_len / oldValue) * newValue; +} +} diff --git a/src/engraving/dom/durationline.h b/src/engraving/dom/durationline.h new file mode 100644 index 000000000000..d7778e9a24b7 --- /dev/null +++ b/src/engraving/dom/durationline.h @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited + * + * 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 . + */ + +#ifndef MU_ENGRAVING_DURATIONLINE_H +#define MU_ENGRAVING_DURATIONLINE_H + +#include "engravingitem.h" + +namespace mu::engraving { +class Chord; + +//--------------------------------------------------------- +// @@ DurationLine +/// Graphic representation of a duration line. +//! +//! parent: Chord +//! x-origin: Chord +//! y-origin: SStaff +//--------------------------------------------------------- + +class DurationLine final : public EngravingItem +{ + OBJECT_ALLOCATOR(engraving, DurationLine) + DECLARE_CLASSOF(ElementType::DURATION_LINE) + +public: + DurationLine(EngravingItem*); + ~DurationLine(); + DurationLine& operator=(const DurationLine&) = delete; + + DurationLine* clone() const override { return new DurationLine(*this); } + + PointF pagePos() const override; ///< position in page coordinates + Chord* chord() const { return toChord(explicitParent()); } + + double len() const { return m_len; } + void setLen(double v) { m_len = v; } + + void setHalving(bool v) { m_halving = v; } + bool halving() const { return m_halving; } + + double measureXPos() const; + + void spatiumChanged(double /*oldValue*/, double /*newValue*/) override; + + struct LayoutData : public EngravingItem::LayoutData { + double lineWidth = 0.0; + }; + DECLARE_LAYOUTDATA_METHODS(DurationLine); + +private: + + double m_len = 0.0; + bool m_halving = true; +}; +} // namespace mu::engraving +#endif diff --git a/src/engraving/dom/factory.cpp b/src/engraving/dom/factory.cpp index bca4c75dd032..9a63ca1bb56c 100644 --- a/src/engraving/dom/factory.cpp +++ b/src/engraving/dom/factory.cpp @@ -41,6 +41,7 @@ #include "chordline.h" #include "capo.h" #include "deadslapped.h" +#include "durationline.h" #include "dynamic.h" #include "expression.h" #include "fermata.h" @@ -72,6 +73,7 @@ #include "mmrestrange.h" #include "note.h" #include "noteline.h" +#include "octavedot.h" #include "ornament.h" #include "ottava.h" #include "page.h" @@ -249,6 +251,8 @@ EngravingItem* Factory::doCreateItem(ElementType type, EngravingItem* parent) case ElementType::PARTIAL_TIE: return new PartialTie(parent->isNote() ? toNote(parent) : dummy->note()); case ElementType::PARTIAL_LYRICSLINE: return new PartialLyricsLine(parent); case ElementType::PARENTHESIS: return new Parenthesis(parent); + case ElementType::DURATION_LINE: return new DurationLine(parent->isChord() ? toChord(parent) : dummy->chord()); + case ElementType::OCTAVE_DOT: return new OctaveDot(parent->isChord() ? toChord(parent) : dummy->chord()); case ElementType::TEXTLINE_BASE: case ElementType::TEXTLINE_SEGMENT: diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index 9082f6cdfe7a..f9af21e92698 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -683,7 +683,8 @@ Note::Note(const Note& n, bool link) for (NoteDot* dot : n.m_dots) { add(Factory::copyNoteDot(*dot)); } - m_mark = n.m_mark; + m_mark = n.m_mark; + m_jianpuDigit = n.m_jianpuDigit; setDropTarget(false); } diff --git a/src/engraving/dom/note.h b/src/engraving/dom/note.h index fd1614df6a83..cdc635ad20b4 100644 --- a/src/engraving/dom/note.h +++ b/src/engraving/dom/note.h @@ -264,6 +264,9 @@ class Note final : public EngravingItem bool fretConflict() const { return m_fretConflict; } void setFretConflict(bool val) { m_fretConflict = val; } + String jianpuDigit() const { return m_jianpuDigit; } + void setJianpuDigit(const String& s) { m_jianpuDigit = s; } + void add(EngravingItem*) override; void remove(EngravingItem*) override; @@ -555,6 +558,7 @@ class Note final : public EngravingItem std::vector m_spannerBack; String m_fretString; + String m_jianpuDigit; std::vector m_lineAttachPoints; TieJumpPointList m_jumpPoints { this }; diff --git a/src/engraving/dom/octavedot.cpp b/src/engraving/dom/octavedot.cpp new file mode 100644 index 000000000000..a23cef7c5944 --- /dev/null +++ b/src/engraving/dom/octavedot.cpp @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited + * + * 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 . + */ + +#include "octavedot.h" + +#include "chord.h" +#include "measure.h" +#include "system.h" + +#include "log.h" + +using namespace mu; + +namespace mu::engraving { +//--------------------------------------------------------- +// OctaveDot +//--------------------------------------------------------- + +OctaveDot::OctaveDot(EngravingItem* s) + : EngravingItem(ElementType::OCTAVE_DOT, s) +{ + setSelectable(false); + m_len = 0.; +} + +OctaveDot::~OctaveDot() +{ +} + +//--------------------------------------------------------- +// pagePos +//--------------------------------------------------------- + +PointF OctaveDot::pagePos() const +{ + System* system = chord()->measure()->system(); + double yp = y() + system->staff(staffIdx())->y() + system->y(); + return PointF(pageX(), yp); +} + +//--------------------------------------------------------- +// measureXPos +//--------------------------------------------------------- + +double OctaveDot::measureXPos() const +{ + double xp = x(); // chord relative + xp += chord()->x(); // segment relative + xp += chord()->segment()->x(); // measure relative + return xp; +} + +//--------------------------------------------------------- +// spatiumChanged +//--------------------------------------------------------- + +void OctaveDot::spatiumChanged(double oldValue, double newValue) +{ + m_len = (m_len / oldValue) * newValue; +} + +//--------------------------------------------------------- +// chord +//--------------------------------------------------------- + +Chord* OctaveDot::chord() const +{ + auto* parent = explicitParent(); + if (parent && parent->isChord()) { + return toChord(parent); + } else { + return nullptr; + } +} +} diff --git a/src/engraving/dom/octavedot.h b/src/engraving/dom/octavedot.h new file mode 100644 index 000000000000..4c2446b33ad6 --- /dev/null +++ b/src/engraving/dom/octavedot.h @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited + * + * 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 . + */ + +#ifndef MU_ENGRAVING_OCTAVEDOT_H +#define MU_ENGRAVING_OCTAVEDOT_H + +#include "engravingitem.h" + +namespace mu::engraving { +class Chord; + +//--------------------------------------------------------- +// @@ OctaveDot +/// Graphic representation of a dot abover or under jianpu note. +//! +//! parent: Chord +//! x-origin: Chord +//! y-origin: SStaff +//--------------------------------------------------------- + +class OctaveDot final : public EngravingItem +{ + OBJECT_ALLOCATOR(engraving, OctaveDot) + DECLARE_CLASSOF(ElementType::OCTAVE_DOT) + +public: + OctaveDot(EngravingItem*); + ~OctaveDot(); + OctaveDot& operator=(const OctaveDot&) = delete; + + OctaveDot* clone() const override { return new OctaveDot(*this); } + + PointF pagePos() const override; ///< position in page coordinates + Chord* chord() const; + + double len() const { return m_len; } + void setLen(double v) { m_len = v; } + + double measureXPos() const; + + void spatiumChanged(double /*oldValue*/, double /*newValue*/) override; + + struct LayoutData : public EngravingItem::LayoutData { + double radius = 0.0; + }; + DECLARE_LAYOUTDATA_METHODS(OctaveDot); + +private: + + double m_len = 0.0; +}; +} // namespace mu::engraving +#endif diff --git a/src/engraving/dom/staff.cpp b/src/engraving/dom/staff.cpp index 8cac93b1f761..70d4e1627860 100644 --- a/src/engraving/dom/staff.cpp +++ b/src/engraving/dom/staff.cpp @@ -1928,6 +1928,15 @@ bool Staff::isDrumStaff(const Fraction& tick) const return part()->instrument(tick)->useDrumset(); } +//--------------------------------------------------------- +// isJianpuStaff +//--------------------------------------------------------- + +bool Staff::isJianpuStaff(const Fraction& tick) const +{ + return staffType(tick)->type() == StaffTypes::JIANPU; +} + //--------------------------------------------------------- // lines //--------------------------------------------------------- diff --git a/src/engraving/dom/staff.h b/src/engraving/dom/staff.h index 80b398f3ceb1..72b1e58a5428 100644 --- a/src/engraving/dom/staff.h +++ b/src/engraving/dom/staff.h @@ -185,6 +185,7 @@ class Staff final : public EngravingItem bool isPitchedStaff(const Fraction&) const; bool isTabStaff(const Fraction&) const; bool isDrumStaff(const Fraction&) const; + bool isJianpuStaff(const Fraction&) const; int lines(const Fraction&) const; void setLines(const Fraction&, int lines); diff --git a/src/engraving/dom/stafftype.cpp b/src/engraving/dom/stafftype.cpp index 2acdee3abf2d..3819ecfc01b1 100644 --- a/src/engraving/dom/stafftype.cpp +++ b/src/engraving/dom/stafftype.cpp @@ -99,6 +99,10 @@ StaffType::StaffType(StaffGroup sg, const String& xml, const String& name, int l m_genTimesig(genTimeSig), m_genKeysig(genKeySig) { + // Jianpu temporarly uses TAB fret font + if (xml == u"stdJianpu") { + setFretTextStyle(TextStyleType::TAB_FRET_NUMBER); + } } StaffType::StaffType(StaffGroup sg, const String& xml, const String& name, int lines, int stpOff, double lineDist, @@ -233,6 +237,7 @@ StaffTypes StaffType::type() const { static const std::map xmlNameToType { { u"stdNormal", StaffTypes::STANDARD }, + { u"stdJianpu", StaffTypes::JIANPU }, { u"perc1Line", StaffTypes::PERC_1LINE }, { u"perc2Line", StaffTypes::PERC_2LINE }, @@ -1126,6 +1131,7 @@ void StaffType::initStaffTypes(const Color& defaultColor) m_presets = { // group, xml-name, human-readable-name, lin stpOff dist clef bars stmless time key ledger invis color StaffType(StaffGroup::STANDARD, u"stdNormal", muse::mtrc("engraving", "Standard"), 5, 0, 1, true, true, false, true, true, true, false, defaultColor), + StaffType(StaffGroup::STANDARD, u"stdJianpu", muse::mtrc("engraving", "Jianpu"), 0, 0, 1, true, true, false, true, true, true, false, defaultColor), StaffType(StaffGroup::PERCUSSION, u"perc1Line", muse::mtrc("engraving", "Perc. 1 line"), 1, 0, 1, true, true, false, true, false, true, false, defaultColor), StaffType(StaffGroup::PERCUSSION, u"perc2Line", muse::mtrc("engraving", "Perc. 2 lines"), 2, 0, 1, true, true, false, true, false, true, false, defaultColor), StaffType(StaffGroup::PERCUSSION, u"perc3Line", muse::mtrc("engraving", "Perc. 3 lines"), 3, 0, 1, true, true, false, true, false, true, false, defaultColor), diff --git a/src/engraving/dom/stafftype.h b/src/engraving/dom/stafftype.h index 4574aec3c8f6..7a01a5594fa0 100644 --- a/src/engraving/dom/stafftype.h +++ b/src/engraving/dom/stafftype.h @@ -119,7 +119,7 @@ struct TablatureDurationFont { // keep in sync with the _presets initialization in StaffType::initStaffTypes() and _defaultPreset enum class StaffTypes : signed char { - STANDARD, + STANDARD, JIANPU, PERC_1LINE, PERC_2LINE, PERC_3LINE, PERC_5LINE, TAB_6SIMPLE, TAB_6COMMON, TAB_6FULL, TAB_4SIMPLE, TAB_4COMMON, TAB_4FULL, @@ -303,6 +303,10 @@ class StaffType void setFretPresetIdx(size_t idx); void setFretPreset(const String& str); + const muse::draw::Font& jianpuFont() const { return m_fretFont; } // Jianpu temporarly uses fret font + double jianpuFontSize() const { return fretFontSize(); } // Jianpu temporarly uses fret font + double jianpuBoxH() const { return fretBoxH(); } // Jianpu temporarly uses fret font + bool isTabStaff() const { return m_group == StaffGroup::TAB; } bool isDrumStaff() const { return m_group == StaffGroup::PERCUSSION; } diff --git a/src/engraving/rendering/score/beamlayout.cpp b/src/engraving/rendering/score/beamlayout.cpp index d243d535eaca..f46c69e9e8af 100644 --- a/src/engraving/rendering/score/beamlayout.cpp +++ b/src/engraving/rendering/score/beamlayout.cpp @@ -60,6 +60,11 @@ void BeamLayout::layout(Beam* item, const LayoutContext& ctx) { TRACEFUNC; + const Staff* staff = item->staff(); + if (staff->isJianpuStaff(item->tick())) { + return; + } + Beam::LayoutData* ldata = item->mutldata(); // all of the beam layout code depends on _elements being in order by tick // this may not be the case if two cr's were recently swapped. diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 7a39e31a66da..48faa17419aa 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -30,6 +30,7 @@ #include "dom/chordbracket.h" #include "dom/beam.h" #include "dom/chord.h" +#include "dom/durationline.h" #include "dom/factory.h" #include "dom/fingering.h" #include "dom/glissando.h" @@ -40,6 +41,7 @@ #include "dom/measure.h" #include "dom/navigate.h" #include "dom/note.h" +#include "dom/octavedot.h" #include "dom/ornament.h" #include "dom/page.h" #include "dom/parenthesis.h" @@ -129,7 +131,15 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) // https://github.com/musescore/MuseScore/issues/8970 std::vector chordAccidentals; + Note* upNote = item->upNote(); + const bool isJianpuStaff = item->staff() && item->staff()->isJianpuStaff(item->tick()); + for (Note* note : item->notes()) { + if (isJianpuStaff && note != upNote) { + // Jianpu only shows the up note + continue; + } + TLayout::layoutNote(note, note->mutldata()); double x1 = note->pos().x() + chordX; double x2 = x1 + note->headWidth(); @@ -1293,6 +1303,7 @@ void ChordLayout::updateLedgerLines(Chord* item, LayoutContext& ctx) double lineDistance = 1; bool staffVisible = true; int stepOffset = 0; // for staff type changes with a step offset + bool isJianpu = false; Segment* segment = item->segment(); @@ -1305,10 +1316,11 @@ void ChordLayout::updateLedgerLines(Chord* item, LayoutContext& ctx) lineDistance = st->lineDistance(tick); staffVisible = !st->isLinesInvisible(tick); stepOffset = st->staffType(tick)->stepOffset(); + isJianpu = st->isJianpuStaff(tick); } // need ledger lines? - if (item->downLine() + stepOffset <= lineBelow + 1 && item->upLine() + stepOffset >= -1) { + if (isJianpu || (item->downLine() + stepOffset <= lineBelow + 1 && item->upLine() + stepOffset >= -1)) { muse::DeleteAll(item->ledgerLines()); item->ledgerLines().clear(); return; @@ -1642,6 +1654,182 @@ int ChordLayout::computeAutoStemDirection(const std::vector& noteDistances) return 0; } +void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) +{ + Segment* segment = item->segment(); + if (!segment) { + return; // no segment, nothing to do + } + Fraction tick = segment->tick(); + staff_idx_t idx = item->staffIdx() + item->staffMove(); + track_idx_t track = staff2track(idx); + const Staff* st = ctx.dom().staff(idx); + bool staffVisible = !st->isLinesInvisible(tick); + bool isJianpu = st->isJianpuStaff(tick); + + int lines = 0; + TDuration durationType = item->durationType(); + if (durationType == DurationType::V_LONG) { + lines = -15; + } else if (durationType == DurationType::V_BREVE) { + lines = -7; + } else if (durationType == DurationType::V_WHOLE) { + lines = -3; + } else if (durationType == DurationType::V_HALF) { + lines = -1; + } else if (durationType == DurationType::V_QUARTER) { + lines = 0; + } else if (durationType == DurationType::V_EIGHTH) { + lines = 1; + } else if (durationType == DurationType::V_16TH) { + lines = 2; + } else if (durationType == DurationType::V_32ND) { + lines = 3; + } else if (durationType == DurationType::V_64TH) { + lines = 4; + } else if (durationType == DurationType::V_128TH) { + lines = 5; + } else if (durationType == DurationType::V_256TH) { + lines = 6; + } else if (durationType == DurationType::V_512TH) { + lines = 7; + } else if (durationType == DurationType::V_1024TH) { + lines = 8; + } + + // need duration lines? + if (!isJianpu || lines == 0) { + muse::DeleteAll(item->durationLines()); + item->durationLines().clear(); + return; + } + + if (const Note* note = item->upNote(); note) { + double hw = note->headWidth(); + double hx = note->pos().x() + note->bboxXShift(); + double maxX = hx + hw; + + size_t prevLines = 0; // Number of duration lines in the previous chord + double prevOffsetX = .0; // Offset of the previous chord + bool first = true; + if (item->isChordRest()) { + Beam* beam = toChordRest(item)->beam(); + if (beam) { + ChordRest* cr = beam->elements().front(); + first = (cr && cr->isChord() && toChord(cr) == item); + } + } + + if (Chord* prev = item->prev(); prev&& !first) { + if (prev->durationLines().size() > 0 && prev->durationLines().back()->halving()) { + prevLines = prev->durationLines().size(); + prevOffsetX = item->pagePos().x() - prev->pagePos().x() - hw; + } + } + + double distance = item->spatium() * .3; + item->resizeDurationLinesTo(std::abs(lines)); + for (int i = 0; i < lines; ++i) { + DurationLine* dl = item->durationLines()[i]; + dl->setParent(item); + dl->setTrack(track); + dl->setVisible(staffVisible); + if (i >= static_cast(prevLines)) { + prevOffsetX = 0.0; // Do not join to duration line in previous chord + } + double minX = hx - prevOffsetX; // extend to join to previous duration line + dl->setLen(maxX - minX); + dl->setPos(minX, (i + 1) * distance); + dl->setHalving(true); // Halving duration + } + + if (lines < 0) { // If we have negative lines, we need to lengthen the duration lines + const StaffType* jianpu = st->staffTypeForElement(item); + double height = jianpu->jianpuBoxH() * item->magS(); + + for (int i = 0; i < -lines; ++i) { + DurationLine* dl = item->durationLines()[i]; + dl->setParent(item); + dl->setTrack(track); + dl->setVisible(staffVisible); + dl->setLen(hw); + dl->setPos(hx + hw * 1.2 * (i + 1), -height * .5); + dl->setHalving(false); // Lengthening duration + } + } + } + + for (DurationLine* dl : item->durationLines()) { + TLayout::layoutDurationLine(dl, ctx); + } +} + +void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) +{ + Segment* segment = item->segment(); + if (!segment) { + return; // no segment, nothing to do + } + Fraction tick = segment->tick(); + staff_idx_t idx = item->staffIdx() + item->staffMove(); + track_idx_t track = staff2track(idx); + const Staff* st = ctx.dom().staff(idx); + bool staffVisible = !st->isLinesInvisible(tick); + bool isJianpu = st->isJianpuStaff(tick); + int baseOctave = 3; // Default base octave for Jianpu + + auto cleanupOctaveDots = [item]() { + muse::DeleteAll(item->octaveDots()); + item->octaveDots().clear(); + }; + + // need octave dots? + if (!isJianpu) { + cleanupOctaveDots(); + return; + } + + if (const Note* note = item->upNote(); note) { + int dots = 0; + double offsetY = 0; + double distance = item->spatium() * .3; + int octave = note->octave(); + if (octave > baseOctave) { + dots = octave - baseOctave; + const StaffType* jianpu = st->staffTypeForElement(item); + double height = jianpu->jianpuBoxH() * item->magS(); + offsetY = -(height + dots * distance); + } else if (octave < baseOctave) { + dots = baseOctave - octave; + offsetY = distance; + if (item->durationLines().size() > 0) { + offsetY += item->durationLines().back()->pos().y() + item->durationLines().back()->height() * .5; + } + } else { + cleanupOctaveDots(); + return; + } + + double hw = note->headWidth(); + double minX = note->pos().x() + note->bboxXShift(); + double maxX = minX + hw; + + item->resizeOctaveDotsTo(dots); + for (int i = 0; i < dots; ++i) { + OctaveDot* dot = item->octaveDots()[i]; + dot->setParent(item); + dot->setTrack(track); + dot->setVisible(staffVisible); + dot->setLen(maxX - minX); + dot->setPos(minX, offsetY + i * distance); + } + } + + for (OctaveDot* od : item->octaveDots()) { + TLayout::layoutOctaveDot(od); + } +} + //--------------------------------------------------------- // layoutSegmentElements //--------------------------------------------------------- @@ -2291,6 +2479,8 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ for (Chord* grace : chord->graceNotes()) { AccidentalsLayout::layoutAccidentals({ grace }, ctx); } + layoutDurationLines(chord, ctx); + layoutOctaveDots(chord, ctx); } } @@ -2697,6 +2887,8 @@ void ChordLayout::layoutChords3(const std::vector& chords, // Track dot position of voices on this stave and possible cross voices from above & below std::array dotPos{}; + bool isJianpu = staff->isJianpuStaff(tick); + int nNotes = int(notes.size()); for (int i = nNotes - 1; i >= 0; --i) { @@ -2706,7 +2898,7 @@ void ChordLayout::layoutChords3(const std::vector& chords, if (chord->dots()) { if (dotPosition == DirectionV::AUTO && nNotes > 1 && note->visible() && !note->dotsHidden()) { // resolve dot conflicts - int line = note->line(); + int line = isJianpu ? 0 : note->line(); Note* above = (i < nNotes - 1) ? notes[i + 1] : nullptr; if (above && (!above->visible() || above->dotsHidden() || above->chord()->dots() == 0 @@ -2792,7 +2984,7 @@ void ChordLayout::layoutChords3(const std::vector& chords, noteX = chord->noteHeadWidth() - note->headBodyWidth(); } - double ny = (note->line() + stepOffset) * stepDistance; + double ny = (isJianpu ? 0 : note->line() + stepOffset) * stepDistance; if (note->ldata()->pos().y() != ny) { note->mutldata()->setPosY(ny); if (stem) { diff --git a/src/engraving/rendering/score/chordlayout.h b/src/engraving/rendering/score/chordlayout.h index b57cfe3b265e..4ef6092549d5 100644 --- a/src/engraving/rendering/score/chordlayout.h +++ b/src/engraving/rendering/score/chordlayout.h @@ -157,6 +157,9 @@ class ChordLayout static void updateLedgerLines(Chord* item, LayoutContext& ctx); + static void layoutDurationLines(Chord* item, LayoutContext& ctx); + static void layoutOctaveDots(Chord* item, LayoutContext& ctx); + static ChordPosInfo calculateChordPosInfo(Segment* segment, staff_idx_t staffIdx, track_idx_t partStartTrack, track_idx_t partEndTrack, LayoutContext& ctx); static void calculateMaxNoteWidths(ChordPosInfo& posInfo, const Fraction& tick, const Staff* staff, LayoutContext& ctx); diff --git a/src/engraving/rendering/score/restlayout.cpp b/src/engraving/rendering/score/restlayout.cpp index ee2c5fbfac8d..31e7ed2d900e 100644 --- a/src/engraving/rendering/score/restlayout.cpp +++ b/src/engraving/rendering/score/restlayout.cpp @@ -79,6 +79,7 @@ void RestLayout::layoutRest(const Rest* item, Rest::LayoutData* ldata, const Lay double spatium = item->spatium(); ldata->setPosX(0.0); + const Staff* stf = item->staff(); const StaffType* stt = item->staffType(); if (stt && stt->isTabStaff()) { // if rests are shown and note values are shown as duration symbols @@ -112,12 +113,14 @@ void RestLayout::layoutRest(const Rest* item, Rest::LayoutData* ldata, const Lay delete item->tabDur(); const_cast(item)->setTabDur(nullptr); } + } else if (stf && stf->isJianpuStaff(item->measure()->ticks())) { + ldata->setPos(0.0, 0.0); + return; } const_cast(item)->setDotLine(Rest::getDotline(item->durationType().type())); double yOff = item->offset().y(); - const Staff* stf = item->staff(); const StaffType* st = stf ? stf->staffTypeForElement(item) : 0; double lineDist = st ? st->lineDistance().val() : 1.0; int userLine = RealIsNull(yOff) ? 0 : lrint(yOff / (lineDist * spatium)); diff --git a/src/engraving/rendering/score/slurtielayout.cpp b/src/engraving/rendering/score/slurtielayout.cpp index cfb54528dadb..02a921180973 100644 --- a/src/engraving/rendering/score/slurtielayout.cpp +++ b/src/engraving/rendering/score/slurtielayout.cpp @@ -39,6 +39,7 @@ #include "dom/measure.h" #include "dom/guitarbend.h" #include "dom/laissezvib.h" +#include "dom/octavedot.h" #include "dom/parenthesis.h" #include "dom/partialtie.h" @@ -383,6 +384,7 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) } bool useTablature = item->staff() && item->staff()->isTabStaff(item->endCR()->tick()); + bool useJianpu = item->staff() && item->staff()->isJianpuStaff(item->endCR()->tick()); bool staffHasStems = true; // assume staff uses stems const StaffType* stt = 0; if (useTablature) { @@ -395,17 +397,28 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) ChordRest* ecr = item->endCR(); Chord* sc = 0; Note* note1 = 0; + double octDotY1 = 0.0; if (scr->isChord()) { sc = toChord(scr); - note1 = item->up() ? sc->upNote() : sc->downNote(); + note1 = item->up() || useJianpu ? sc->upNote() : sc->downNote(); + if (!sc->octaveDots().empty()) { + octDotY1 = sc->octaveDots().front()->ldata()->pos().y(); + } } Chord* ec = 0; Note* note2 = 0; + double octDotY2 = 0.0; if (ecr->isChord()) { ec = toChord(ecr); - note2 = item->up() ? ec->upNote() : ec->downNote(); + note2 = item->up() || useJianpu ? ec->upNote() : ec->downNote(); + if (!ec->octaveDots().empty()) { + octDotY2 = ec->octaveDots().front()->ldata()->pos().y(); + } } + // Slur for jianpu is always horizontal above dots + double octDotY = std::min(0.0, std::min(octDotY1, octDotY2)); + sp->system1 = scr->measure()->system(); sp->system2 = ecr->measure()->system(); @@ -566,7 +579,9 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) // default positions po.rx() = hw1 * .5 + (note1 ? note1->bboxXShift() : 0.0); - if (note1) { + if (useJianpu) { + po.ry() = scr->ldata()->bbox().top() + octDotY; + } else if (note1) { po.ry() = note1->pos().y(); } else if (item->up()) { po.ry() = scr->ldata()->bbox().top(); @@ -702,7 +717,9 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) if (sa2 == SlurAnchor::NONE) { // default positions po.rx() = hw2 * .5 + (note2 ? note2->bboxXShift() : 0.0); - if (note2) { + if (useJianpu) { + po.ry() = scr->ldata()->bbox().top() + octDotY; + } else if (note2) { po.ry() = note2->pos().y(); } else if (item->up()) { po.ry() = item->endCR()->ldata()->bbox().top(); @@ -2604,6 +2621,12 @@ void SlurTieLayout::computeUp(Slur* slur, LayoutContext& ctx) break; } + if (chord1 && chord1->staff()->isJianpuStaff(chord1->tick())) { + // Jianpu slurs always go up + slur->setUp(true); + break; + } + slur->setUp(!(chordRest1->up())); // Check if multiple voices diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index feae5cd5699b..2255ce1f1819 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -51,6 +51,7 @@ #include "dom/capo.h" #include "dom/deadslapped.h" +#include "dom/durationline.h" #include "dom/dynamic.h" #include "dom/expression.h" @@ -97,6 +98,7 @@ #include "dom/notedot.h" #include "dom/noteline.h" +#include "dom/octavedot.h" #include "dom/ornament.h" #include "dom/ottava.h" @@ -220,6 +222,8 @@ void TDraw::drawItem(const EngravingItem* item, Painter* painter, const PaintOpt case ElementType::DEAD_SLAPPED: draw(item_cast(item), painter, opt); break; + case ElementType::DURATION_LINE: draw(item_cast(item), painter, opt); + break; case ElementType::DYNAMIC: draw(item_cast(item), painter, opt); break; @@ -312,6 +316,8 @@ void TDraw::drawItem(const EngravingItem* item, Painter* painter, const PaintOpt case ElementType::NOTELINE_SEGMENT: draw(item_cast(item), painter, opt); break; + case ElementType::OCTAVE_DOT: draw(item_cast(item), painter, opt); + break; case ElementType::ORNAMENT: draw(item_cast(item), painter, opt); break; case ElementType::OTTAVA_SEGMENT: draw(item_cast(item), painter, opt); @@ -1243,6 +1249,15 @@ void TDraw::draw(const Dynamic* item, Painter* painter, const PaintOptions& opt) drawTextBase(item, painter, opt); } +void TDraw::draw(const DurationLine* item, Painter* painter, const PaintOptions& opt) +{ + TRACE_DRAW_ITEM; + + const DurationLine::LayoutData* ldata = item->ldata(); + painter->setPen(Pen(item->curColor(opt), ldata->lineWidth, PenStyle::SolidLine, PenCapStyle::FlatCap)); + painter->drawLine(LineF(0.0, 0.0, item->len(), 0.0)); +} + void TDraw::draw(const Expression* item, Painter* painter, const PaintOptions& opt) { drawTextBase(item, painter, opt); @@ -2346,6 +2361,7 @@ void TDraw::draw(const Note* item, Painter* painter, const PaintOptions& opt) const auto config = item->configuration(); const StaffType* staffType = item->staff() ? item->staff()->staffTypeForElement(item) : nullptr; + const bool isJianpu = item->staff() && item->staff()->isJianpuStaff(item->tick()); const bool isTabStaff = staffType && staffType->isTabStaff(); const bool negativeFret = isTabStaff && item->negativeFretUsed(); const bool useCriticalColor = negativeFret && !item->deadNote() && !opt.isPrinting; @@ -2377,7 +2393,21 @@ void TDraw::draw(const Note* item, Painter* painter, const PaintOptions& opt) painter->drawText(PointF(startPosX, yOffset * item->magS()), item->fretString()); } // NOT tablature - else { + else if (isJianpu) { + const Staff* staff = item->staff(); + const StaffType* staffType = staff->staffTypeForElement(item); + + Font f(staffType->jianpuFont()); + f.setPointSizeF(f.pointSizeF() * item->magS()); + painter->setFont(f); + + FontMetrics fm(f); + const double fw = fm.width(item->jianpuDigit()) / item->magS(); + const double bw = ldata->bbox().width(); + const double startPosX = ldata->bbox().x() + (bw - fw) * .5; + const double startPosY = ldata->bbox().bottom(); + painter->drawText(PointF(startPosX, startPosY), item->jianpuDigit()); + } else { // skip drawing, if second note of a cross-measure value if (item->chord() && item->chord()->crossMeasure() == CrossMeasure::SECOND) { return; @@ -2444,6 +2474,18 @@ void TDraw::draw(const NoteLineSegment* item, Painter* painter, const PaintOptio drawTextLineBaseSegment(item, painter, opt); } +void TDraw::draw(const OctaveDot* item, Painter* painter, const PaintOptions& opt) +{ + TRACE_DRAW_ITEM; + + double rad = item->ldata()->radius; + double x = item->len() * 0.5; // draw dot in the middle + + painter->setPen(Pen(item->curColor(opt))); + painter->setBrush(Brush(item->curColor(opt))); + painter->drawEllipse(PointF(x, 0), rad, rad); +} + void TDraw::draw(const OttavaSegment* item, Painter* painter, const PaintOptions& opt) { TRACE_DRAW_ITEM; @@ -2544,6 +2586,20 @@ void TDraw::draw(const Rest* item, Painter* painter, const PaintOptions& opt) painter->setPen(item->curColor(opt)); + bool jianpu = item->staff() && item->staff()->isJianpuStaff(item->tick()); + if (jianpu) { + const Staff* staff = item->staff(); + const StaffType* staffType = staff->staffTypeForElement(item); + + Font f(staffType->jianpuFont()); + f.setPointSizeF(f.pointSizeF() * item->magS()); + painter->setFont(f); + painter->setPen(item->curColor(opt)); + double startPosX = ldata->bbox().x(); + painter->drawText(PointF(startPosX, 0), String(u"0")); + return; + } + if (DeadSlapped* ds = item->deadSlapped()) { draw(ds, painter, opt); } else { diff --git a/src/engraving/rendering/score/tdraw.h b/src/engraving/rendering/score/tdraw.h index 57f6ac600a37..9b963e6d0a93 100644 --- a/src/engraving/rendering/score/tdraw.h +++ b/src/engraving/rendering/score/tdraw.h @@ -51,6 +51,7 @@ class Clef; class Capo; class DeadSlapped; +class DurationLine; class Dynamic; class Expression; @@ -107,8 +108,8 @@ class Note; class NoteDot; class NoteLineSegment; +class OctaveDot; class Ornament; - class Ottava; class OttavaSegment; @@ -215,6 +216,7 @@ class TDraw static void draw(const Capo* item, muse::draw::Painter* painter, const PaintOptions& opt); static void draw(const DeadSlapped* item, muse::draw::Painter* painter, const PaintOptions& opt); + static void draw(const DurationLine* item, muse::draw::Painter* painter, const PaintOptions& opt); static void draw(const Dynamic* item, muse::draw::Painter* painter, const PaintOptions& opt); static void draw(const Expression* item, muse::draw::Painter* painter, const PaintOptions& opt); @@ -265,6 +267,7 @@ class TDraw static void draw(const NoteHead* item, muse::draw::Painter* painter, const PaintOptions& opt); static void draw(const NoteLineSegment* item, muse::draw::Painter* painter, const PaintOptions& opt); + static void draw(const OctaveDot* item, muse::draw::Painter* painter, const PaintOptions& opt); static void draw(const Ornament* item, muse::draw::Painter* painter, const PaintOptions& opt); static void draw(const OttavaSegment* item, muse::draw::Painter* painter, const PaintOptions& opt); diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index a32bb5b89109..afb0fd8906df 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -54,6 +54,7 @@ #include "dom/capo.h" #include "dom/deadslapped.h" +#include "dom/durationline.h" #include "dom/dynamic.h" #include "dom/expression.h" @@ -101,6 +102,7 @@ #include "dom/notedot.h" #include "dom/noteline.h" +#include "dom/octavedot.h" #include "dom/ornament.h" #include "dom/ottava.h" @@ -376,6 +378,8 @@ void TLayout::layoutItem(EngravingItem* item, LayoutContext& ctx) case ElementType::ORNAMENT: layoutOrnament(item_cast(item), static_cast(ldata), ctx.conf()); break; + case ElementType::OCTAVE_DOT: layoutOctaveDot(item_cast(item)); + break; case ElementType::OTTAVA: layoutOttava(item_cast(item), ctx); break; case ElementType::OTTAVA_SEGMENT: layoutOttavaSegment(item_cast(item), ctx); @@ -1883,6 +1887,21 @@ void TLayout::layoutDeadSlapped(const DeadSlapped* item, DeadSlapped::LayoutData } } +void TLayout::layoutDurationLine(DurationLine* item, LayoutContext& ctx) +{ + LAYOUT_CALL_ITEM(item); + double chordMag = item->chord()->mag(); + DurationLine::LayoutData* ldata = item->mutldata(); + ldata->setMag(chordMag); + ldata->lineWidth = ctx.conf().styleMM(Sid::staffLineWidth) * chordMag; + if (item->staff()) { + const_cast(item)->setColor(item->staff()->staffType(item->tick())->color()); + } + + double w2 = ldata->lineWidth * .5; + ldata->setBbox(0, -w2, item->len(), 2 * w2); +} + void TLayout::layoutDynamic(Dynamic* item, Dynamic::LayoutData* ldata, const LayoutConfiguration& conf) { DynamicsLayout::layoutDynamic(item, ldata, conf); @@ -4097,6 +4116,7 @@ void TLayout::layoutNote(const Note* item, Note::LayoutData* ldata) } bool useTablature = item->staff() && item->staff()->isTabStaff(item->chord()->tick()); + bool useJianpu = item->staff() && item->staff()->isJianpuStaff(item->chord()->tick()); ldata->useTablature.set_value(useTablature); RectF noteBBox; @@ -4130,6 +4150,21 @@ void TLayout::layoutNote(const Note* item, Note::LayoutData* ldata) double height = item->deadNote() ? tab->deadFretBoxH() : tab->fretBoxH(); noteBBox = RectF(0, y * mags, w, height * mags); + } else if (useJianpu) { + const Staff* st = item->staff(); + const StaffType* jianpu = st->staffTypeForElement(item); + + KeySigEvent ks = st->keySigEvent(item->chord()->tick()); + int tpc = pitch2tpc(item->pitch(), ks.key(), Prefer::NEAREST); + + String accName, stepName; + tpc2Function(tpc, ks.key(), accName, stepName); + const_cast(item)->setJianpuDigit(String(u"%1").arg(stepName)); + + double width = item->headWidth(); + double height = jianpu->jianpuBoxH() * item->magS(); + + noteBBox = RectF(0, -height, width, height); } else { if (item->deadNote()) { const_cast(item)->setHeadGroup(NoteHeadGroup::HEAD_CROSS); @@ -4229,6 +4264,17 @@ void TLayout::layoutNoteDot(const NoteDot* item, NoteDot::LayoutData* ldata) ldata->setBbox(item->symBbox(SymId::augmentationDot)); } +void TLayout::layoutOctaveDot(OctaveDot* item) +{ + LAYOUT_CALL_ITEM(item); + OctaveDot::LayoutData* ldata = item->mutldata(); + ldata->setMag(item->chord()->mag()); + ldata->radius = item->defaultSpatium() * .1; // TODO: style + + double diameter = ldata->radius * 2; + ldata->setBbox(0, -ldata->radius, item->len(), diameter); +} + void TLayout::layoutOrnament(const Ornament* item, Ornament::LayoutData* ldata, const LayoutConfiguration& conf) { LAYOUT_CALL_ITEM(item); diff --git a/src/engraving/rendering/score/tlayout.h b/src/engraving/rendering/score/tlayout.h index 3aa445a74318..377756741cd7 100644 --- a/src/engraving/rendering/score/tlayout.h +++ b/src/engraving/rendering/score/tlayout.h @@ -118,6 +118,7 @@ class BagpipeEmbellishment; class Beam; class Chord; +class DurationLine; class Glissando; class GlissandoSegment; @@ -230,6 +231,7 @@ class TLayout static void layoutCapo(const Capo* item, Capo::LayoutData* ldata, const LayoutContext& ctx); static void layoutDeadSlapped(const DeadSlapped* item, DeadSlapped::LayoutData* ldata); + static void layoutDurationLine(DurationLine* item, LayoutContext& ctx); static void layoutDynamic(Dynamic* item, Dynamic::LayoutData* ldata, const LayoutConfiguration& conf); static void layoutExpression(const Expression* item, Expression::LayoutData* ldata); @@ -295,6 +297,7 @@ class TLayout static void layoutNoteLine(NoteLine* item, LayoutContext& ctx); static void layoutNoteLineSegment(NoteLineSegment* item, LayoutContext& ctx); + static void layoutOctaveDot(OctaveDot* item); static void layoutOrnament(const Ornament* item, Ornament::LayoutData* ldata, const LayoutConfiguration& conf); static void layoutOrnamentCueNote(Ornament* item, LayoutContext& ctx); diff --git a/src/engraving/types/types.h b/src/engraving/types/types.h index 7e86ae6967e1..32cda1f600ff 100644 --- a/src/engraving/types/types.h +++ b/src/engraving/types/types.h @@ -216,6 +216,8 @@ enum class ElementType : unsigned char { TAPPING_HALF_SLUR, TAPPING_HALF_SLUR_SEGMENT, TAPPING_TEXT, + OCTAVE_DOT, + DURATION_LINE, ROOT_ITEM, DUMMY, diff --git a/src/engraving/types/typesconv.cpp b/src/engraving/types/typesconv.cpp index cbb2829b2a51..f57ba48835a5 100644 --- a/src/engraving/types/typesconv.cpp +++ b/src/engraving/types/typesconv.cpp @@ -634,6 +634,12 @@ static const std::array ELEMENT_TYPES { Item{ ElementType::TAPPING_TEXT, "TappingText", TranslatableString("engraving", "tapping text(s)", nullptr, 1), TranslatableString("engraving", "Tapping text(s)", nullptr, 1) }, + Item{ ElementType::OCTAVE_DOT, "OctaveDot", + TranslatableString("engraving", "octave dot"), + TranslatableString("engraving", "Octave dot") }, + Item{ ElementType::DURATION_LINE, "DurationLine", + TranslatableString("engraving", "duration line"), + TranslatableString("engraving", "Duration line") }, Item{ ElementType::ROOT_ITEM, "RootItem", TranslatableString::untranslatable("root item"), TranslatableString::untranslatable("Root item") }, From a341a9c9f260540b4447740372c84521b73cde9e Mon Sep 17 00:00:00 2001 From: songzhenk Date: Fri, 26 Sep 2025 21:39:28 +1000 Subject: [PATCH 02/24] Hanle slur and note dot, enhance duration line, hide clef --- src/engraving/rendering/score/chordlayout.cpp | 26 ++++++++++--- .../rendering/score/slurtielayout.cpp | 38 ++++++++++++++++++- src/engraving/rendering/score/slurtielayout.h | 1 + src/engraving/rendering/score/tlayout.cpp | 5 +++ 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 48faa17419aa..0e91d9668385 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1667,16 +1667,28 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) bool staffVisible = !st->isLinesInvisible(tick); bool isJianpu = st->isJianpuStaff(tick); + auto getLinesByDots = [item](int base){ + int lines = base - 1; + int dots = item->dots(); + while (dots > 0) + { + base /= 2; + lines += base; + dots--; + } + return -lines; // negative indicates lines to lengthen the duration + }; + int lines = 0; TDuration durationType = item->durationType(); if (durationType == DurationType::V_LONG) { - lines = -15; + lines = getLinesByDots(16); } else if (durationType == DurationType::V_BREVE) { - lines = -7; + lines = getLinesByDots(8); } else if (durationType == DurationType::V_WHOLE) { - lines = -3; + lines = getLinesByDots(4); } else if (durationType == DurationType::V_HALF) { - lines = -1; + lines = getLinesByDots(2); } else if (durationType == DurationType::V_QUARTER) { lines = 0; } else if (durationType == DurationType::V_EIGHTH) { @@ -1802,8 +1814,10 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) } else if (octave < baseOctave) { dots = baseOctave - octave; offsetY = distance; - if (item->durationLines().size() > 0) { - offsetY += item->durationLines().back()->pos().y() + item->durationLines().back()->height() * .5; + auto lines = item->durationLines(); + // The octave dot should be under the halving lines + if (lines.size() > 0 && lines.back()->halving()) { + offsetY += lines.back()->pos().y() + lines.back()->height() * .5; } } else { cleanupOctaveDots(); diff --git a/src/engraving/rendering/score/slurtielayout.cpp b/src/engraving/rendering/score/slurtielayout.cpp index 02a921180973..549c5e7b84ef 100644 --- a/src/engraving/rendering/score/slurtielayout.cpp +++ b/src/engraving/rendering/score/slurtielayout.cpp @@ -1580,6 +1580,7 @@ TieSegment* SlurTieLayout::layoutTieFor(Tie* item, System* system) correctForCrossStaff(item, sPos, sPos.system1 != sPos.system2 ? SpannerSegmentType::BEGIN : SpannerSegmentType::SINGLE); forceHorizontal(item, sPos); + correctForJianpu(item, sPos); item->fixupSegments(segmentCount); TieSegment* segment = item->segmentAt(0); @@ -1614,6 +1615,39 @@ TieSegment* SlurTieLayout::layoutTieFor(Tie* item, System* system) return segment; } +void SlurTieLayout::correctForJianpu(Tie* item, SlurTiePos& sPos) +{ + if (!item->startNote() || !item->endNote()) { + return; + } + + if (!item->staff()->isJianpuStaff(item->startNote()->tick())) { + return; + } + + double y1 = 0.0; + Chord* chord1 = item->startNote()->chord(); + if (chord1) { + y1 = chord1->ldata()->bbox().top(); + if (!chord1->octaveDots().empty()) { + y1 += chord1->octaveDots().front()->ldata()->pos().y(); + } + } + + double y2 = 0.0; + Chord* chord2 = item->endNote()->chord(); + if (chord2) { + y2 = chord2->ldata()->bbox().top(); + if (!chord2->octaveDots().empty()) { + y2 += chord2->octaveDots().front()->ldata()->pos().y(); + } + } + + double minY = std::min(y1, y2); + sPos.p1 += PointF(0, minY); + sPos.p2 += PointF(0, minY); +} + TieSegment* SlurTieLayout::layoutTieBack(Tie* item, System* system, LayoutContext& ctx) { Chord* chord = item->endNote() ? item->endNote()->chord() : nullptr; @@ -3047,7 +3081,9 @@ void SlurTieLayout::calculateDirection(Tie* item) StaffType* st = item->staff()->staffType(primaryNote ? primaryNote->tick() : Fraction(0, 1)); bool simpleException = st && st->isSimpleTabStaff(); // if there are multiple voices, the tie direction goes on stem side - if (primaryMeasure->hasVoices(primaryChord->staffIdx(), primaryChord->tick(), primaryChord->actualTicks())) { + if (item->staff()->isJianpuStaff(primaryNote ? primaryNote->tick() : Fraction(0, 1))) { + item->setUp(true); + } else if (primaryMeasure->hasVoices(primaryChord->staffIdx(), primaryChord->tick(), primaryChord->actualTicks())) { item->setUp(simpleException ? isUpVoice(primaryChord->voice()) : primaryChord->up()); } else if (tieHasBothNotes && secondaryMeasure->hasVoices(secondaryChord->staffIdx(), secondaryChord->tick(), secondaryChord->actualTicks())) { diff --git a/src/engraving/rendering/score/slurtielayout.h b/src/engraving/rendering/score/slurtielayout.h index fd7b4caeda9d..0bd83a6db6a5 100644 --- a/src/engraving/rendering/score/slurtielayout.h +++ b/src/engraving/rendering/score/slurtielayout.h @@ -99,6 +99,7 @@ class SlurTieLayout static void computeStartAndEndSystem(Tie* item, SlurTiePos& slurTiePos); static PointF computeDefaultStartOrEndPoint(const Tie* tie, Grip startOrEnd); static void correctForCrossStaff(Tie* tie, SlurTiePos& sPos, SpannerSegmentType type); + static void correctForJianpu(Tie* item, SlurTiePos& sPos); static void forceHorizontal(Tie* tie, SlurTiePos& sPos); static void adjustX(TieSegment* tieSegment, SlurTiePos& sPos, Grip startOrEnd); static void adjustXforLedgerLines(TieSegment* tieSegment, bool start, Chord* chord, Note* note, const PointF& chordSystemPos, diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index afb0fd8906df..06d9c1cbb498 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -1731,6 +1731,11 @@ void TLayout::layoutClef(const Clef* item, Clef::LayoutData* ldata, const Layout const Fraction tick = endOfMeasureClef && !item->isTrailer() ? item->tick() : clefSeg->measure()->tick(); const Fraction tickPrev = tick - Fraction::eps(); + + if (item->staff()->isJianpuStaff(tick)) { + return; // Jianpu do not need to show clef + } + const StaffType* st = item->staff()->staffType(tick); const StaffType* stPrev = !tickPrev.negative() ? item->staff()->staffType(tickPrev) : nullptr; StaffGroup staffGroup = st->group(); From 0af70ef4971b0814a86900fb1c31b0de54be2546 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Sun, 21 Dec 2025 23:42:44 +1100 Subject: [PATCH 03/24] Fix duration line when load file by moving duration line to system layout --- src/engraving/rendering/score/chordlayout.cpp | 15 +++++++++++---- src/engraving/rendering/score/chordlayout.h | 3 ++- src/engraving/rendering/score/systemlayout.cpp | 1 + 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 0e91d9668385..eed75c405f0a 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1667,6 +1667,10 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) bool staffVisible = !st->isLinesInvisible(tick); bool isJianpu = st->isJianpuStaff(tick); + if (!isJianpu) { + return; + } + auto getLinesByDots = [item](int base){ int lines = base - 1; int dots = item->dots(); @@ -1710,7 +1714,7 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) } // need duration lines? - if (!isJianpu || lines == 0) { + if (lines == 0) { muse::DeleteAll(item->durationLines()); item->durationLines().clear(); return; @@ -1735,7 +1739,11 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) if (Chord* prev = item->prev(); prev&& !first) { if (prev->durationLines().size() > 0 && prev->durationLines().back()->halving()) { prevLines = prev->durationLines().size(); - prevOffsetX = item->pagePos().x() - prev->pagePos().x() - hw; + auto currX = item->pagePos().x(); + auto prevX = prev->pagePos().x(); + if (currX > prevX) { + prevOffsetX = currX - prevX - hw; // Offset to join to previous duration line + } } } @@ -1749,7 +1757,7 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) if (i >= static_cast(prevLines)) { prevOffsetX = 0.0; // Do not join to duration line in previous chord } - double minX = hx - prevOffsetX; // extend to join to previous duration line + double minX = hx - prevOffsetX; // Extend to join to previous duration line dl->setLen(maxX - minX); dl->setPos(minX, (i + 1) * distance); dl->setHalving(true); // Halving duration @@ -2493,7 +2501,6 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ for (Chord* grace : chord->graceNotes()) { AccidentalsLayout::layoutAccidentals({ grace }, ctx); } - layoutDurationLines(chord, ctx); layoutOctaveDots(chord, ctx); } } diff --git a/src/engraving/rendering/score/chordlayout.h b/src/engraving/rendering/score/chordlayout.h index 4ef6092549d5..362d0cfe880b 100644 --- a/src/engraving/rendering/score/chordlayout.h +++ b/src/engraving/rendering/score/chordlayout.h @@ -113,6 +113,8 @@ class ChordLayout static void layoutChordBaseFingering(Chord* chord, System* system, LayoutContext& ctx); + static void layoutDurationLines(Chord* item, LayoutContext& ctx); + static void crossMeasureSetup(Chord* chord, bool on, LayoutContext& ctx); static void checkStartEndSlurs(Chord* chord, LayoutContext& ctx); @@ -157,7 +159,6 @@ class ChordLayout static void updateLedgerLines(Chord* item, LayoutContext& ctx); - static void layoutDurationLines(Chord* item, LayoutContext& ctx); static void layoutOctaveDots(Chord* item, LayoutContext& ctx); static ChordPosInfo calculateChordPosInfo(Segment* segment, staff_idx_t staffIdx, track_idx_t partStartTrack, track_idx_t partEndTrack, diff --git a/src/engraving/rendering/score/systemlayout.cpp b/src/engraving/rendering/score/systemlayout.cpp index d7909c8896e9..f9d8ab10e16d 100644 --- a/src/engraving/rendering/score/systemlayout.cpp +++ b/src/engraving/rendering/score/systemlayout.cpp @@ -1247,6 +1247,7 @@ void SystemLayout::layoutSystemElements(System* system, LayoutContext& ctx) ChordLayout::layoutArticulations(c, ctx); ChordLayout::layoutArticulations2(c, ctx); ChordLayout::layoutChordBaseFingering(c, system, ctx); + ChordLayout::layoutDurationLines(c, ctx); } layoutTuplets(elementsToLayout.chordRests, ctx); From 3c170471737d44a8074c1cce33cdbe058b105b99 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Wed, 31 Dec 2025 00:19:54 +1100 Subject: [PATCH 04/24] support jianpu in shadow node --- src/engraving/dom/chord.cpp | 44 ++++------- src/engraving/dom/chord.h | 8 -- src/engraving/dom/chordrest.cpp | 17 +++++ src/engraving/dom/chordrest.h | 7 ++ src/engraving/dom/durationline.cpp | 39 +++++++++- src/engraving/dom/durationline.h | 3 +- src/engraving/dom/durationtype.cpp | 64 ++++++++++++++++ src/engraving/dom/durationtype.h | 3 + src/engraving/dom/shadownote.h | 12 +++ src/engraving/rendering/score/chordlayout.cpp | 74 ++++--------------- src/engraving/rendering/score/chordlayout.h | 3 +- src/engraving/rendering/score/restlayout.cpp | 67 ++++++++++++++++- src/engraving/rendering/score/restlayout.h | 1 + .../rendering/score/systemlayout.cpp | 1 + src/engraving/rendering/score/tdraw.cpp | 66 ++++++++++++++++- src/engraving/rendering/score/tlayout.cpp | 72 +++++++++++++++++- src/engraving/rendering/score/tlayout.h | 2 +- 17 files changed, 372 insertions(+), 111 deletions(-) diff --git a/src/engraving/dom/chord.cpp b/src/engraving/dom/chord.cpp index 811ce30332f4..f70e66f118c4 100644 --- a/src/engraving/dom/chord.cpp +++ b/src/engraving/dom/chord.cpp @@ -39,7 +39,6 @@ #include "chordline.h" #include "dom/parenthesis.h" #include "drumset.h" -#include "durationline.h" #include "factory.h" #include "guitarbend.h" #include "hook.h" @@ -477,7 +476,6 @@ Chord::~Chord() delete m_stemSlash; delete m_stem; delete m_hook; - muse::DeleteAll(m_durationLines); muse::DeleteAll(m_ledgerLines); muse::DeleteAll(m_octaveDots); muse::DeleteAll(m_graceNotes); @@ -1033,37 +1031,26 @@ Chord* Chord::next() const return nullptr; } -template -void Chord::resizeTo(std::vector& vec, size_t newSize) -{ - int diff = static_cast(newSize - vec.size()); - if (diff > 0) { - for (int i = 0; i < diff; ++i) { - vec.push_back(new T(score()->dummy())); - } - } else { - for (int i = 0; i < std::abs(diff); ++i) { - delete vec.back(); - vec.pop_back(); - } - } - - assert(vec.size() == newSize); -} - -void Chord::resizeDurationLinesTo(size_t newSize) -{ - resizeTo(m_durationLines, newSize); -} - void Chord::resizeLedgerLinesTo(size_t newSize) { - resizeTo(m_ledgerLines, newSize); + while (m_ledgerLines.size() < newSize) { + m_ledgerLines.push_back(new LedgerLine(score()->dummy())); + } + while (m_ledgerLines.size() > newSize) { + delete m_ledgerLines.back(); + m_ledgerLines.pop_back(); + } } void Chord::resizeOctaveDotsTo(size_t newSize) { - resizeTo(m_octaveDots, newSize); + while (m_octaveDots.size() < newSize) { + m_octaveDots.push_back(new OctaveDot(score()->dummy())); + } + while (m_octaveDots.size() > newSize) { + delete m_octaveDots.back(); + m_octaveDots.pop_back(); + } } void Chord::setBeamExtension(double extension) @@ -1336,9 +1323,6 @@ void Chord::scanElements(std::function func) for (Note* note : m_notes) { note->scanElements(func); } - for (DurationLine* dl : m_durationLines) { - func(dl); - } for (OctaveDot* dot : m_octaveDots) { func(dot); } diff --git a/src/engraving/dom/chord.h b/src/engraving/dom/chord.h index 28e028c1f29f..4421e7a92d61 100644 --- a/src/engraving/dom/chord.h +++ b/src/engraving/dom/chord.h @@ -40,7 +40,6 @@ namespace mu::engraving { class AccidentalState; class Arpeggio; class Chord; -class DurationLine; class Hook; class LedgerLine; class Note; @@ -168,10 +167,6 @@ class Chord final : public ChordRest bool isUiItem() const { return m_isUiItem; } void setIsUiItem(bool val) { m_isUiItem = val; } - const std::vector& durationLines() const { return m_durationLines; } - std::vector& durationLines() { return m_durationLines; } - void resizeDurationLinesTo(size_t newSize); - const std::vector& ledgerLines() const { return m_ledgerLines; } std::vector& ledgerLines() { return m_ledgerLines; } void resizeLedgerLinesTo(size_t newSize); @@ -401,10 +396,7 @@ class Chord final : public ChordRest // `includeTemporarySiblings`: whether items that are deleted & recreated during every layout should also be processed void processSiblings(std::function func, bool includeTemporarySiblings) const; - template void resizeTo(std::vector& vec, size_t newSize); - std::vector m_notes; // sorted to decreasing line step - std::vector m_durationLines; std::vector m_ledgerLines; std::vector m_octaveDots; NoteParenthesisInfoList m_noteParens; diff --git a/src/engraving/dom/chordrest.cpp b/src/engraving/dom/chordrest.cpp index c29864d923fa..27507985adb6 100644 --- a/src/engraving/dom/chordrest.cpp +++ b/src/engraving/dom/chordrest.cpp @@ -32,6 +32,7 @@ #include "breath.h" #include "chord.h" #include "clef.h" +#include "durationline.h" #include "factory.h" #include "figuredbass.h" #include "harmony.h" @@ -127,6 +128,7 @@ ChordRest::~ChordRest() { muse::DeleteAll(m_lyrics); muse::DeleteAll(m_el); + muse::DeleteAll(m_durationLines); delete m_tabDur; if (m_beam) { @@ -1044,6 +1046,9 @@ void ChordRest::scanElements(std::function func) if (m_tabDur) { func(m_tabDur); } + for (DurationLine* dl : m_durationLines) { + func(dl); + } } //--------------------------------------------------------- @@ -1327,4 +1332,16 @@ bool ChordRest::hasPrecedingJumpItem() const return !precedingRepeatMeasures.empty(); } + +void ChordRest::resizeDurationLinesTo(size_t newSize) +{ + while (m_durationLines.size() < newSize) { + m_durationLines.push_back(new DurationLine(score()->dummy())); + } + + while (m_durationLines.size() > newSize) { + delete m_durationLines.back(); + m_durationLines.pop_back(); + } +} } diff --git a/src/engraving/dom/chordrest.h b/src/engraving/dom/chordrest.h index 3065f1e955f3..2f5c19efe2f7 100644 --- a/src/engraving/dom/chordrest.h +++ b/src/engraving/dom/chordrest.h @@ -40,6 +40,7 @@ enum class CrossMeasure : signed char { class Articulation; class BeamBase; +class DurationLine; class Lyrics; class Measure; class Score; @@ -121,6 +122,10 @@ class ChordRest : public DurationElement String durationUserName() const; + const std::vector& durationLines() const { return m_durationLines; } + std::vector& durationLines() { return m_durationLines; } + void resizeDurationLinesTo(size_t newSize); + void setTrack(track_idx_t val) override; const std::vector& lyrics() const { return m_lyrics; } @@ -230,6 +235,8 @@ class ChordRest : public DurationElement TDuration m_durationType; int m_staffMove = 0; // -1, 0, +1, used for crossbeaming int m_storedStaffMove = 0; // used to remember and re-apply staff move if needed + + std::vector m_durationLines; }; } // namespace mu::engraving #endif diff --git a/src/engraving/dom/durationline.cpp b/src/engraving/dom/durationline.cpp index 6ee0f5a2d157..d0cfa4164206 100644 --- a/src/engraving/dom/durationline.cpp +++ b/src/engraving/dom/durationline.cpp @@ -24,6 +24,8 @@ #include "chord.h" #include "measure.h" +#include "rest.h" +#include "staff.h" #include "system.h" #include "log.h" @@ -52,7 +54,7 @@ DurationLine::~DurationLine() PointF DurationLine::pagePos() const { - System* system = chord()->measure()->system(); + System* system = chordRest()->measure()->system(); double yp = y() + system->staff(staffIdx())->y() + system->y(); return PointF(pageX(), yp); } @@ -64,8 +66,8 @@ PointF DurationLine::pagePos() const double DurationLine::measureXPos() const { double xp = x(); // chord relative - xp += chord()->x(); // segment relative - xp += chord()->segment()->x(); // measure relative + xp += chordRest()->x(); // segment relative + xp += chordRest()->segment()->x(); // measure relative return xp; } @@ -77,4 +79,35 @@ void DurationLine::spatiumChanged(double oldValue, double newValue) { m_len = (m_len / oldValue) * newValue; } + +//--------------------------------------------------------- +// mag +//--------------------------------------------------------- + +double DurationLine::mag() const +{ + auto* parent = explicitParent(); + if (parent && parent->isRest()) { + return toRest(parent)->mag(); + } + if (parent && parent->isChord()) { + return toChord(parent)->mag(); + } + const Staff* st = staff(); + return st ? st->staffMag(this) : 1.0; +} + +//--------------------------------------------------------- +// chord +//--------------------------------------------------------- + +ChordRest* DurationLine::chordRest() const +{ + auto* parent = explicitParent(); + if (parent && parent->isChordRest()) { + return toChordRest(parent); + } else { + return nullptr; + } +} } diff --git a/src/engraving/dom/durationline.h b/src/engraving/dom/durationline.h index d7778e9a24b7..7859c9883a01 100644 --- a/src/engraving/dom/durationline.h +++ b/src/engraving/dom/durationline.h @@ -50,7 +50,8 @@ class DurationLine final : public EngravingItem DurationLine* clone() const override { return new DurationLine(*this); } PointF pagePos() const override; ///< position in page coordinates - Chord* chord() const { return toChord(explicitParent()); } + ChordRest* chordRest() const; + double mag() const; double len() const { return m_len; } void setLen(double v) { m_len = v; } diff --git a/src/engraving/dom/durationtype.cpp b/src/engraving/dom/durationtype.cpp index 8a84c2911aca..3a81efa12292 100644 --- a/src/engraving/dom/durationtype.cpp +++ b/src/engraving/dom/durationtype.cpp @@ -823,4 +823,68 @@ bool TDuration::isValid(Fraction f) t.truncateToFraction(f, 4); return t.isValid() && (t.fraction() - f).numerator() == 0; } + +//--------------------------------------------------------- +// augmentation lines - number of Line to lengthen duration +//--------------------------------------------------------- +int TDuration::augmentationLines() const +{ + auto getLinesByDots = [this](int base){ + int lines = base - 1; + int dots = this->dots(); + while (dots > 0) + { + base /= 2; + lines += base; + dots--; + } + return lines; + }; + + switch (m_val) { + case DurationType::V_LONG: + return getLinesByDots(16); + case DurationType::V_BREVE: + return getLinesByDots(8); + case DurationType::V_WHOLE: + return getLinesByDots(4); + case DurationType::V_HALF: + return getLinesByDots(2); + default: + return 0; + } +} + +//--------------------------------------------------------- +// diminution lines - number of Line to halve duration +//--------------------------------------------------------- +int TDuration::diminutionLines() const +{ + switch (m_val) { + case DurationType::V_LONG: + case DurationType::V_BREVE: + case DurationType::V_WHOLE: + case DurationType::V_HALF: + case DurationType::V_QUARTER: + return 0; + case DurationType::V_EIGHTH: + return 1; + case DurationType::V_16TH: + return 2; + case DurationType::V_32ND: + return 3; + case DurationType::V_64TH: + return 4; + case DurationType::V_128TH: + return 5; + case DurationType::V_256TH: + return 6; + case DurationType::V_512TH: + return 7; + case DurationType::V_1024TH: + return 8; + default: + return 0; + } +} } diff --git a/src/engraving/dom/durationtype.h b/src/engraving/dom/durationtype.h index 041f224d7fbc..26ee28cd2bac 100644 --- a/src/engraving/dom/durationtype.h +++ b/src/engraving/dom/durationtype.h @@ -83,6 +83,9 @@ class TDuration Fraction fraction() const; static bool isValid(Fraction f); + int augmentationLines() const; + int diminutionLines() const; + private: void shiftType(int nSteps, bool stepDotted = false); void truncateToFraction(const Fraction& l, int maxDots); diff --git a/src/engraving/dom/shadownote.h b/src/engraving/dom/shadownote.h index 7b152bb2e9ee..17cba1f5a2bf 100644 --- a/src/engraving/dom/shadownote.h +++ b/src/engraving/dom/shadownote.h @@ -81,6 +81,15 @@ class ShadowNote final : public EngravingItem bool isBeyondScore() const { return m_beyondScore; } + String jianpuDigit() const { return m_jianpuDigit; } + void setJianpuDigit(const String& s) { m_jianpuDigit = s; } + + int jianpuDurationLine() const { return m_jianpuDurationLine; } + void setJianpuDurationLine(int lines) { m_jianpuDurationLine = lines; } + + int jianpuOctaveDot() const { return m_jianpuOctaveDot; } + void setJianpuOctaveDot(int dots) { m_jianpuOctaveDot = dots; } + private: Fraction m_tick; @@ -91,5 +100,8 @@ class ShadowNote final : public EngravingItem AccidentalType m_accidentalType = AccidentalType::NONE; std::set m_articulationIds; bool m_beyondScore = false; + String m_jianpuDigit; + int m_jianpuDurationLine = 0; + int m_jianpuOctaveDot = 0; }; } // namespace mu::engraving diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index eed75c405f0a..aabbdc5a463c 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1671,50 +1671,9 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) return; } - auto getLinesByDots = [item](int base){ - int lines = base - 1; - int dots = item->dots(); - while (dots > 0) - { - base /= 2; - lines += base; - dots--; - } - return -lines; // negative indicates lines to lengthen the duration - }; - - int lines = 0; - TDuration durationType = item->durationType(); - if (durationType == DurationType::V_LONG) { - lines = getLinesByDots(16); - } else if (durationType == DurationType::V_BREVE) { - lines = getLinesByDots(8); - } else if (durationType == DurationType::V_WHOLE) { - lines = getLinesByDots(4); - } else if (durationType == DurationType::V_HALF) { - lines = getLinesByDots(2); - } else if (durationType == DurationType::V_QUARTER) { - lines = 0; - } else if (durationType == DurationType::V_EIGHTH) { - lines = 1; - } else if (durationType == DurationType::V_16TH) { - lines = 2; - } else if (durationType == DurationType::V_32ND) { - lines = 3; - } else if (durationType == DurationType::V_64TH) { - lines = 4; - } else if (durationType == DurationType::V_128TH) { - lines = 5; - } else if (durationType == DurationType::V_256TH) { - lines = 6; - } else if (durationType == DurationType::V_512TH) { - lines = 7; - } else if (durationType == DurationType::V_1024TH) { - lines = 8; - } - - // need duration lines? - if (lines == 0) { + int augmentationLines = item->durationType().augmentationLines(); + int diminutionLines = item->durationType().diminutionLines(); + if (augmentationLines == 0 && diminutionLines == 0) { muse::DeleteAll(item->durationLines()); item->durationLines().clear(); return; @@ -1748,8 +1707,8 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) } double distance = item->spatium() * .3; - item->resizeDurationLinesTo(std::abs(lines)); - for (int i = 0; i < lines; ++i) { + item->resizeDurationLinesTo(std::max(augmentationLines, diminutionLines)); + for (int i = 0; i < diminutionLines; ++i) { DurationLine* dl = item->durationLines()[i]; dl->setParent(item); dl->setTrack(track); @@ -1763,19 +1722,17 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) dl->setHalving(true); // Halving duration } - if (lines < 0) { // If we have negative lines, we need to lengthen the duration lines - const StaffType* jianpu = st->staffTypeForElement(item); - double height = jianpu->jianpuBoxH() * item->magS(); + const StaffType* jianpu = st->staffTypeForElement(item); + double height = jianpu->jianpuBoxH() * item->magS(); - for (int i = 0; i < -lines; ++i) { - DurationLine* dl = item->durationLines()[i]; - dl->setParent(item); - dl->setTrack(track); - dl->setVisible(staffVisible); - dl->setLen(hw); - dl->setPos(hx + hw * 1.2 * (i + 1), -height * .5); - dl->setHalving(false); // Lengthening duration - } + for (int i = 0; i < augmentationLines; ++i) { + DurationLine* dl = item->durationLines()[i]; + dl->setParent(item); + dl->setTrack(track); + dl->setVisible(staffVisible); + dl->setLen(hw); + dl->setPos(hx + hw * 1.2 * (i + 1), -height * .5); + dl->setHalving(false); // Lengthening duration } } @@ -2501,7 +2458,6 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ for (Chord* grace : chord->graceNotes()) { AccidentalsLayout::layoutAccidentals({ grace }, ctx); } - layoutOctaveDots(chord, ctx); } } diff --git a/src/engraving/rendering/score/chordlayout.h b/src/engraving/rendering/score/chordlayout.h index 362d0cfe880b..c15e16d973b0 100644 --- a/src/engraving/rendering/score/chordlayout.h +++ b/src/engraving/rendering/score/chordlayout.h @@ -114,6 +114,7 @@ class ChordLayout static void layoutChordBaseFingering(Chord* chord, System* system, LayoutContext& ctx); static void layoutDurationLines(Chord* item, LayoutContext& ctx); + static void layoutOctaveDots(Chord* item, LayoutContext& ctx); static void crossMeasureSetup(Chord* chord, bool on, LayoutContext& ctx); @@ -159,8 +160,6 @@ class ChordLayout static void updateLedgerLines(Chord* item, LayoutContext& ctx); - static void layoutOctaveDots(Chord* item, LayoutContext& ctx); - static ChordPosInfo calculateChordPosInfo(Segment* segment, staff_idx_t staffIdx, track_idx_t partStartTrack, track_idx_t partEndTrack, LayoutContext& ctx); static void calculateMaxNoteWidths(ChordPosInfo& posInfo, const Fraction& tick, const Staff* staff, LayoutContext& ctx); diff --git a/src/engraving/rendering/score/restlayout.cpp b/src/engraving/rendering/score/restlayout.cpp index 31e7ed2d900e..05822a7f2533 100644 --- a/src/engraving/rendering/score/restlayout.cpp +++ b/src/engraving/rendering/score/restlayout.cpp @@ -27,6 +27,7 @@ #include "tlayout.h" #include "dom/beam.h" +#include "dom/durationline.h" #include "dom/system.h" using namespace muse; @@ -113,8 +114,9 @@ void RestLayout::layoutRest(const Rest* item, Rest::LayoutData* ldata, const Lay delete item->tabDur(); const_cast(item)->setTabDur(nullptr); } - } else if (stf && stf->isJianpuStaff(item->measure()->ticks())) { - ldata->setPos(0.0, 0.0); + } + + if (layoutDurationLines(item, ldata, ctx)) { return; } @@ -172,6 +174,67 @@ void RestLayout::fillShape(const Rest* item, Rest::LayoutData* ldata, const Layo } } +bool RestLayout::layoutDurationLines(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx) +{ + Fraction tick = item->measure()->ticks(); + const Staff* staff = item->staff(); + if (!staff->isJianpuStaff(tick)) { + return false; + } + + ldata->setPos(0.0, 0.0); + + staff_idx_t idx = item->staffIdx() + item->staffMove(); + track_idx_t track = staff2track(idx); + bool staffVisible = !staff->isLinesInvisible(tick); + + int augmentationLines = item->durationType().augmentationLines(); + int diminutionLines = item->durationType().diminutionLines(); + if (augmentationLines == 0 && diminutionLines == 0) { + muse::DeleteAll(item->durationLines()); + const_cast(item)->durationLines().clear(); + return true; + } + + muse::draw::Font font(item->staffType()->jianpuFont()); + font.setPointSizeF(font.pointSizeF() * item->mag()); + + muse::draw::FontMetrics fm(font); + const double hw = fm.width(String(u"0")) / item->magS(); + const double hx = ldata->bbox().x(); + const double distance = item->spatium() * .3; + const_cast(item)->resizeDurationLinesTo(std::max(augmentationLines, diminutionLines)); + + for (int i = 0; i < diminutionLines; ++i) { + DurationLine* dl = item->durationLines()[i]; + dl->setParent(const_cast(item)); + dl->setTrack(track); + dl->setVisible(staffVisible); + dl->setLen(hw); + dl->setPos(hx, (i + 1) * distance); + dl->setHalving(true); // Halving duration + } + + const StaffType* jianpu = staff->staffTypeForElement(item); + const double height = jianpu->jianpuBoxH() * item->magS(); + + for (int i = 0; i < augmentationLines; ++i) { + DurationLine* dl = item->durationLines()[i]; + dl->setParent(const_cast(item)); + dl->setTrack(track); + dl->setVisible(staffVisible); + dl->setLen(hw); + dl->setPos(hx + hw * 1.2 * (i + 1), -height * .5); + dl->setHalving(false); // Lengthening duration + } + + for (DurationLine* dl : item->durationLines()) { + TLayout::layoutDurationLine(dl, ctx); + } + + return true; +} + void RestLayout::resolveVerticalRestConflicts(LayoutContext& ctx, Segment* segment, staff_idx_t staffIdx) { std::vector rests; diff --git a/src/engraving/rendering/score/restlayout.h b/src/engraving/rendering/score/restlayout.h index 92bf4f806a65..faf0ccb6d6ef 100644 --- a/src/engraving/rendering/score/restlayout.h +++ b/src/engraving/rendering/score/restlayout.h @@ -41,6 +41,7 @@ class RestLayout public: static void layoutRest(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx); static void fillShape(const Rest* item, Rest::LayoutData* ldata, const LayoutConfiguration& conf); + static bool layoutDurationLines(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx); static void resolveVerticalRestConflicts(LayoutContext& ctx, Segment* segment, staff_idx_t staffIdx); static void resolveRestVSChord(std::vector& rests, std::vector& chords, const Staff* staff, Segment* segment); diff --git a/src/engraving/rendering/score/systemlayout.cpp b/src/engraving/rendering/score/systemlayout.cpp index f9d8ab10e16d..66ad41e86804 100644 --- a/src/engraving/rendering/score/systemlayout.cpp +++ b/src/engraving/rendering/score/systemlayout.cpp @@ -1248,6 +1248,7 @@ void SystemLayout::layoutSystemElements(System* system, LayoutContext& ctx) ChordLayout::layoutArticulations2(c, ctx); ChordLayout::layoutChordBaseFingering(c, system, ctx); ChordLayout::layoutDurationLines(c, ctx); + ChordLayout::layoutOctaveDots(c, ctx); } layoutTuplets(elementsToLayout.chordRests, ctx); diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index 2255ce1f1819..e4f5072de5f1 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -2632,6 +2632,7 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) Pen pen(item->color(), lw, PenStyle::SolidLine, PenCapStyle::FlatCap); painter->setPen(pen); + bool jianpu = item->staff() && item->staff()->isJianpuStaff(item->tick()); bool up = item->computeUp(); // Draw the accidental @@ -2708,8 +2709,71 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) int l = item->staffType()->lines() * 2 + yOffset / step; // first ledger line below staff for (int i = l; i <= item->lineIndex(); i += 2) { double y = step * (i - item->lineIndex()); - painter->drawLine(LineF(x1, y, x2, y)); + if (jianpu && i < 9) { + // jianpu ledger line width is double in the 5-line staff + painter->drawLine(LineF(x1 - 2 * extraLen, y, x2 + 2 * extraLen, y)); + } else { + // Normal ledger lines below staff + painter->drawLine(LineF(x1, y, x2, y)); + } + } + } + + // Draw jianpu digit + if (jianpu) { + const Staff* staff = item->staff(); + const StaffType* staffType = staff->staffTypeForElement(item); + Font f(staffType->jianpuFont()); + f.setPointSizeF(f.pointSizeF() * item->magS()); + painter->setFont(f); + + FontMetrics fm(f); + const double jianpuWidth = fm.width(item->jianpuDigit()) / item->magS(); + const double jianpuHeight = staffType->jianpuBoxH() * item->magS(); + + auto bbox = item->ldata()->bbox(); + double jianpuX = (noteheadWidth - jianpuWidth) * .5; + double jianpuY = bbox.y(); + double distance = sp * .3; + double rad = sp * .1; + + if (item->lineIndex() >= 0) { + jianpuY = bbox.height() + bbox.y(); + jianpuY -= jianpuHeight; + jianpuY -= distance * (abs(item->jianpuOctaveDot()) + item->jianpuDurationLine()); + jianpuY -= rad * 2; } + + painter->save(); + pen.setWidthF(1.0); + painter->setPen(pen); + painter->setBrush(Brush(pen.color())); + + double dotX = noteheadWidth * .5; + for (int i = 0; i < -item->jianpuOctaveDot(); i++) { + painter->drawEllipse(PointF(dotX, jianpuY + rad), rad, rad); + jianpuY += distance; + } + + jianpuY += jianpuHeight; + painter->drawText(jianpuX, jianpuY, item->jianpuDigit()); + jianpuY += distance; + + pen.setWidthF(lw); + painter->setPen(pen); + for (int i = 0; i < item->jianpuDurationLine(); i++) { + painter->drawLine(LineF(0.0, jianpuY, noteheadWidth, jianpuY)); + jianpuY += distance; + } + + pen.setWidthF(1.0); + painter->setPen(pen); + for (int i = 0; i < item->jianpuOctaveDot(); i++) { + painter->drawEllipse(PointF(dotX, jianpuY + rad), rad, rad); + jianpuY += distance; + } + + painter->restore(); } item->drawArticulations(painter); diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index 06d9c1cbb498..440147b94da5 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -1892,13 +1892,13 @@ void TLayout::layoutDeadSlapped(const DeadSlapped* item, DeadSlapped::LayoutData } } -void TLayout::layoutDurationLine(DurationLine* item, LayoutContext& ctx) +void TLayout::layoutDurationLine(DurationLine* item, const LayoutContext& ctx) { LAYOUT_CALL_ITEM(item); - double chordMag = item->chord()->mag(); + double mag = item->mag(); DurationLine::LayoutData* ldata = item->mutldata(); - ldata->setMag(chordMag); - ldata->lineWidth = ctx.conf().styleMM(Sid::staffLineWidth) * chordMag; + ldata->setMag(mag); + ldata->lineWidth = ctx.conf().styleAbsolute(Sid::staffLineWidth) * mag; if (item->staff()) { const_cast(item)->setColor(item->staff()->staffType(item->tick())->color()); } @@ -4643,6 +4643,7 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) double mag = item->mag(); RectF noteheadBbox = item->symBbox(item->noteheadSymbol()); bool up = item->computeUp(); + bool isJianpu = item->staff() && item->staff()->isJianpuStaff(item->tick()); // Layout dots double dotWidth = 0.0; @@ -4697,6 +4698,12 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) double lw = ctx.conf().styleAbsolute(Sid::ledgerLineWidth); + // jianpu will show the 5-line staff with more extra length + if (isJianpu && lineIdx > 0) { + x -= 2 * extraLen; + w += 4 * extraLen; + } + RectF r(x, -lw * .5, w, lw); const int topLine = -2 + yOffset / step; for (int i = topLine; i >= lineIdx; i -= 2) { @@ -4727,6 +4734,63 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) newBbox.setHeight(newBbox.height() + dh); } + // Layout jianpu + RectF jianpuBbox(noteheadBbox.x(), 0, noteheadBbox.width(), 0); + if (isJianpu) { + const Staff* staff = item->staff(); + const StaffType* jianpu = staff->staffTypeForElement(item); + int tpc = 0; + NoteVal nval; + + // jianpu digit + if (item->isRest()) { + item->setJianpuDigit(String(u"0")); + } else { + Position pos; + Score* score = item->score(); + score->getPosition(&pos, item->pos(), item->track()); + bool error = false; + nval = score->noteValForPosition(pos, item->accidentalType(), error); + + KeySigEvent ks = staff->keySigEvent(item->tick()); + tpc = pitch2tpc(nval.pitch, ks.key(), Prefer::NEAREST); + + String accName, stepName; + tpc2Function(tpc, ks.key(), accName, stepName); + item->setJianpuDigit(String(u"%1").arg(stepName)); + } + double distance = _spatium * .3; + jianpuBbox.setHeight(jianpu->jianpuBoxH() * mag + distance); + if (!up) { + jianpuBbox.setY(noteheadBbox.y() - jianpuBbox.height()); // Jianpu is above the head note + } else { + jianpuBbox.setY(newBbox.y() + newBbox.height()); // Jianpu is under the head note + } + + // jianpu duration line + int lines = item->duration().diminutionLines(); + item->setJianpuDurationLine(lines); + + // jianpu dot + int dots = 0; + if (!item->isRest()) { + int baseOctave = 3; + int alteration = static_cast(tpc2alter(tpc)); + int octave = (nval.pitch - alteration) / 12 - 1; // See Note::octave + dots = baseOctave - octave; + } + item->setJianpuOctaveDot(dots); + + // Always has one extra distance + double extraHeight = (1 + abs(dots) + lines) * distance; + + jianpuBbox.setHeight(jianpuBbox.height() + extraHeight); + if (!up) { + jianpuBbox.setY(jianpuBbox.y() - extraHeight); // Jianpu is above the head note + } + newBbox |= jianpuBbox; + } + const std::set& articulationIds = item->articulationIds(); if (articulationIds.empty()) { item->setbbox(newBbox); diff --git a/src/engraving/rendering/score/tlayout.h b/src/engraving/rendering/score/tlayout.h index 377756741cd7..8d510a3e2fc2 100644 --- a/src/engraving/rendering/score/tlayout.h +++ b/src/engraving/rendering/score/tlayout.h @@ -231,7 +231,7 @@ class TLayout static void layoutCapo(const Capo* item, Capo::LayoutData* ldata, const LayoutContext& ctx); static void layoutDeadSlapped(const DeadSlapped* item, DeadSlapped::LayoutData* ldata); - static void layoutDurationLine(DurationLine* item, LayoutContext& ctx); + static void layoutDurationLine(DurationLine* item, const LayoutContext& ctx); static void layoutDynamic(Dynamic* item, Dynamic::LayoutData* ldata, const LayoutConfiguration& conf); static void layoutExpression(const Expression* item, Expression::LayoutData* ldata); From 67379b935de4df1ced9c5e3c9206ad1d1f40ed38 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Thu, 1 Jan 2026 17:18:00 +1100 Subject: [PATCH 05/24] fix default preset for staff found by unit test in engraving_test --- src/engraving/dom/stafftype.cpp | 11 ++++++----- src/engraving/dom/stafftype.h | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/engraving/dom/stafftype.cpp b/src/engraving/dom/stafftype.cpp index 3819ecfc01b1..4c690ac98ddc 100644 --- a/src/engraving/dom/stafftype.cpp +++ b/src/engraving/dom/stafftype.cpp @@ -1081,10 +1081,11 @@ double StaffType::spatium() const // //========================================================= -static const int _defaultPreset[STAFF_GROUP_MAX] = -{ 0, // default pitched preset is "stdNormal" - 4, // default percussion preset is "perc5lines" - 5 // default tab preset is "tab6StrCommon" +static const StaffTypes _defaultPreset[STAFF_GROUP_MAX] = +{ + StaffTypes::STANDARD, // default pitched preset is "stdNormal" + StaffTypes::PERC_5LINE, // default percussion preset is "perc5lines" + StaffTypes::TAB_6COMMON // default tab preset is "tab6StrCommon" }; //--------------------------------------------------------- @@ -1113,7 +1114,7 @@ const StaffType* StaffType::presetFromXmlName(const String& xmlName) const StaffType* StaffType::getDefaultPreset(StaffGroup grp) { - int _idx = _defaultPreset[int(grp)]; + int _idx = static_cast(_defaultPreset[int(grp)]); return &m_presets[_idx]; } diff --git a/src/engraving/dom/stafftype.h b/src/engraving/dom/stafftype.h index 7a01a5594fa0..985228e1c99d 100644 --- a/src/engraving/dom/stafftype.h +++ b/src/engraving/dom/stafftype.h @@ -116,7 +116,7 @@ struct TablatureDurationFont { }; // ready-made staff types -// keep in sync with the _presets initialization in StaffType::initStaffTypes() and _defaultPreset +// keep in sync with the _presets initialization in StaffType::initStaffTypes() enum class StaffTypes : signed char { STANDARD, JIANPU, From 28188d67f8b589461ca47dfbce1afd827cf4d9e3 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Tue, 20 Jan 2026 19:13:10 +1100 Subject: [PATCH 06/24] Move octave dot from chord to note --- src/engraving/dom/chord.cpp | 32 ++++++----------- src/engraving/dom/chord.h | 6 ---- src/engraving/dom/durationline.cpp | 12 ------- src/engraving/dom/durationline.h | 2 -- src/engraving/dom/note.cpp | 16 +++++++++ src/engraving/dom/note.h | 6 ++++ src/engraving/dom/octavedot.cpp | 23 ++++-------- src/engraving/dom/octavedot.h | 4 +-- src/engraving/rendering/score/chordlayout.cpp | 32 ++++++++--------- .../rendering/score/slurtielayout.cpp | 36 +++++-------------- .../rendering/score/systemlayout.cpp | 1 - src/engraving/rendering/score/tlayout.cpp | 6 +++- 12 files changed, 68 insertions(+), 108 deletions(-) diff --git a/src/engraving/dom/chord.cpp b/src/engraving/dom/chord.cpp index f70e66f118c4..50cbf533031e 100644 --- a/src/engraving/dom/chord.cpp +++ b/src/engraving/dom/chord.cpp @@ -51,7 +51,6 @@ #include "notedot.h" #include "noteevent.h" #include "noteline.h" -#include "octavedot.h" #include "ornament.h" #include "part.h" #include "rest.h" @@ -477,7 +476,6 @@ Chord::~Chord() delete m_stem; delete m_hook; muse::DeleteAll(m_ledgerLines); - muse::DeleteAll(m_octaveDots); muse::DeleteAll(m_graceNotes); muse::DeleteAll(m_notes); muse::DeleteAll(m_noteParens); @@ -1033,24 +1031,19 @@ Chord* Chord::next() const void Chord::resizeLedgerLinesTo(size_t newSize) { - while (m_ledgerLines.size() < newSize) { - m_ledgerLines.push_back(new LedgerLine(score()->dummy())); - } - while (m_ledgerLines.size() > newSize) { - delete m_ledgerLines.back(); - m_ledgerLines.pop_back(); + int ledgerLineCountDiff = static_cast(newSize - m_ledgerLines.size()); + if (ledgerLineCountDiff > 0) { + for (int i = 0; i < ledgerLineCountDiff; ++i) { + m_ledgerLines.push_back(new LedgerLine(score()->dummy())); + } + } else { + for (int i = 0; i < std::abs(ledgerLineCountDiff); ++i) { + delete m_ledgerLines.back(); + m_ledgerLines.pop_back(); + } } -} -void Chord::resizeOctaveDotsTo(size_t newSize) -{ - while (m_octaveDots.size() < newSize) { - m_octaveDots.push_back(new OctaveDot(score()->dummy())); - } - while (m_octaveDots.size() > newSize) { - delete m_octaveDots.back(); - m_octaveDots.pop_back(); - } + assert(m_ledgerLines.size() == newSize); } void Chord::setBeamExtension(double extension) @@ -1323,9 +1316,6 @@ void Chord::scanElements(std::function func) for (Note* note : m_notes) { note->scanElements(func); } - for (OctaveDot* dot : m_octaveDots) { - func(dot); - } for (Chord* chord : m_graceNotes) { chord->scanElements(func); } diff --git a/src/engraving/dom/chord.h b/src/engraving/dom/chord.h index 4421e7a92d61..8f4fadec461c 100644 --- a/src/engraving/dom/chord.h +++ b/src/engraving/dom/chord.h @@ -44,7 +44,6 @@ class Hook; class LedgerLine; class Note; class NoteEventList; -class OctaveDot; class Stem; class StemSlash; class TremoloTwoChord; @@ -171,10 +170,6 @@ class Chord final : public ChordRest std::vector& ledgerLines() { return m_ledgerLines; } void resizeLedgerLinesTo(size_t newSize); - const std::vector& octaveDots() const { return m_octaveDots; } - std::vector& octaveDots() { return m_octaveDots; } - void resizeOctaveDotsTo(size_t newSize); - double defaultStemLength() const { return m_defaultStemLength; } void setDefaultStemLength(double l) { m_defaultStemLength = l; } void setBeamExtension(double extension); @@ -398,7 +393,6 @@ class Chord final : public ChordRest std::vector m_notes; // sorted to decreasing line step std::vector m_ledgerLines; - std::vector m_octaveDots; NoteParenthesisInfoList m_noteParens; Stem* m_stem = nullptr; diff --git a/src/engraving/dom/durationline.cpp b/src/engraving/dom/durationline.cpp index d0cfa4164206..c730161d7dec 100644 --- a/src/engraving/dom/durationline.cpp +++ b/src/engraving/dom/durationline.cpp @@ -59,18 +59,6 @@ PointF DurationLine::pagePos() const return PointF(pageX(), yp); } -//--------------------------------------------------------- -// measureXPos -//--------------------------------------------------------- - -double DurationLine::measureXPos() const -{ - double xp = x(); // chord relative - xp += chordRest()->x(); // segment relative - xp += chordRest()->segment()->x(); // measure relative - return xp; -} - //--------------------------------------------------------- // spatiumChanged //--------------------------------------------------------- diff --git a/src/engraving/dom/durationline.h b/src/engraving/dom/durationline.h index 7859c9883a01..be8b1417b030 100644 --- a/src/engraving/dom/durationline.h +++ b/src/engraving/dom/durationline.h @@ -59,8 +59,6 @@ class DurationLine final : public EngravingItem void setHalving(bool v) { m_halving = v; } bool halving() const { return m_halving; } - double measureXPos() const; - void spatiumChanged(double /*oldValue*/, double /*newValue*/) override; struct LayoutData : public EngravingItem::LayoutData { diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index f9af21e92698..4d9fd6a913f0 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -59,6 +59,7 @@ #include "navigate.h" #include "notedot.h" #include "noteline.h" +#include "octavedot.h" #include "parenthesis.h" #include "part.h" #include "partialtie.h" @@ -558,6 +559,7 @@ Note::~Note() } muse::DeleteAll(m_dots); + muse::DeleteAll(m_octaveDots); } std::vector Note::compoundNotes() const @@ -2244,6 +2246,9 @@ void Note::scanElements(std::function func) for (NoteDot* dot : m_dots) { func(dot); } + for (OctaveDot* od : m_octaveDots) { + func(od); + } } //--------------------------------------------------------- @@ -4135,4 +4140,15 @@ bool Note::transpose(Interval interval, bool useDoubleSharpsFlats) EditNote::undoChangePitch(score(), this, npitch, ntpc1, ntpc2); return true; } + +void Note::resizeOctaveDotsTo(size_t newSize) +{ + while (m_octaveDots.size() < newSize) { + m_octaveDots.push_back(new OctaveDot(score()->dummy())); + } + while (m_octaveDots.size() > newSize) { + delete m_octaveDots.back(); + m_octaveDots.pop_back(); + } +} } diff --git a/src/engraving/dom/note.h b/src/engraving/dom/note.h index cdc635ad20b4..347ff4e05d53 100644 --- a/src/engraving/dom/note.h +++ b/src/engraving/dom/note.h @@ -42,6 +42,7 @@ class Score; class AccidentalState; class Accidental; class NoteDot; +class OctaveDot; class Spanner; class StaffType; class NoteEditData; @@ -454,6 +455,10 @@ class Note final : public EngravingItem bool isExactUnison(Note* other); + const std::vector& octaveDots() const { return m_octaveDots; } + std::vector& octaveDots() { return m_octaveDots; } + void resizeOctaveDotsTo(size_t newSize); + struct LayoutData : public EngravingItem::LayoutData { ld_field useTablature = { "[Note] useTablature", false }; ld_field cachedNoteheadSym = { "[Note] cachedNoteheadSym", SymId::noSym }; // use in draw to avoid recomputing at every update @@ -559,6 +564,7 @@ class Note final : public EngravingItem String m_fretString; String m_jianpuDigit; + std::vector m_octaveDots; std::vector m_lineAttachPoints; TieJumpPointList m_jumpPoints { this }; diff --git a/src/engraving/dom/octavedot.cpp b/src/engraving/dom/octavedot.cpp index a23cef7c5944..1add8c35f436 100644 --- a/src/engraving/dom/octavedot.cpp +++ b/src/engraving/dom/octavedot.cpp @@ -24,6 +24,7 @@ #include "chord.h" #include "measure.h" +#include "note.h" #include "system.h" #include "log.h" @@ -52,23 +53,11 @@ OctaveDot::~OctaveDot() PointF OctaveDot::pagePos() const { - System* system = chord()->measure()->system(); + System* system = note()->chord()->measure()->system(); double yp = y() + system->staff(staffIdx())->y() + system->y(); return PointF(pageX(), yp); } -//--------------------------------------------------------- -// measureXPos -//--------------------------------------------------------- - -double OctaveDot::measureXPos() const -{ - double xp = x(); // chord relative - xp += chord()->x(); // segment relative - xp += chord()->segment()->x(); // measure relative - return xp; -} - //--------------------------------------------------------- // spatiumChanged //--------------------------------------------------------- @@ -79,14 +68,14 @@ void OctaveDot::spatiumChanged(double oldValue, double newValue) } //--------------------------------------------------------- -// chord +// note //--------------------------------------------------------- -Chord* OctaveDot::chord() const +Note* OctaveDot::note() const { auto* parent = explicitParent(); - if (parent && parent->isChord()) { - return toChord(parent); + if (parent && parent->isNote()) { + return toNote(parent); } else { return nullptr; } diff --git a/src/engraving/dom/octavedot.h b/src/engraving/dom/octavedot.h index 4c2446b33ad6..bf25aa0ab5bb 100644 --- a/src/engraving/dom/octavedot.h +++ b/src/engraving/dom/octavedot.h @@ -50,13 +50,11 @@ class OctaveDot final : public EngravingItem OctaveDot* clone() const override { return new OctaveDot(*this); } PointF pagePos() const override; ///< position in page coordinates - Chord* chord() const; + Note* note() const; double len() const { return m_len; } void setLen(double v) { m_len = v; } - double measureXPos() const; - void spatiumChanged(double /*oldValue*/, double /*newValue*/) override; struct LayoutData : public EngravingItem::LayoutData { diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index aabbdc5a463c..cf8e0ea4a8dd 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1755,18 +1755,13 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) bool isJianpu = st->isJianpuStaff(tick); int baseOctave = 3; // Default base octave for Jianpu - auto cleanupOctaveDots = [item]() { - muse::DeleteAll(item->octaveDots()); - item->octaveDots().clear(); - }; - - // need octave dots? - if (!isJianpu) { - cleanupOctaveDots(); - return; - } + if (Note* note = item->upNote(); note) { + // need octave dots? + if (!isJianpu) { + note->resizeOctaveDotsTo(0); + return; + } - if (const Note* note = item->upNote(); note) { int dots = 0; double offsetY = 0; double distance = item->spatium() * .3; @@ -1785,7 +1780,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) offsetY += lines.back()->pos().y() + lines.back()->height() * .5; } } else { - cleanupOctaveDots(); + note->resizeOctaveDotsTo(0); return; } @@ -1793,19 +1788,19 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) double minX = note->pos().x() + note->bboxXShift(); double maxX = minX + hw; - item->resizeOctaveDotsTo(dots); + note->resizeOctaveDotsTo(dots); for (int i = 0; i < dots; ++i) { - OctaveDot* dot = item->octaveDots()[i]; - dot->setParent(item); + OctaveDot* dot = note->octaveDots()[i]; + dot->setParent(note); dot->setTrack(track); dot->setVisible(staffVisible); dot->setLen(maxX - minX); dot->setPos(minX, offsetY + i * distance); } - } - for (OctaveDot* od : item->octaveDots()) { - TLayout::layoutOctaveDot(od); + for (OctaveDot* od : note->octaveDots()) { + TLayout::layoutOctaveDot(od); + } } } @@ -2458,6 +2453,7 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ for (Chord* grace : chord->graceNotes()) { AccidentalsLayout::layoutAccidentals({ grace }, ctx); } + ChordLayout::layoutOctaveDots(chord, ctx); } } diff --git a/src/engraving/rendering/score/slurtielayout.cpp b/src/engraving/rendering/score/slurtielayout.cpp index 549c5e7b84ef..fd69bb86e31b 100644 --- a/src/engraving/rendering/score/slurtielayout.cpp +++ b/src/engraving/rendering/score/slurtielayout.cpp @@ -397,28 +397,17 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) ChordRest* ecr = item->endCR(); Chord* sc = 0; Note* note1 = 0; - double octDotY1 = 0.0; if (scr->isChord()) { sc = toChord(scr); note1 = item->up() || useJianpu ? sc->upNote() : sc->downNote(); - if (!sc->octaveDots().empty()) { - octDotY1 = sc->octaveDots().front()->ldata()->pos().y(); - } } Chord* ec = 0; Note* note2 = 0; - double octDotY2 = 0.0; if (ecr->isChord()) { ec = toChord(ecr); note2 = item->up() || useJianpu ? ec->upNote() : ec->downNote(); - if (!ec->octaveDots().empty()) { - octDotY2 = ec->octaveDots().front()->ldata()->pos().y(); - } } - // Slur for jianpu is always horizontal above dots - double octDotY = std::min(0.0, std::min(octDotY1, octDotY2)); - sp->system1 = scr->measure()->system(); sp->system2 = ecr->measure()->system(); @@ -567,6 +556,9 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) break; } + // Slur for jianpu is always horizontal above chords + double jianpuY = std::min(scr->ldata()->bbox().top(), ecr->ldata()->bbox().top()); + // // default position: // horizontal: middle of notehead @@ -580,7 +572,7 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) // default positions po.rx() = hw1 * .5 + (note1 ? note1->bboxXShift() : 0.0); if (useJianpu) { - po.ry() = scr->ldata()->bbox().top() + octDotY; + po.ry() = jianpuY; } else if (note1) { po.ry() = note1->pos().y(); } else if (item->up()) { @@ -718,7 +710,7 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) // default positions po.rx() = hw2 * .5 + (note2 ? note2->bboxXShift() : 0.0); if (useJianpu) { - po.ry() = scr->ldata()->bbox().top() + octDotY; + po.ry() = jianpuY; } else if (note2) { po.ry() = note2->pos().y(); } else if (item->up()) { @@ -1625,24 +1617,14 @@ void SlurTieLayout::correctForJianpu(Tie* item, SlurTiePos& sPos) return; } - double y1 = 0.0; Chord* chord1 = item->startNote()->chord(); - if (chord1) { - y1 = chord1->ldata()->bbox().top(); - if (!chord1->octaveDots().empty()) { - y1 += chord1->octaveDots().front()->ldata()->pos().y(); - } - } - - double y2 = 0.0; Chord* chord2 = item->endNote()->chord(); - if (chord2) { - y2 = chord2->ldata()->bbox().top(); - if (!chord2->octaveDots().empty()) { - y2 += chord2->octaveDots().front()->ldata()->pos().y(); - } + if (!chord1 || !chord2) { + return; } + double y1 = chord1->ldata()->bbox().top(); + double y2 = chord2->ldata()->bbox().top(); double minY = std::min(y1, y2); sPos.p1 += PointF(0, minY); sPos.p2 += PointF(0, minY); diff --git a/src/engraving/rendering/score/systemlayout.cpp b/src/engraving/rendering/score/systemlayout.cpp index 66ad41e86804..f9d8ab10e16d 100644 --- a/src/engraving/rendering/score/systemlayout.cpp +++ b/src/engraving/rendering/score/systemlayout.cpp @@ -1248,7 +1248,6 @@ void SystemLayout::layoutSystemElements(System* system, LayoutContext& ctx) ChordLayout::layoutArticulations2(c, ctx); ChordLayout::layoutChordBaseFingering(c, system, ctx); ChordLayout::layoutDurationLines(c, ctx); - ChordLayout::layoutOctaveDots(c, ctx); } layoutTuplets(elementsToLayout.chordRests, ctx); diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index 440147b94da5..c052c1736b53 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -4254,6 +4254,10 @@ void TLayout::fillNoteShape(const Note* item, Note::LayoutData* ldata) } } + for (OctaveDot* octaveDot : item->octaveDots()) { + shape.add(octaveDot->ldata()->bbox().translated(octaveDot->pos()), octaveDot); + } + ldata->setShape(shape); } @@ -4273,7 +4277,7 @@ void TLayout::layoutOctaveDot(OctaveDot* item) { LAYOUT_CALL_ITEM(item); OctaveDot::LayoutData* ldata = item->mutldata(); - ldata->setMag(item->chord()->mag()); + ldata->setMag(item->note()->mag()); ldata->radius = item->defaultSpatium() * .1; // TODO: style double diameter = ldata->radius * 2; From 8aa7baba6ebdfa914889b4a3b24cf41d18f0abf7 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Sat, 7 Feb 2026 18:53:50 +1100 Subject: [PATCH 07/24] Show chord in vertical jianpu. Support transposing instrument. Fix some chord lines --- src/engraving/dom/octavedot.cpp | 12 +- src/engraving/dom/octavedot.h | 7 +- src/engraving/rendering/score/chordlayout.cpp | 109 ++++++++++++------ src/engraving/rendering/score/restlayout.cpp | 48 +++++--- src/engraving/rendering/score/restlayout.h | 3 +- src/engraving/rendering/score/tdraw.cpp | 3 +- src/engraving/rendering/score/tlayout.cpp | 49 ++++---- 7 files changed, 137 insertions(+), 94 deletions(-) diff --git a/src/engraving/dom/octavedot.cpp b/src/engraving/dom/octavedot.cpp index 1add8c35f436..c583a17ab6ef 100644 --- a/src/engraving/dom/octavedot.cpp +++ b/src/engraving/dom/octavedot.cpp @@ -41,23 +41,13 @@ OctaveDot::OctaveDot(EngravingItem* s) { setSelectable(false); m_len = 0.; + m_above = false; } OctaveDot::~OctaveDot() { } -//--------------------------------------------------------- -// pagePos -//--------------------------------------------------------- - -PointF OctaveDot::pagePos() const -{ - System* system = note()->chord()->measure()->system(); - double yp = y() + system->staff(staffIdx())->y() + system->y(); - return PointF(pageX(), yp); -} - //--------------------------------------------------------- // spatiumChanged //--------------------------------------------------------- diff --git a/src/engraving/dom/octavedot.h b/src/engraving/dom/octavedot.h index bf25aa0ab5bb..b6d334010e25 100644 --- a/src/engraving/dom/octavedot.h +++ b/src/engraving/dom/octavedot.h @@ -32,7 +32,7 @@ class Chord; // @@ OctaveDot /// Graphic representation of a dot abover or under jianpu note. //! -//! parent: Chord +//! parent: Node //! x-origin: Chord //! y-origin: SStaff //--------------------------------------------------------- @@ -49,12 +49,14 @@ class OctaveDot final : public EngravingItem OctaveDot* clone() const override { return new OctaveDot(*this); } - PointF pagePos() const override; ///< position in page coordinates Note* note() const; double len() const { return m_len; } void setLen(double v) { m_len = v; } + bool above() const { return m_above; } + void setAbove(bool v) { m_above = v; } + void spatiumChanged(double /*oldValue*/, double /*newValue*/) override; struct LayoutData : public EngravingItem::LayoutData { @@ -65,6 +67,7 @@ class OctaveDot final : public EngravingItem private: double m_len = 0.0; + bool m_above = false; }; } // namespace mu::engraving #endif diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index cf8e0ea4a8dd..1522d255ee5c 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -131,16 +131,48 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) // https://github.com/musescore/MuseScore/issues/8970 std::vector chordAccidentals; - Note* upNote = item->upNote(); const bool isJianpuStaff = item->staff() && item->staff()->isJianpuStaff(item->tick()); - for (Note* note : item->notes()) { - if (isJianpuStaff && note != upNote) { - // Jianpu only shows the up note - continue; + auto notes = item->notes(); + if (isJianpuStaff) { + std::sort(notes.begin(), notes.end(), [](const Note* n1, const Note* n2) { + return noteIsBefore(n2, n1); // highest pitch on top + }); + } + + double jianpuOffsetY = upnote ? -upnote->spatium() * .5 : 0.0; + for (Note* note : notes) { + Note::LayoutData* ldata = note->mutldata(); + TLayout::layoutNote(note, ldata); + + if (isJianpuStaff) { + std::vector dots = note->octaveDots(); + double dotDistance = note->spatium() * .3; // TODO: jianpu style setting for distance of octave dots + + // Move the current note down if it is has octave dots above it + if (note != upnote && !dots.empty() && dots.back()->above()) { + jianpuOffsetY += dots.size() * dotDistance; + } + + // Jianpu digits in same chord will be vertically stacked + ldata->moveY(jianpuOffsetY); + + // Move the next note down if current note has octave dots in below + if (!dots.empty() && !dots.back()->above()) { + jianpuOffsetY += dots.size() * dotDistance; + } + + if (note == upnote) { + auto& durationLines = item->durationLines(); + if (!durationLines.empty() && durationLines.back()->halving()) { + jianpuOffsetY += durationLines.size() * dotDistance; + } + } + + jianpuOffsetY += ldata->bbox().height(); + jianpuOffsetY += note->spatium() * .5; // TODO: jianpu style setting for distance of notes } - TLayout::layoutNote(note, note->mutldata()); double x1 = note->pos().x() + chordX; double x2 = x1 + note->headWidth(); lll = std::max(lll, -x1); @@ -1663,9 +1695,9 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) Fraction tick = segment->tick(); staff_idx_t idx = item->staffIdx() + item->staffMove(); track_idx_t track = staff2track(idx); - const Staff* st = ctx.dom().staff(idx); - bool staffVisible = !st->isLinesInvisible(tick); - bool isJianpu = st->isJianpuStaff(tick); + const Staff* staff = ctx.dom().staff(idx); + bool staffVisible = !staff->isLinesInvisible(tick); + bool isJianpu = staff->isJianpuStaff(tick); if (!isJianpu) { return; @@ -1707,6 +1739,9 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) } double distance = item->spatium() * .3; + const StaffType* st = staff->staffTypeForElement(item); + double height = st->jianpuBoxH() * item->magS(); + double diminutionY = height * .5 - item->spatium() * .5; item->resizeDurationLinesTo(std::max(augmentationLines, diminutionLines)); for (int i = 0; i < diminutionLines; ++i) { DurationLine* dl = item->durationLines()[i]; @@ -1718,20 +1753,18 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) } double minX = hx - prevOffsetX; // Extend to join to previous duration line dl->setLen(maxX - minX); - dl->setPos(minX, (i + 1) * distance); + dl->setPos(minX, diminutionY + (i + 1) * distance); dl->setHalving(true); // Halving duration } - const StaffType* jianpu = st->staffTypeForElement(item); - double height = jianpu->jianpuBoxH() * item->magS(); - + double lengtheningY = -item->spatium() * .5; for (int i = 0; i < augmentationLines; ++i) { DurationLine* dl = item->durationLines()[i]; dl->setParent(item); dl->setTrack(track); dl->setVisible(staffVisible); dl->setLen(hw); - dl->setPos(hx + hw * 1.2 * (i + 1), -height * .5); + dl->setPos(hx + hw * 1.2 * (i + 1), lengtheningY); dl->setHalving(false); // Lengthening duration } } @@ -1750,38 +1783,38 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) Fraction tick = segment->tick(); staff_idx_t idx = item->staffIdx() + item->staffMove(); track_idx_t track = staff2track(idx); - const Staff* st = ctx.dom().staff(idx); - bool staffVisible = !st->isLinesInvisible(tick); - bool isJianpu = st->isJianpuStaff(tick); - int baseOctave = 3; // Default base octave for Jianpu - - if (Note* note = item->upNote(); note) { - // need octave dots? - if (!isJianpu) { - note->resizeOctaveDotsTo(0); - return; - } + const Staff* staff = ctx.dom().staff(idx); + bool staffVisible = !staff->isLinesInvisible(tick); + bool isJianpu = staff->isJianpuStaff(tick); + if (!isJianpu) { + return; + } + + const StaffType* st = staff->staffTypeForElement(item); + double height = st->jianpuBoxH() * item->magS(); + int baseOctave = 3; // Default base octave for Jianpu is C3 + for (Note* note : item->notes()) { int dots = 0; double offsetY = 0; double distance = item->spatium() * .3; int octave = note->octave(); if (octave > baseOctave) { dots = octave - baseOctave; - const StaffType* jianpu = st->staffTypeForElement(item); - double height = jianpu->jianpuBoxH() * item->magS(); - offsetY = -(height + dots * distance); + offsetY = -(height * .5 + dots * distance); } else if (octave < baseOctave) { dots = baseOctave - octave; - offsetY = distance; - auto lines = item->durationLines(); - // The octave dot should be under the halving lines - if (lines.size() > 0 && lines.back()->halving()) { - offsetY += lines.back()->pos().y() + lines.back()->height() * .5; + offsetY = height * .5 + distance; + if (note == item->upNote()) { + // The octave dot should be under the halving lines + auto lines = item->durationLines(); + if (lines.size() > 0 && lines.back()->halving()) { + offsetY += lines.size() * distance; + } } } else { note->resizeOctaveDotsTo(0); - return; + continue; } double hw = note->headWidth(); @@ -1794,6 +1827,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) dot->setParent(note); dot->setTrack(track); dot->setVisible(staffVisible); + dot->setAbove(octave > baseOctave); dot->setLen(maxX - minX); dot->setPos(minX, offsetY + i * distance); } @@ -2453,6 +2487,7 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ for (Chord* grace : chord->graceNotes()) { AccidentalsLayout::layoutAccidentals({ grace }, ctx); } + ChordLayout::layoutDurationLines(chord, ctx); ChordLayout::layoutOctaveDots(chord, ctx); } } @@ -2641,7 +2676,9 @@ void ChordLayout::placeDots(const std::vector& chords, const std::vector } } } - if (!chord || (chord->staff()->isTabStaff(chord->tick()) && !chord->staff()->staffType(chord->tick())->stemThrough())) { + + Staff* staff = chord ? chord->staff() : nullptr; + if (!staff || (staff->isTabStaff(chord->tick()) && !staff->staffType(chord->tick())->stemThrough())) { return; } std::vector topDownNotes; @@ -2651,7 +2688,7 @@ void ChordLayout::placeDots(const std::vector& chords, const std::vector getNoteListForDots(chord, topDownNotes, bottomUpNotes, anchoredDots); for (Note* note : notes) { - bool onLine = !(note->line() & 1); + bool onLine = !(note->line() & 1) && !staff->isJianpuStaff(chord->tick()); if (onLine) { std::unordered_map alreadyAdded; bool finished = false; diff --git a/src/engraving/rendering/score/restlayout.cpp b/src/engraving/rendering/score/restlayout.cpp index 05822a7f2533..a9b48cd62880 100644 --- a/src/engraving/rendering/score/restlayout.cpp +++ b/src/engraving/rendering/score/restlayout.cpp @@ -116,7 +116,10 @@ void RestLayout::layoutRest(const Rest* item, Rest::LayoutData* ldata, const Lay } } - if (layoutDurationLines(item, ldata, ctx)) { + if (stf->isJianpuStaff(item->tick())) { + ldata->sym = SymId::noSym; + layoutRestForJianpu(item, ldata, ctx); + fillShape(item, ldata, ctx.conf()); return; } @@ -174,15 +177,15 @@ void RestLayout::fillShape(const Rest* item, Rest::LayoutData* ldata, const Layo } } -bool RestLayout::layoutDurationLines(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx) +void RestLayout::layoutRestForJianpu(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx) { Fraction tick = item->measure()->ticks(); const Staff* staff = item->staff(); - if (!staff->isJianpuStaff(tick)) { - return false; - } + const StaffType* st = staff->staffTypeForElement(item); - ldata->setPos(0.0, 0.0); + const double hx = ldata->bbox().x(); + const double hy = -item->spatium() * .5; + ldata->setPos(hx, hy); staff_idx_t idx = item->staffIdx() + item->staffMove(); track_idx_t track = staff2track(idx); @@ -193,46 +196,43 @@ bool RestLayout::layoutDurationLines(const Rest* item, Rest::LayoutData* ldata, if (augmentationLines == 0 && diminutionLines == 0) { muse::DeleteAll(item->durationLines()); const_cast(item)->durationLines().clear(); - return true; } muse::draw::Font font(item->staffType()->jianpuFont()); font.setPointSizeF(font.pointSizeF() * item->mag()); muse::draw::FontMetrics fm(font); - const double hw = fm.width(String(u"0")) / item->magS(); - const double hx = ldata->bbox().x(); + const double width = fm.width(String(u"0")) / item->magS(); + const double height = st->jianpuBoxH() * item->magS(); const double distance = item->spatium() * .3; const_cast(item)->resizeDurationLinesTo(std::max(augmentationLines, diminutionLines)); + RectF noteBBox = RectF(0, -height * .5, width, height); + ldata->setBbox(noteBBox); + for (int i = 0; i < diminutionLines; ++i) { DurationLine* dl = item->durationLines()[i]; dl->setParent(const_cast(item)); dl->setTrack(track); dl->setVisible(staffVisible); - dl->setLen(hw); - dl->setPos(hx, (i + 1) * distance); + dl->setLen(width); + dl->setPos(hx, hy + height * .5 + (i + 1) * distance); dl->setHalving(true); // Halving duration } - const StaffType* jianpu = staff->staffTypeForElement(item); - const double height = jianpu->jianpuBoxH() * item->magS(); - for (int i = 0; i < augmentationLines; ++i) { DurationLine* dl = item->durationLines()[i]; dl->setParent(const_cast(item)); dl->setTrack(track); dl->setVisible(staffVisible); - dl->setLen(hw); - dl->setPos(hx + hw * 1.2 * (i + 1), -height * .5); + dl->setLen(width); + dl->setPos(hx + width * 1.2 * (i + 1), hy); dl->setHalving(false); // Lengthening duration } for (DurationLine* dl : item->durationLines()) { TLayout::layoutDurationLine(dl, ctx); } - - return true; } void RestLayout::resolveVerticalRestConflicts(LayoutContext& ctx, Segment* segment, staff_idx_t staffIdx) @@ -721,7 +721,13 @@ void RestLayout::fillShape(const Rest* item, Rest::LayoutData* ldata) if ((!item->isGap() || item->debugDrawGap()) && !item->shouldNotBeDrawn()) { shape.add(ChordLayout::chordRestShape(item)); - shape.add(item->symBbox(ldata->sym), item); + + if (ldata->sym == SymId::noSym) { + shape.add(ldata->bbox(), item); + } else { + shape.add(item->symBbox(ldata->sym), item); + } + for (const NoteDot* dot : item->dotList()) { if (dot->addToSkyline()) { shape.add(item->symBbox(SymId::augmentationDot).translated(dot->pos()), dot); @@ -849,6 +855,10 @@ void RestLayout::updateSymbol(const Rest* item, Rest::LayoutData* ldata) { Fraction t = item->tick(); Staff* st = item->staff(); + if (st->isJianpuStaff(t)) { + return; + } + double lineDistance = st->lineDistance(t) * item->spatium(); int lines = st->lines(t); diff --git a/src/engraving/rendering/score/restlayout.h b/src/engraving/rendering/score/restlayout.h index faf0ccb6d6ef..709ff38f820b 100644 --- a/src/engraving/rendering/score/restlayout.h +++ b/src/engraving/rendering/score/restlayout.h @@ -41,7 +41,6 @@ class RestLayout public: static void layoutRest(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx); static void fillShape(const Rest* item, Rest::LayoutData* ldata, const LayoutConfiguration& conf); - static bool layoutDurationLines(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx); static void resolveVerticalRestConflicts(LayoutContext& ctx, Segment* segment, staff_idx_t staffIdx); static void resolveRestVSChord(std::vector& rests, std::vector& chords, const Staff* staff, Segment* segment); @@ -63,5 +62,7 @@ class RestLayout static RestGroups computeRestGroups(const System* system, LayoutContext& ctx); static InterruptionPoints computeInterruptionPoints(const Measure* measure, staff_idx_t staffIdx); + + static void layoutRestForJianpu(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx); }; } diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index e4f5072de5f1..76fb3b242035 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -2590,13 +2590,14 @@ void TDraw::draw(const Rest* item, Painter* painter, const PaintOptions& opt) if (jianpu) { const Staff* staff = item->staff(); const StaffType* staffType = staff->staffTypeForElement(item); + const double height = staffType->jianpuBoxH() * item->magS(); Font f(staffType->jianpuFont()); f.setPointSizeF(f.pointSizeF() * item->magS()); painter->setFont(f); painter->setPen(item->curColor(opt)); double startPosX = ldata->bbox().x(); - painter->drawText(PointF(startPosX, 0), String(u"0")); + painter->drawText(PointF(startPosX, height * .5), String(u"0")); return; } diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index c052c1736b53..16bc2a2d9e3d 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -4160,16 +4160,14 @@ void TLayout::layoutNote(const Note* item, Note::LayoutData* ldata) const StaffType* jianpu = st->staffTypeForElement(item); KeySigEvent ks = st->keySigEvent(item->chord()->tick()); - int tpc = pitch2tpc(item->pitch(), ks.key(), Prefer::NEAREST); - String accName, stepName; - tpc2Function(tpc, ks.key(), accName, stepName); + tpc2Function(item->tpc(), ks.key(), accName, stepName); const_cast(item)->setJianpuDigit(String(u"%1").arg(stepName)); double width = item->headWidth(); double height = jianpu->jianpuBoxH() * item->magS(); - noteBBox = RectF(0, -height, width, height); + noteBBox = RectF(0, -height * .5, width, height); } else { if (item->deadNote()) { const_cast(item)->setHeadGroup(NoteHeadGroup::HEAD_CROSS); @@ -4742,9 +4740,8 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) RectF jianpuBbox(noteheadBbox.x(), 0, noteheadBbox.width(), 0); if (isJianpu) { const Staff* staff = item->staff(); - const StaffType* jianpu = staff->staffTypeForElement(item); - int tpc = 0; - NoteVal nval; + const StaffType* st = staff->staffTypeForElement(item); + int dots = 0; // jianpu digit if (item->isRest()) { @@ -4754,17 +4751,28 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) Score* score = item->score(); score->getPosition(&pos, item->pos(), item->track()); bool error = false; - nval = score->noteValForPosition(pos, item->accidentalType(), error); - - KeySigEvent ks = staff->keySigEvent(item->tick()); - tpc = pitch2tpc(nval.pitch, ks.key(), Prefer::NEAREST); - - String accName, stepName; - tpc2Function(tpc, ks.key(), accName, stepName); - item->setJianpuDigit(String(u"%1").arg(stepName)); + NoteVal nval = score->noteValForPosition(pos, item->accidentalType(), error); + if (!error) { + bool concertPitch = ctx.conf().styleB(Sid::concertPitch); + int tpc = nval.tpc(concertPitch); + + if (tpc != Tpc::TPC_INVALID) { + String accName, stepName; + KeySigEvent ks = staff->keySigEvent(item->tick()); + tpc2Function(tpc, ks.key(), accName, stepName); + item->setJianpuDigit(String(u"%1").arg(stepName)); + + Interval transpose = item->part()->instrument(item->tick())->transpose(); + int alteration = static_cast(tpc2alter(tpc)); + int epitch = nval.pitch - transpose.chromatic; + int octave = (epitch - alteration) / 12 - 1; // See Note::octave + int baseOctave = 3; // Default base octave for Jianpu is C3 + dots = baseOctave - octave; + } + } } double distance = _spatium * .3; - jianpuBbox.setHeight(jianpu->jianpuBoxH() * mag + distance); + jianpuBbox.setHeight(st->jianpuBoxH() * mag + distance); if (!up) { jianpuBbox.setY(noteheadBbox.y() - jianpuBbox.height()); // Jianpu is above the head note } else { @@ -4775,14 +4783,7 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) int lines = item->duration().diminutionLines(); item->setJianpuDurationLine(lines); - // jianpu dot - int dots = 0; - if (!item->isRest()) { - int baseOctave = 3; - int alteration = static_cast(tpc2alter(tpc)); - int octave = (nval.pitch - alteration) / 12 - 1; // See Note::octave - dots = baseOctave - octave; - } + // jianpu octave dots item->setJianpuOctaveDot(dots); // Always has one extra distance From 0a89c4aa12de3f4a426c47032cd192b85f307464 Mon Sep 17 00:00:00 2001 From: Song <8042214+songzhenke@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:35:45 +1100 Subject: [PATCH 08/24] Jianpu config and use beam (#2) * jianpu text style * apply jianpu style * jianpu diminution beam --- src/engraving/dom/durationline.h | 4 - src/engraving/dom/engravingitem.cpp | 6 + src/engraving/dom/engravingitem.h | 1 + src/engraving/dom/shadownote.h | 13 +- src/engraving/dom/stafftype.cpp | 62 +- src/engraving/dom/stafftype.h | 15 +- src/engraving/rendering/score/beamlayout.cpp | 59 +- .../rendering/score/beamtremololayout.cpp | 69 +- src/engraving/rendering/score/chordlayout.cpp | 124 ++-- src/engraving/rendering/score/restlayout.cpp | 52 +- src/engraving/rendering/score/tdraw.cpp | 24 +- src/engraving/rendering/score/tlayout.cpp | 25 +- src/engraving/rendering/score/tlayout.h | 2 +- src/engraving/rw/read460/tread.cpp | 2 + src/engraving/rw/write/twrite.cpp | 3 + src/engraving/style/style.cpp | 12 + src/engraving/style/styledef.cpp | 22 + src/engraving/style/styledef.h | 21 + src/engraving/style/textstyle.cpp | 20 + src/engraving/tests/beam_data/jianpuBeam.mscx | 590 ++++++++++++++++++ src/engraving/tests/beam_tests.cpp | 5 + src/engraving/types/types.h | 1 + src/engraving/types/typesconv.cpp | 1 + .../internal/import/importmusicxmlpass1.cpp | 3 +- src/notationscene/widgets/editstyle.cpp | 9 + src/notationscene/widgets/editstyle.ui | 251 ++++++++ src/notationscene/widgets/editstyleutils.cpp | 3 + 27 files changed, 1210 insertions(+), 189 deletions(-) create mode 100644 src/engraving/tests/beam_data/jianpuBeam.mscx diff --git a/src/engraving/dom/durationline.h b/src/engraving/dom/durationline.h index be8b1417b030..678eac617164 100644 --- a/src/engraving/dom/durationline.h +++ b/src/engraving/dom/durationline.h @@ -56,9 +56,6 @@ class DurationLine final : public EngravingItem double len() const { return m_len; } void setLen(double v) { m_len = v; } - void setHalving(bool v) { m_halving = v; } - bool halving() const { return m_halving; } - void spatiumChanged(double /*oldValue*/, double /*newValue*/) override; struct LayoutData : public EngravingItem::LayoutData { @@ -69,7 +66,6 @@ class DurationLine final : public EngravingItem private: double m_len = 0.0; - bool m_halving = true; }; } // namespace mu::engraving #endif diff --git a/src/engraving/dom/engravingitem.cpp b/src/engraving/dom/engravingitem.cpp index 4867eb2c7b3c..55c098d3acd0 100644 --- a/src/engraving/dom/engravingitem.cpp +++ b/src/engraving/dom/engravingitem.cpp @@ -464,6 +464,12 @@ bool EngravingItem::onTabStaff() const return stt ? stt->isTabStaff() : false; } +bool EngravingItem::isJianpuStaff() const +{ + const Staff* st = staff(); + return st ? st->isJianpuStaff(tick()) : false; +} + bool EngravingItem::hasGrips() const { return gripsCount() > 0; diff --git a/src/engraving/dom/engravingitem.h b/src/engraving/dom/engravingitem.h index c0ffdc6b21ef..b75fc36f5600 100644 --- a/src/engraving/dom/engravingitem.h +++ b/src/engraving/dom/engravingitem.h @@ -340,6 +340,7 @@ class EngravingItem : public EngravingObject bool hasStaff() const; const StaffType* staffType() const; bool onTabStaff() const; + bool isJianpuStaff() const; Part* part() const; virtual void add(EngravingItem*); diff --git a/src/engraving/dom/shadownote.h b/src/engraving/dom/shadownote.h index 17cba1f5a2bf..05d40c5e573f 100644 --- a/src/engraving/dom/shadownote.h +++ b/src/engraving/dom/shadownote.h @@ -84,12 +84,11 @@ class ShadowNote final : public EngravingItem String jianpuDigit() const { return m_jianpuDigit; } void setJianpuDigit(const String& s) { m_jianpuDigit = s; } - int jianpuDurationLine() const { return m_jianpuDurationLine; } - void setJianpuDurationLine(int lines) { m_jianpuDurationLine = lines; } - - int jianpuOctaveDot() const { return m_jianpuOctaveDot; } - void setJianpuOctaveDot(int dots) { m_jianpuOctaveDot = dots; } + int jianpuDiminutionLines() const { return m_jianpuDiminutionLines; } + void setJianpuDiminutionLines(int lines) { m_jianpuDiminutionLines = lines; } + int jianpuOctaveDots() const { return m_jianpuOctaveDots; } + void setJianpuOctaveDots(int dots) { m_jianpuOctaveDots = dots; } private: Fraction m_tick; @@ -101,7 +100,7 @@ class ShadowNote final : public EngravingItem std::set m_articulationIds; bool m_beyondScore = false; String m_jianpuDigit; - int m_jianpuDurationLine = 0; - int m_jianpuOctaveDot = 0; + int m_jianpuDiminutionLines = 0; + int m_jianpuOctaveDots = 0; }; } // namespace mu::engraving diff --git a/src/engraving/dom/stafftype.cpp b/src/engraving/dom/stafftype.cpp index 4c690ac98ddc..9d4374960602 100644 --- a/src/engraving/dom/stafftype.cpp +++ b/src/engraving/dom/stafftype.cpp @@ -81,6 +81,7 @@ StaffType::StaffType() m_symRepeat = TablatureSymbolRepeat::NEVER; setDurationFontName(m_durationFonts[0].displayName); setFretPresetIdx(0); + setJianpuTextStyle(TextStyleType::JIANPU_NUMBER); } StaffType::StaffType(StaffGroup sg, const String& xml, const String& name, int lines, int stpOff, double lineDist, @@ -99,10 +100,7 @@ StaffType::StaffType(StaffGroup sg, const String& xml, const String& name, int l m_genTimesig(genTimeSig), m_genKeysig(genKeySig) { - // Jianpu temporarly uses TAB fret font - if (xml == u"stdJianpu") { - setFretTextStyle(TextStyleType::TAB_FRET_NUMBER); - } + setJianpuTextStyle(TextStyleType::JIANPU_NUMBER); } StaffType::StaffType(StaffGroup sg, const String& xml, const String& name, int lines, int stpOff, double lineDist, @@ -229,7 +227,10 @@ bool StaffType::operator==(const StaffType& st) const equal &= (m_fretUseTextStyle == st.m_fretUseTextStyle); equal &= (m_fretTextStyle == st.m_fretTextStyle); equal &= (m_fretPresetIdx == st.m_fretPresetIdx); - + equal &= (m_jianpuTextStyle == st.m_jianpuTextStyle); + equal &= (m_jianpuFont == st.m_jianpuFont); + equal &= (m_jianpuFontSize == st.m_jianpuFontSize); + equal &= (m_jianpuBoxH == st.m_jianpuBoxH); return equal; } @@ -328,6 +329,8 @@ bool StaffType::isHiddenElementOnTab(Sid commonTabStyle, Sid simpleTabStyle) con void StaffType::styleChanged() { + setJianpuTextStyle(m_jianpuTextStyle); + if (!m_fretUseTextStyle) { return; } @@ -351,6 +354,37 @@ void StaffType::setUseNumbers(bool val) setFretMetrics(); } +void StaffType::setJianpuTextStyle(const TextStyleType& val) +{ + m_jianpuTextStyle = val; + m_jianpuFont = Font(); + + const TextStyle* ts = textStyle(m_jianpuTextStyle); + for (const TextStyleProperty property : *ts) { + switch (property.type) { + case TextStylePropertyType::FontFace: { + String fontName = style().styleSt(property.sid); + m_jianpuFont.setFamily(fontName, Font::Type::Tablature); + } break; + case TextStylePropertyType::FontSize: { + double fontSize = style().styleD(property.sid); + setJianpuFontSize(fontSize); + } break; + case TextStylePropertyType::FontStyle: { + FontStyle fStyle = style().styleV(property.sid).value(); + m_jianpuFont.setBold(fStyle & FontStyle::Bold); + m_jianpuFont.setItalic(fStyle & FontStyle::Italic); + m_jianpuFont.setUnderline(fStyle & FontStyle::Underline); + m_jianpuFont.setStrike(fStyle & FontStyle::Strike); + } break; + default: + continue; + } + } + + setJianpuMetrics(); +} + void StaffType::setFretTextStyle(const TextStyleType& val) { m_fretTextStyle = val; @@ -496,6 +530,17 @@ void StaffType::setFretMetrics() m_deadFretBoxY = deadBb.y() + m_deadFretYOffset; } +void StaffType::setJianpuMetrics() +{ + FontMetrics fm(jianpuFont()); + + // compute total height of used characters + String txt = u"01234567"; + RectF bb = fm.tightBoundingRect(txt); + + m_jianpuBoxH = bb.height(); +} + //--------------------------------------------------------- // setDurationFontName //--------------------------------------------------------- @@ -589,6 +634,13 @@ void StaffType::setFretFontSize(double val) setFretMetrics(); } +void StaffType::setJianpuFontSize(double val) +{ + m_jianpuFontSize = val; + m_jianpuFont.setPointSizeF(val); + setJianpuMetrics(); +} + //--------------------------------------------------------- // fretString / durationString // diff --git a/src/engraving/dom/stafftype.h b/src/engraving/dom/stafftype.h index 985228e1c99d..4dce576cb70d 100644 --- a/src/engraving/dom/stafftype.h +++ b/src/engraving/dom/stafftype.h @@ -302,10 +302,13 @@ class StaffType void setFretTextStyle(const TextStyleType& val); void setFretPresetIdx(size_t idx); void setFretPreset(const String& str); + void setJianpuTextStyle(const TextStyleType& val); + void setJianpuFontSize(double val); - const muse::draw::Font& jianpuFont() const { return m_fretFont; } // Jianpu temporarly uses fret font - double jianpuFontSize() const { return fretFontSize(); } // Jianpu temporarly uses fret font - double jianpuBoxH() const { return fretBoxH(); } // Jianpu temporarly uses fret font + TextStyleType jianpuTextStyle() const { return m_jianpuTextStyle; } + const muse::draw::Font& jianpuFont() const { return m_jianpuFont; } + double jianpuFontSize() const { return m_jianpuFontSize; } + double jianpuBoxH() const { return m_jianpuBoxH; } bool isTabStaff() const { return m_group == StaffGroup::TAB; } bool isDrumStaff() const { return m_group == StaffGroup::PERCUSSION; } @@ -332,6 +335,7 @@ class StaffType double defaultSpatium() const; void setDurationMetrics(); void setFretMetrics(); + void setJianpuMetrics(); static bool readTabConfigFile(const String& fileName); @@ -412,6 +416,11 @@ class StaffType // (raster units); internally computed: depends upon _onString, _useNumbers // and the metrics of the fret font + TextStyleType m_jianpuTextStyle = TextStyleType::JIANPU_NUMBER; + muse::draw::Font m_jianpuFont; // font used to draw jianpu; cached for efficiency + double m_jianpuFontSize = 9.0; // the size (in points) for the jianpu font + double m_jianpuBoxH = 0.0; + // the array of configured fonts static std::vector m_fretFonts; static std::vector m_durationFonts; diff --git a/src/engraving/rendering/score/beamlayout.cpp b/src/engraving/rendering/score/beamlayout.cpp index f46c69e9e8af..83b0e902bc7c 100644 --- a/src/engraving/rendering/score/beamlayout.cpp +++ b/src/engraving/rendering/score/beamlayout.cpp @@ -60,11 +60,6 @@ void BeamLayout::layout(Beam* item, const LayoutContext& ctx) { TRACEFUNC; - const Staff* staff = item->staff(); - if (staff->isJianpuStaff(item->tick())) { - return; - } - Beam::LayoutData* ldata = item->mutldata(); // all of the beam layout code depends on _elements being in order by tick // this may not be the case if two cr's were recently swapped. @@ -311,23 +306,26 @@ void BeamLayout::layout2(Beam* item, const LayoutContext& ctx, const std::vector TRACEFUNC; BeamTremoloLayout::setupLData(item, item->mutldata(), ctx); - Chord* startChord = nullptr; - Chord* endChord = nullptr; + ChordRest* startCr = nullptr; + ChordRest* endCr = nullptr; if (chordRests.empty()) { return; } for (ChordRest* chordRest : chordRests) { - if (chordRest->isChord()) { - if (!startChord) { - startChord = toChord(chordRest); - endChord = startChord; + if (chordRest->isChord() || chordRest->isJianpuStaff()) { + if (!startCr) { + startCr = chordRest; + endCr = startCr; } else { - endChord = toChord(chordRest); + endCr = chordRest; + } + + if (chordRest->isChord()) { + ChordLayout::layoutStem(toChord(chordRest), ctx); } - ChordLayout::layoutStem(toChord(chordRest), ctx); } } - if (!startChord) { + if (!startCr) { // we were passed a vector of only rests. we don't support beams across only rests // this beam will be deleted in LayoutBeams return; @@ -337,8 +335,8 @@ void BeamLayout::layout2(Beam* item, const LayoutContext& ctx, const std::vector item->setBeamDist((item->beamSpacing() / 4.0) * item->spatium() * item->mag()); item->setBeamWidth(item->absoluteFromSpatium(ctx.conf().styleS(Sid::beamWidth)) * item->mag()); - item->setStartAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), startChord, ChordBeamAnchorType::Start)); - item->setEndAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), endChord, ChordBeamAnchorType::End)); + item->setStartAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), startCr, ChordBeamAnchorType::Start)); + item->setEndAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), endCr, ChordBeamAnchorType::End)); if (item->isGrace()) { item->setBeamDist(item->beamDist() * ctx.conf().styleD(Sid::graceNoteMag)); @@ -369,7 +367,15 @@ void BeamLayout::layout2(Beam* item, const LayoutContext& ctx, const std::vector // anchor represents the middle of the beam, not the tip of the stem // location depends on _isBesideTabStaff - if (!item->isBesideTabStaff()) { + if (item->isJianpuStaff()) { + BeamTremoloLayout::setupLData(item, item->mutldata(), ctx); + BeamTremoloLayout::calculateAnchors(item, item->mutldata(), ctx, chordRests, item->notePositions()); + item->setStartAnchor(item->ldata()->startAnchor); + item->setEndAnchor(item->ldata()->endAnchor); + item->setSlope(0.0); + item->setBeamWidth(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->mag()); + item->setBeamDist(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->mag()); + } else if (!item->isBesideTabStaff()) { BeamTremoloLayout::setupLData(item, item->mutldata(), ctx); BeamTremoloLayout::calculateAnchors(item, item->mutldata(), ctx, chordRests, item->notePositions()); item->setStartAnchor(item->ldata()->startAnchor); @@ -729,6 +735,10 @@ void BeamLayout::createBeams(LayoutContext& ctx, Measure* measure) } bm = Groups::actualBeamMode(cr, prev, &beatSubdivision); + if (cr->isRest() && bm == BeamMode::NONE && cr->isJianpuStaff()) { + // Jianpu rest can be beamed + bm = BeamMode::AUTO; + } prev = cr; @@ -841,9 +851,11 @@ void BeamLayout::layoutNonCrossBeams(ChordRest* cr, LayoutContext& ctx) } } - for (ChordRest* beamCR : beam->elements()) { - if (beamCR->isRest() && beamCR->vStaffIdx() == beam->staffIdx()) { - verticalAdjustBeamedRests(toRest(beamCR), beam, ctx); + if (!cr->isJianpuStaff()) { + for (ChordRest* beamCR : beam->elements()) { + if (beamCR->isRest() && beamCR->vStaffIdx() == beam->staffIdx()) { + verticalAdjustBeamedRests(toRest(beamCR), beam, ctx); + } } } } @@ -971,7 +983,8 @@ void BeamLayout::createBeamSegments(Beam* item, const LayoutContext& ctx, const ChordRest* chordRest = chordRests[i]; ChordRest* prevChordRest = i < 1 ? nullptr : chordRests[i - 1]; - if (level < chordRest->beams() && !chordRest->isRest()) { + // Jianpu rest can be beamed like normal chords + if (level < chordRest->beams() && (!chordRest->isRest() || item->isJianpuStaff())) { levelHasBeam = true; } bool isBroken16 = false; @@ -1232,7 +1245,7 @@ void BeamLayout::createBeamSegment(Beam* item, ChordRest* startCr, ChordRest* en ++(beam->above ? beamsAbove : beamsBelow); } - const int upValue = overallUp ? -1 : 1; + const int upValue = (overallUp || item->isJianpuStaff()) ? -1 : 1; const int extraBeamAdjust = overallUp ? beamsAbove : beamsBelow; const double verticalOffset = item->beamDist() * (level - extraBeamAdjust) * upValue; @@ -1346,7 +1359,7 @@ void BeamLayout::createBeamletSegment(Beam* item, const LayoutContext& ctx, Chor } } - const int upValue = cr->up() ? -1 : 1; + const int upValue = (cr->up() || item->isJianpuStaff()) ? -1 : 1; const double verticalOffset = item->beamDist() * (level - extraBeamAdjust) * upValue; if (muse::RealIsEqual(item->growLeft(), item->growRight())) { diff --git a/src/engraving/rendering/score/beamtremololayout.cpp b/src/engraving/rendering/score/beamtremololayout.cpp index 4b276f044d16..26032cf4477b 100644 --- a/src/engraving/rendering/score/beamtremololayout.cpp +++ b/src/engraving/rendering/score/beamtremololayout.cpp @@ -645,6 +645,19 @@ bool BeamTremoloLayout::calculateAnchors(const BeamBase* item, BeamBase::LayoutD hasChordAboveBeam = true; } } + + if (item->isJianpuStaff()) { + ldata->startAnchor = chordBeamAnchor(ldata, startCr, ChordBeamAnchorType::Start); + ldata->endAnchor = chordBeamAnchor(ldata, endCr, ChordBeamAnchorType::End); + + double heigh = item->staff()->staffType()->jianpuBoxH() * item->mag(); + double distance = ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->mag(); + double offsetY = heigh * .5 + distance; // Jianpu Y origin is at the center of the number. + ldata->startAnchor += PointF(0, offsetY); + ldata->endAnchor += PointF(0, offsetY); + return true; + } + if (!startChord) { // we were passed a vector of only rests. we don't support beams across only rests // this beam will be deleted in LayoutBeams @@ -1292,53 +1305,61 @@ int BeamTremoloLayout::getBeamCount(const BeamBase::LayoutData* ldata, const std double BeamTremoloLayout::chordBeamAnchorX(const BeamBase::LayoutData* ldata, const ChordRest* cr, ChordBeamAnchorType anchorType) { + bool isJianpu = cr->isJianpuStaff(); double pagePosX = ldata->trem ? ldata->trem->pagePos().x() : ldata->beam ? ldata->beam->pagePos().x() : 0.0; - double stemPosX = StemLayout::stemPosX(cr) + cr->pagePos().x() - pagePosX; + double crPosX = cr->pagePos().x() - pagePosX; + auto stemPosX = [cr, crPosX]() { return StemLayout::stemPosX(cr) + crPosX; }; - if (!cr->isChord() || !toChord(cr)->stem()) { - if (!ldata->up) { + if (!isJianpu && (!cr->isChord() || !toChord(cr)->stem())) { + if (ldata->up) { + return stemPosX(); + } else { // rests always return the right side of the glyph as their stemPosX // so we need to adjust back to the left side if stems are down - stemPosX -= StemLayout::stemPosX(cr); + return stemPosX() - StemLayout::stemPosX(cr); } - return stemPosX; } - const Chord* chord = toChord(cr); - const Stem* stem = chord->stem(); + const Chord* chord = cr->isChord() ? toChord(cr) : nullptr; + const Stem* stem = chord ? chord->stem() : nullptr; - double stemWidth = stem->lineWidthMag(); + auto crWidth = [cr]() { return cr->ldata()->bbox().width(); }; + auto stemWidth = [stem]() { return stem ? stem->lineWidthMag() : 0.0; }; switch (anchorType) { case ChordBeamAnchorType::Start: + if (isJianpu) { + return crPosX; + } if (ldata->tab) { - return stemPosX - 0.5 * stemWidth; + return stemPosX() - 0.5 * stemWidth(); } - - if (chord->up()) { - return stemPosX - stemWidth; + if (chord && chord->up()) { + return stemPosX() - stemWidth(); } - break; case ChordBeamAnchorType::Middle: + if (isJianpu) { + return crPosX + crWidth() / 2; + } if (ldata->tab) { - return stemPosX; + return stemPosX(); } - - return chord->up() ? stemPosX - 0.5 * stemWidth : stemPosX + 0.5 * stemWidth; + return chord && chord->up() ? stemPosX() - 0.5 * stemWidth() : stemPosX() + 0.5 * stemWidth(); case ChordBeamAnchorType::End: + if (isJianpu) { + return crPosX + crWidth(); + } if (ldata->tab) { - return stemPosX + 0.5 * stemWidth; + return stemPosX() + 0.5 * stemWidth(); } - - if (!chord->up()) { - return stemPosX + stemWidth; + if (chord && !chord->up()) { + return stemPosX() + stemWidth(); } - break; } - return stemPosX; + return stemPosX(); } double BeamTremoloLayout::chordBeamAnchorY(const BeamBase::LayoutData* ldata, const ChordRest* cr) @@ -1348,6 +1369,10 @@ double BeamTremoloLayout::chordBeamAnchorY(const BeamBase::LayoutData* ldata, co } const Chord* chord = toChord(cr); + if (chord->isJianpuStaff()) { + return chord->upNote()->pagePos().y(); + } + Note* note = cr->up() ? chord->downNote() : chord->upNote(); PointF position = note->pagePos(); diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 1522d255ee5c..03c7e5bb9e5c 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -140,14 +140,16 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) }); } - double jianpuOffsetY = upnote ? -upnote->spatium() * .5 : 0.0; + // Jianpu Y origin is at the center of the number. + // It always starts half a spatium above, aligned with the middle of the measure line. + double jianpuOffsetY = upnote ? -upnote->spatium() * .5 * mag_ : 0.0; for (Note* note : notes) { Note::LayoutData* ldata = note->mutldata(); TLayout::layoutNote(note, ldata); if (isJianpuStaff) { std::vector dots = note->octaveDots(); - double dotDistance = note->spatium() * .3; // TODO: jianpu style setting for distance of octave dots + double dotDistance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * mag_; // Move the current note down if it is has octave dots above it if (note != upnote && !dots.empty() && dots.back()->above()) { @@ -163,14 +165,16 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) } if (note == upnote) { - auto& durationLines = item->durationLines(); - if (!durationLines.empty() && durationLines.back()->halving()) { - jianpuOffsetY += durationLines.size() * dotDistance; + Beam* beam = item->beam(); + if (beam) { + jianpuOffsetY += ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * mag_; + jianpuOffsetY -= ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * mag_; + jianpuOffsetY += beam->ldata()->bbox().height(); } } jianpuOffsetY += ldata->bbox().height(); - jianpuOffsetY += note->spatium() * .5; // TODO: jianpu style setting for distance of notes + jianpuOffsetY += ctx.conf().styleAbsolute(Sid::jianpuNumberVerticalDistance) * mag_; } double x1 = note->pos().x() + chordX; @@ -1703,70 +1707,30 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) return; } - int augmentationLines = item->durationType().augmentationLines(); - int diminutionLines = item->durationType().diminutionLines(); - if (augmentationLines == 0 && diminutionLines == 0) { + int lines = item->durationType().augmentationLines(); + if (lines == 0) { muse::DeleteAll(item->durationLines()); item->durationLines().clear(); return; } - if (const Note* note = item->upNote(); note) { - double hw = note->headWidth(); - double hx = note->pos().x() + note->bboxXShift(); - double maxX = hx + hw; + item->resizeDurationLinesTo(lines); - size_t prevLines = 0; // Number of duration lines in the previous chord - double prevOffsetX = .0; // Offset of the previous chord - bool first = true; - if (item->isChordRest()) { - Beam* beam = toChordRest(item)->beam(); - if (beam) { - ChordRest* cr = beam->elements().front(); - first = (cr && cr->isChord() && toChord(cr) == item); - } - } - - if (Chord* prev = item->prev(); prev&& !first) { - if (prev->durationLines().size() > 0 && prev->durationLines().back()->halving()) { - prevLines = prev->durationLines().size(); - auto currX = item->pagePos().x(); - auto prevX = prev->pagePos().x(); - if (currX > prevX) { - prevOffsetX = currX - prevX - hw; // Offset to join to previous duration line - } - } - } - - double distance = item->spatium() * .3; - const StaffType* st = staff->staffTypeForElement(item); - double height = st->jianpuBoxH() * item->magS(); - double diminutionY = height * .5 - item->spatium() * .5; - item->resizeDurationLinesTo(std::max(augmentationLines, diminutionLines)); - for (int i = 0; i < diminutionLines; ++i) { - DurationLine* dl = item->durationLines()[i]; - dl->setParent(item); - dl->setTrack(track); - dl->setVisible(staffVisible); - if (i >= static_cast(prevLines)) { - prevOffsetX = 0.0; // Do not join to duration line in previous chord - } - double minX = hx - prevOffsetX; // Extend to join to previous duration line - dl->setLen(maxX - minX); - dl->setPos(minX, diminutionY + (i + 1) * distance); - dl->setHalving(true); // Halving duration - } + const Note* note = item->upNote(); + if (!note) { + return; + } - double lengtheningY = -item->spatium() * .5; - for (int i = 0; i < augmentationLines; ++i) { - DurationLine* dl = item->durationLines()[i]; - dl->setParent(item); - dl->setTrack(track); - dl->setVisible(staffVisible); - dl->setLen(hw); - dl->setPos(hx + hw * 1.2 * (i + 1), lengtheningY); - dl->setHalving(false); // Lengthening duration - } + double hw = note->headWidth(); + double hx = note->pos().x() + note->bboxXShift(); + double lengtheningY = -item->spatium() * .5; + for (int i = 0; i < lines; ++i) { + DurationLine* dl = item->durationLines()[i]; + dl->setParent(item); + dl->setTrack(track); + dl->setVisible(staffVisible); + dl->setLen(hw); + dl->setPos(hx + hw * 1.2 * (i + 1), lengtheningY); } for (DurationLine* dl : item->durationLines()) { @@ -1797,7 +1761,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) for (Note* note : item->notes()) { int dots = 0; double offsetY = 0; - double distance = item->spatium() * .3; + double distance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * item->magS(); int octave = note->octave(); if (octave > baseOctave) { dots = octave - baseOctave; @@ -1806,10 +1770,12 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) dots = baseOctave - octave; offsetY = height * .5 + distance; if (note == item->upNote()) { - // The octave dot should be under the halving lines - auto lines = item->durationLines(); - if (lines.size() > 0 && lines.back()->halving()) { - offsetY += lines.size() * distance; + // The octave dot should be under the jianpu beam + Beam* beam = item->beam(); + if (beam) { + offsetY += ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->magS(); + offsetY -= ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->magS(); + offsetY += beam->ldata()->bbox().height(); } } } else { @@ -1833,7 +1799,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) } for (OctaveDot* od : note->octaveDots()) { - TLayout::layoutOctaveDot(od); + TLayout::layoutOctaveDot(od, ctx); } } } @@ -2795,6 +2761,26 @@ void ChordLayout::setDotRelativeLine(Note* note, int dotMove, LayoutContext& ctx int cdots = static_cast(note->chord()->dots()); int ndots = static_cast(note->dots().size()); + // remove dots for jianpu which already convert to lengthen duration lines + int ddots = 0; + if (note->staff()->isJianpuStaff(note->chord()->tick())) { + if (note->chord()->upNote() == note) { + TDuration durationType = note->chord()->durationType(); + if (durationType == DurationType::V_LONG) { + ddots = 4; + } else if (durationType == DurationType::V_BREVE) { + ddots = 3; + } else if (durationType == DurationType::V_WHOLE) { + ddots = 2; + } else if (durationType == DurationType::V_HALF) { + ddots = 1; + } + } else { + cdots = 0; // only show dots for up note in jianpu + } + } + cdots = std::max(0, cdots - ddots); + int n = cdots - ndots; for (int i = 0; i < n; ++i) { NoteDot* dot = Factory::createNoteDot(note); diff --git a/src/engraving/rendering/score/restlayout.cpp b/src/engraving/rendering/score/restlayout.cpp index a9b48cd62880..af6cdf8dc4ea 100644 --- a/src/engraving/rendering/score/restlayout.cpp +++ b/src/engraving/rendering/score/restlayout.cpp @@ -183,55 +183,43 @@ void RestLayout::layoutRestForJianpu(const Rest* item, Rest::LayoutData* ldata, const Staff* staff = item->staff(); const StaffType* st = staff->staffTypeForElement(item); + // Jianpu Y origin is at the center of the number. + // It always starts half a spatium above, aligned with the middle of the measure line. + const double hy = -item->spatium() * .5 * item->mag(); const double hx = ldata->bbox().x(); - const double hy = -item->spatium() * .5; ldata->setPos(hx, hy); staff_idx_t idx = item->staffIdx() + item->staffMove(); track_idx_t track = staff2track(idx); bool staffVisible = !staff->isLinesInvisible(tick); - int augmentationLines = item->durationType().augmentationLines(); - int diminutionLines = item->durationType().diminutionLines(); - if (augmentationLines == 0 && diminutionLines == 0) { - muse::DeleteAll(item->durationLines()); - const_cast(item)->durationLines().clear(); - } - muse::draw::Font font(item->staffType()->jianpuFont()); font.setPointSizeF(font.pointSizeF() * item->mag()); muse::draw::FontMetrics fm(font); const double width = fm.width(String(u"0")) / item->magS(); const double height = st->jianpuBoxH() * item->magS(); - const double distance = item->spatium() * .3; - const_cast(item)->resizeDurationLinesTo(std::max(augmentationLines, diminutionLines)); RectF noteBBox = RectF(0, -height * .5, width, height); ldata->setBbox(noteBBox); + int lines = item->durationType().augmentationLines(); + if (lines == 0) { + muse::DeleteAll(item->durationLines()); + const_cast(item)->durationLines().clear(); + } else { + const_cast(item)->resizeDurationLinesTo(lines); + for (int i = 0; i < lines; ++i) { + DurationLine* dl = item->durationLines()[i]; + dl->setParent(const_cast(item)); + dl->setTrack(track); + dl->setVisible(staffVisible); + dl->setLen(width); + dl->setPos(hx + width * 1.2 * (i + 1), hy); + } - for (int i = 0; i < diminutionLines; ++i) { - DurationLine* dl = item->durationLines()[i]; - dl->setParent(const_cast(item)); - dl->setTrack(track); - dl->setVisible(staffVisible); - dl->setLen(width); - dl->setPos(hx, hy + height * .5 + (i + 1) * distance); - dl->setHalving(true); // Halving duration - } - - for (int i = 0; i < augmentationLines; ++i) { - DurationLine* dl = item->durationLines()[i]; - dl->setParent(const_cast(item)); - dl->setTrack(track); - dl->setVisible(staffVisible); - dl->setLen(width); - dl->setPos(hx + width * 1.2 * (i + 1), hy); - dl->setHalving(false); // Lengthening duration - } - - for (DurationLine* dl : item->durationLines()) { - TLayout::layoutDurationLine(dl, ctx); + for (DurationLine* dl : item->durationLines()) { + TLayout::layoutDurationLine(dl, ctx); + } } } diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index 76fb3b242035..254129f4bd75 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -2735,13 +2735,15 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) auto bbox = item->ldata()->bbox(); double jianpuX = (noteheadWidth - jianpuWidth) * .5; double jianpuY = bbox.y(); - double distance = sp * .3; - double rad = sp * .1; + double lineDistance = item->style().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->magS(); + double lineThickness = item->style().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->magS(); + double dotDistance = item->style().styleAbsolute(Sid::jianpuOctaveDotDistance) * item->magS(); + double rad = item->style().styleAbsolute(Sid::jianpuOctaveDotDiameter) * item->magS(); if (item->lineIndex() >= 0) { jianpuY = bbox.height() + bbox.y(); jianpuY -= jianpuHeight; - jianpuY -= distance * (abs(item->jianpuOctaveDot()) + item->jianpuDurationLine()); + jianpuY -= dotDistance * abs(item->jianpuOctaveDots()) + lineDistance * item->jianpuDiminutionLines(); jianpuY -= rad * 2; } @@ -2751,27 +2753,27 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) painter->setBrush(Brush(pen.color())); double dotX = noteheadWidth * .5; - for (int i = 0; i < -item->jianpuOctaveDot(); i++) { + for (int i = 0; i < -item->jianpuOctaveDots(); i++) { painter->drawEllipse(PointF(dotX, jianpuY + rad), rad, rad); - jianpuY += distance; + jianpuY += dotDistance; } jianpuY += jianpuHeight; painter->drawText(jianpuX, jianpuY, item->jianpuDigit()); - jianpuY += distance; + jianpuY += item->jianpuDiminutionLines() ? lineDistance : dotDistance; - pen.setWidthF(lw); + pen.setWidthF(lineThickness); painter->setPen(pen); - for (int i = 0; i < item->jianpuDurationLine(); i++) { + for (int i = 0; i < item->jianpuDiminutionLines(); i++) { painter->drawLine(LineF(0.0, jianpuY, noteheadWidth, jianpuY)); - jianpuY += distance; + jianpuY += lineDistance; } pen.setWidthF(1.0); painter->setPen(pen); - for (int i = 0; i < item->jianpuOctaveDot(); i++) { + for (int i = 0; i < item->jianpuOctaveDots(); i++) { painter->drawEllipse(PointF(dotX, jianpuY + rad), rad, rad); - jianpuY += distance; + jianpuY += dotDistance; } painter->restore(); diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index 16bc2a2d9e3d..b485a69504b9 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -378,7 +378,7 @@ void TLayout::layoutItem(EngravingItem* item, LayoutContext& ctx) case ElementType::ORNAMENT: layoutOrnament(item_cast(item), static_cast(ldata), ctx.conf()); break; - case ElementType::OCTAVE_DOT: layoutOctaveDot(item_cast(item)); + case ElementType::OCTAVE_DOT: layoutOctaveDot(item_cast(item), ctx); break; case ElementType::OTTAVA: layoutOttava(item_cast(item), ctx); break; @@ -1898,7 +1898,7 @@ void TLayout::layoutDurationLine(DurationLine* item, const LayoutContext& ctx) double mag = item->mag(); DurationLine::LayoutData* ldata = item->mutldata(); ldata->setMag(mag); - ldata->lineWidth = ctx.conf().styleAbsolute(Sid::staffLineWidth) * mag; + ldata->lineWidth = ctx.conf().styleAbsolute(Sid::jianpuDurationLineThickness) * mag; if (item->staff()) { const_cast(item)->setColor(item->staff()->staffType(item->tick())->color()); } @@ -4271,12 +4271,14 @@ void TLayout::layoutNoteDot(const NoteDot* item, NoteDot::LayoutData* ldata) ldata->setBbox(item->symBbox(SymId::augmentationDot)); } -void TLayout::layoutOctaveDot(OctaveDot* item) +void TLayout::layoutOctaveDot(OctaveDot* item, LayoutContext& ctx) { LAYOUT_CALL_ITEM(item); OctaveDot::LayoutData* ldata = item->mutldata(); - ldata->setMag(item->note()->mag()); - ldata->radius = item->defaultSpatium() * .1; // TODO: style + + double mag = item->note()->mag(); + ldata->setMag(mag); + ldata->radius = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDiameter) * mag; double diameter = ldata->radius * 2; ldata->setBbox(0, -ldata->radius, item->len(), diameter); @@ -4771,23 +4773,24 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) } } } - double distance = _spatium * .3; - jianpuBbox.setHeight(st->jianpuBoxH() * mag + distance); + double dotDistance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * mag; + double lineDistance = ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * mag; + jianpuBbox.setHeight(st->jianpuBoxH() * mag + dotDistance); if (!up) { jianpuBbox.setY(noteheadBbox.y() - jianpuBbox.height()); // Jianpu is above the head note } else { jianpuBbox.setY(newBbox.y() + newBbox.height()); // Jianpu is under the head note } - // jianpu duration line + // jianpu diminution line int lines = item->duration().diminutionLines(); - item->setJianpuDurationLine(lines); + item->setJianpuDiminutionLines(lines); // jianpu octave dots - item->setJianpuOctaveDot(dots); + item->setJianpuOctaveDots(dots); // Always has one extra distance - double extraHeight = (1 + abs(dots) + lines) * distance; + double extraHeight = (1 + abs(dots)) * dotDistance + lines * lineDistance; jianpuBbox.setHeight(jianpuBbox.height() + extraHeight); if (!up) { diff --git a/src/engraving/rendering/score/tlayout.h b/src/engraving/rendering/score/tlayout.h index 8d510a3e2fc2..5bbe7b73f49d 100644 --- a/src/engraving/rendering/score/tlayout.h +++ b/src/engraving/rendering/score/tlayout.h @@ -297,7 +297,7 @@ class TLayout static void layoutNoteLine(NoteLine* item, LayoutContext& ctx); static void layoutNoteLineSegment(NoteLineSegment* item, LayoutContext& ctx); - static void layoutOctaveDot(OctaveDot* item); + static void layoutOctaveDot(OctaveDot* item, LayoutContext& ctx); static void layoutOrnament(const Ornament* item, Ornament::LayoutData* ldata, const LayoutConfiguration& conf); static void layoutOrnamentCueNote(Ornament* item, LayoutContext& ctx); diff --git a/src/engraving/rw/read460/tread.cpp b/src/engraving/rw/read460/tread.cpp index 43f67a4d64e3..cd04fde72549 100644 --- a/src/engraving/rw/read460/tread.cpp +++ b/src/engraving/rw/read460/tread.cpp @@ -3989,6 +3989,8 @@ void TRead::read(StaffType* t, XmlReader& e, ReadContext& ctx) t->setFretUseTextStyle(e.readBool()); } else if (tag == "fretTextStyle") { t->setFretTextStyle(TextStyleType(TConv::fromXml(e.readAsciiText(), TextStyleType::TAB_FRET_NUMBER))); + } else if (tag == "jianpuTextStyle") { + t->setJianpuTextStyle(TextStyleType(TConv::fromXml(e.readAsciiText(), TextStyleType::JIANPU_NUMBER))); } else if (tag == "symbolRepeat") { t->setSymbolRepeat((TablatureSymbolRepeat)e.readInt()); } else if (tag == "linesThrough") { diff --git a/src/engraving/rw/write/twrite.cpp b/src/engraving/rw/write/twrite.cpp index 5643264a7fb3..bbe2ac7816c1 100644 --- a/src/engraving/rw/write/twrite.cpp +++ b/src/engraving/rw/write/twrite.cpp @@ -3083,6 +3083,9 @@ void TWrite::write(const StaffType* item, XmlWriter& xml, WriteContext& ctx) } if (item->group() == StaffGroup::STANDARD) { xml.tag("noteheadScheme", TConv::toXml(item->noteHeadScheme()), TConv::toXml(NoteHeadScheme::HEAD_NORMAL)); + if (item->xmlName() == u"stdJianpu") { + xml.tag("jianpuTextStyle", TConv::toXml(item->jianpuTextStyle())); + } } if (item->group() == StaffGroup::STANDARD || item->group() == StaffGroup::PERCUSSION) { if (!item->genKeysig()) { diff --git a/src/engraving/style/style.cpp b/src/engraving/style/style.cpp index 2e0dcf975849..631d5cf4cceb 100644 --- a/src/engraving/style/style.cpp +++ b/src/engraving/style/style.cpp @@ -640,6 +640,18 @@ void MStyle::read(XmlReader& e, compat::ReadChordListHook* readChordListHook, in const StyleDef::StyleValue& s = *i; set(s.sid, compat::CompatUtils::convertPre470FrameRadius(e.readDouble())); } + } else if (tag == "JianpuNumberVerticalDistance") { + set(Sid::jianpuNumberVerticalDistance, Spatium(e.readDouble())); + } else if (tag == "JianpuOctaveDotDiameter") { + set(Sid::jianpuOctaveDotDiameter, Spatium(e.readDouble())); + } else if (tag == "JianpuOctaveDotDistance") { + set(Sid::jianpuOctaveDotDistance, Spatium(e.readDouble())); + } else if (tag == "JianpuDiminutionBeamThickness") { + set(Sid::jianpuDiminutionBeamThickness, Spatium(e.readDouble())); + } else if (tag == "JianpuDiminutionBeamDistance") { + set(Sid::jianpuDiminutionBeamDistance, Spatium(e.readDouble())); + } else if (tag == "JianpuDurationLineThickness") { + set(Sid::jianpuDurationLineThickness, Spatium(e.readDouble())); } else if (!readProperties(e)) { e.unknown(); } diff --git a/src/engraving/style/styledef.cpp b/src/engraving/style/styledef.cpp index 7c1b79c9d30c..693fb7cf9f3a 100644 --- a/src/engraving/style/styledef.cpp +++ b/src/engraving/style/styledef.cpp @@ -1087,6 +1087,21 @@ const std::array StyleDef::styleValue styleDef(articulationOffset, PointF()), styleDef(articulationPosition, AlignH::LEFT), + styleDef(jianpuNumberFontFace, "FreeSans"), + styleDef(jianpuNumberFontSize, 9.0), + styleDef(jianpuNumberLineSpacing, 1.0), + styleDef(jianpuNumberFontSpatiumDependent, true), + styleDef(jianpuNumberFontStyle, int(FontStyle::Normal)), + styleDef(jianpuNumberColor, PropertyValue::fromValue(Color::BLACK)), + styleDef(jianpuNumberAlign, Align(AlignH::LEFT, AlignV::BASELINE)), + styleDef(jianpuNumberOffset, PointF()), + styleDef(jianpuNumberFrameType, int(FrameType::NO_FRAME)), + styleDef(jianpuNumberFramePadding, 0.2), + styleDef(jianpuNumberFrameWidth, 0.1), + styleDef(jianpuNumberFrameRound, 0), + styleDef(jianpuNumberFrameFgColor, PropertyValue::fromValue(Color::BLACK)), + styleDef(jianpuNumberFrameBgColor, PropertyValue::fromValue(Color::transparent)), + styleDef(longInstrumentFontFace, "Edwin"), styleDef(longInstrumentFontSize, 10.0), styleDef(longInstrumentLineSpacing, 1.0), @@ -2211,6 +2226,13 @@ const std::array StyleDef::styleValue styleDef(palmMuteBeginFilledArrowWidth, 0.85_sp), styleDef(palmMuteEndFilledArrowHeight, 1.0_sp), styleDef(palmMuteEndFilledArrowWidth, 0.85_sp), + + styleDef(jianpuNumberVerticalDistance, 0.5_sp), + styleDef(jianpuOctaveDotDiameter, 0.1_sp), + styleDef(jianpuOctaveDotDistance, 0.3_sp), + styleDef(jianpuDiminutionBeamThickness, 0.1_sp), + styleDef(jianpuDiminutionBeamDistance, 0.3_sp), + styleDef(jianpuDurationLineThickness, 0.1_sp), } }; #undef styleDef diff --git a/src/engraving/style/styledef.h b/src/engraving/style/styledef.h index c91e3c26df27..b44c8f4c7e2b 100644 --- a/src/engraving/style/styledef.h +++ b/src/engraving/style/styledef.h @@ -1097,6 +1097,21 @@ enum class Sid : short { articulationOffset, articulationPosition, + jianpuNumberFontFace, + jianpuNumberFontSize, + jianpuNumberLineSpacing, + jianpuNumberFontSpatiumDependent, + jianpuNumberFontStyle, + jianpuNumberColor, + jianpuNumberAlign, + jianpuNumberOffset, + jianpuNumberFrameType, + jianpuNumberFramePadding, + jianpuNumberFrameWidth, + jianpuNumberFrameRound, + jianpuNumberFrameFgColor, + jianpuNumberFrameBgColor, + longInstrumentFontFace, longInstrumentFontSize, longInstrumentLineSpacing, @@ -2230,6 +2245,12 @@ enum class Sid : short { palmMuteEndFilledArrowHeight, palmMuteEndFilledArrowWidth, + jianpuNumberVerticalDistance, + jianpuOctaveDotDiameter, + jianpuOctaveDotDistance, + jianpuDiminutionBeamThickness, + jianpuDiminutionBeamDistance, + jianpuDurationLineThickness, STYLES }; diff --git a/src/engraving/style/textstyle.cpp b/src/engraving/style/textstyle.cpp index ffb2bf8fafd2..41b13842a902 100644 --- a/src/engraving/style/textstyle.cpp +++ b/src/engraving/style/textstyle.cpp @@ -446,6 +446,25 @@ const TextStyle harpPedalTextDiagramTextStyle { { Sid::harpPedalTextDiagramOffset, Sid::harpPedalTextDiagramOffset } }; +const TextStyle jianpuNumberTextStyle { + { { + { TextStylePropertyType::FontFace, Sid::jianpuNumberFontFace, Pid::FONT_FACE }, + { TextStylePropertyType::FontSize, Sid::jianpuNumberFontSize, Pid::FONT_SIZE }, + { TextStylePropertyType::LineSpacing, Sid::jianpuNumberLineSpacing, Pid::TEXT_LINE_SPACING }, + { TextStylePropertyType::SizeSpatiumDependent, Sid::jianpuNumberFontSpatiumDependent, Pid::SIZE_SPATIUM_DEPENDENT }, + { TextStylePropertyType::FontStyle, Sid::jianpuNumberFontStyle, Pid::FONT_STYLE }, + { TextStylePropertyType::Color, Sid::jianpuNumberColor, Pid::COLOR }, + { TextStylePropertyType::TextAlign, Sid::jianpuNumberAlign, Pid::ALIGN }, + { TextStylePropertyType::FrameType, Sid::jianpuNumberFrameType, Pid::FRAME_TYPE }, + { TextStylePropertyType::FramePadding, Sid::jianpuNumberFramePadding, Pid::FRAME_PADDING }, + { TextStylePropertyType::FrameWidth, Sid::jianpuNumberFrameWidth, Pid::FRAME_WIDTH }, + { TextStylePropertyType::FrameRound, Sid::jianpuNumberFrameRound, Pid::FRAME_ROUND }, + { TextStylePropertyType::FrameBorderColor, Sid::jianpuNumberFrameFgColor, Pid::FRAME_FG_COLOR }, + { TextStylePropertyType::FrameFillColor, Sid::jianpuNumberFrameBgColor, Pid::FRAME_BG_COLOR }, + } }, + { Sid::jianpuNumberOffset, Sid::jianpuNumberOffset } +}; + const TextStyle longInstrumentTextStyle { { { { TextStylePropertyType::FontFace, Sid::longInstrumentFontFace, Pid::FONT_FACE }, @@ -1714,6 +1733,7 @@ const TextStyle* textStyle(TextStyleType idx) case TextStyleType::FRET_DIAGRAM_FRET_NUMBER: return &fretDiagramFretNumberStyle; case TextStyleType::HARP_PEDAL_DIAGRAM: return &harpPedalDiagramTextStyle; case TextStyleType::HARP_PEDAL_TEXT_DIAGRAM: return &harpPedalTextDiagramTextStyle; + case TextStyleType::JIANPU_NUMBER: return &jianpuNumberTextStyle; case TextStyleType::TEXTLINE: return &textLineTextStyle; case TextStyleType::SYSTEM_TEXTLINE: return &systemTextLineTextStyle; diff --git a/src/engraving/tests/beam_data/jianpuBeam.mscx b/src/engraving/tests/beam_data/jianpuBeam.mscx new file mode 100644 index 000000000000..c6fba15f6cc0 --- /dev/null +++ b/src/engraving/tests/beam_data/jianpuBeam.mscx @@ -0,0 +1,590 @@ + + + + A_A + 480 + + 1 + 1 + 1 + 0 + 1 + + + Composer / arranger + + + + + + + Subtitle + + + Untitled Score + + Orchestral + + Keyboards + +
+ flutes + oboes + clarinets + saxophones + bassoons + +
+
+ horns + trumpets + cornets + flugelhorns + trombones + tubas +
+
+ timpani +
+
+ keyboard-percussion + drums + unpitched-metal-percussion + unpitched-wooden-percussion + other-percussion +
+ keyboards + harps + organs + synths + +
+ voices + voice-groups +
+
+ orchestral-strings +
+ +
+ + d_d + + A_B + + stdJianpu + 0 + jianpu_number + + + Brace + 2 + 0 + + 1 + + + + Piano + Pno. + + Piano + 21 + 108 + 21 + 108 + keyboard.piano + + 100 + 95 + + + 100 + 33 + + + 100 + 50 + + + 100 + 67 + + + 100 + 100 + + + 120 + 67 + + + 150 + 100 + + + 150 + 50 + + + 120 + 50 + + + 120 + 100 + + + + + + + + + A_C + + + A_D + 4 + 4 + + + A_E + 4 + 4 + + + A_F + eighth + + A_G + 60 + 14 + + + + A_H + eighth + + A_I + 62 + 16 + + + + A_J + eighth + + A_K + 64 + 18 + + + + A_L + eighth + + A_M + 65 + 13 + + + + A_N + 4 + 4 + + + A_O + eighth + + A_P + 67 + 15 + + + + A_Q + eighth + + A_R + 69 + 17 + + + + A_S + eighth + + A_T + 71 + 19 + + + + A_U + eighth + + A_V + 72 + 14 + + + + + + A_W + + + A_X + 4 + 4 + + + A_Y + eighth + + A_Z + 74 + 16 + + + + B_A + eighth + + B_B + 76 + 18 + + + + B_C + eighth + + B_D + 77 + 13 + + + + B_E + eighth + + B_F + 79 + 15 + + + + B_G + 4 + 4 + + + B_H + eighth + + B_I + 81 + 17 + + + + B_J + eighth + + B_K + 83 + 19 + + + + B_L + eighth + + B_M + 84 + 14 + + + + B_N + eighth + + B_O + 86 + 16 + + + + + + B_P + + + B_Q + 4 + 4 + + + B_R + eighth + + B_S + 60 + 14 + + + + B_T + eighth + + B_U + 59 + 19 + + + + B_V + eighth + + B_W + 57 + 17 + + + + B_X + eighth + + B_Y + 55 + 15 + + + + B_Z + 4 + 4 + + + C_A + eighth + + C_B + 53 + 13 + + + + C_C + eighth + + C_D + 52 + 18 + + + + C_E + eighth + + C_F + 50 + 16 + + + + C_G + eighth + + C_H + 48 + 14 + + + + + + C_I + + + C_J + 4 + 4 + + + C_K + 64th + + C_L + 83 + 19 + + + + C_M + 64th + + C_N + 47 + 19 + + + + C_O + 32nd + + C_P + 59 + 19 + + + + C_Q + 16th + + C_R + 47 + 19 + + + + C_S + eighth + + C_T + 62 + 16 + + + + C_U + 4 + 4 + + + C_V + 64th + + C_W + 84 + 14 + + + + C_X + 64th + + C_Y + 72 + 14 + + + + C_Z + 32nd + + D_A + 59 + 19 + + + + D_B + 16th + + D_C + 72 + 14 + + + + D_D + eighth + + D_E + 84 + 14 + + + + a_a + 4 + 4 + + + D_F + eighth + + D_G + 71 + 19 + + + + D_H + eighth + + + b_b + 4 + 4 + + + D_I + begin + eighth + + D_J + 47 + 19 + + + + D_K + eighth + + + + + D_L + + + c_c + 4 + 4 + + + D_M + eighth + + D_N + 60 + 14 + + + + D_O + eighth + + + D_P + eighth + + + D_Q + eighth + + + D_R + half + + + + +
+
diff --git a/src/engraving/tests/beam_tests.cpp b/src/engraving/tests/beam_tests.cpp index 24ee937a28ba..e940d8e2fb15 100644 --- a/src/engraving/tests/beam_tests.cpp +++ b/src/engraving/tests/beam_tests.cpp @@ -115,6 +115,11 @@ TEST_F(Engraving_BeamTests, wideBeams) beam("wideBeams.mscx"); } +TEST_F(Engraving_BeamTests, jianpuBeam) +{ + beam("jianpuBeam.mscx"); +} + TEST_F(Engraving_BeamTests, flatBeams) { MasterScore* score = ScoreRW::readScore(BEAM_DATA_DIR + u"flatBeams.mscx"); diff --git a/src/engraving/types/types.h b/src/engraving/types/types.h index 32cda1f600ff..a1ec3e987a2e 100644 --- a/src/engraving/types/types.h +++ b/src/engraving/types/types.h @@ -887,6 +887,7 @@ enum class TextStyleType : unsigned char { FRET_DIAGRAM_FRET_NUMBER, HARP_PEDAL_DIAGRAM, HARP_PEDAL_TEXT_DIAGRAM, + JIANPU_NUMBER, // Line-oriented styles TEXTLINE, diff --git a/src/engraving/types/typesconv.cpp b/src/engraving/types/typesconv.cpp index f57ba48835a5..359c249f68ed 100644 --- a/src/engraving/types/typesconv.cpp +++ b/src/engraving/types/typesconv.cpp @@ -1766,6 +1766,7 @@ static const std::vector > TEXTSTYLE_TYPES = { { TextStyleType::HARP_PEDAL_DIAGRAM, "harp_pedal_diagram", muse::TranslatableString("engraving", "Harp pedal diagram") }, { TextStyleType::HARP_PEDAL_TEXT_DIAGRAM, "harp_pedal_text_diagram", muse::TranslatableString("engraving", "Harp pedal text diagram") }, { TextStyleType::ARTICULATION, "articulation", muse::TranslatableString("engraving", "Articulation") }, + { TextStyleType::JIANPU_NUMBER, "jianpu_number", muse::TranslatableString("engraving", "Jianpu number") }, { TextStyleType::TEXTLINE, "textline", muse::TranslatableString("engraving", "Text line") }, { TextStyleType::SYSTEM_TEXTLINE, "system_textline", muse::TranslatableString("engraving", "System text line") }, diff --git a/src/importexport/musicxml/internal/import/importmusicxmlpass1.cpp b/src/importexport/musicxml/internal/import/importmusicxmlpass1.cpp index 6a2d447afd72..6f2c1a59114b 100644 --- a/src/importexport/musicxml/internal/import/importmusicxmlpass1.cpp +++ b/src/importexport/musicxml/internal/import/importmusicxmlpass1.cpp @@ -1843,7 +1843,8 @@ static void updateStyles(Score* score, if (tid == TextStyleType::LYRICS_ODD || tid == TextStyleType::LYRICS_EVEN || tid == TextStyleType::FRET_DIAGRAM_FINGERING || tid == TextStyleType::FRET_DIAGRAM_FRET_NUMBER - || tid == TextStyleType::HARMONY_ROMAN || tid == TextStyleType::TAB_FRET_NUMBER) { + || tid == TextStyleType::HARMONY_ROMAN || tid == TextStyleType::TAB_FRET_NUMBER + || tid == TextStyleType::JIANPU_NUMBER) { continue; } diff --git a/src/notationscene/widgets/editstyle.cpp b/src/notationscene/widgets/editstyle.cpp index 625c8b52bd69..2b5d3788f4ac 100644 --- a/src/notationscene/widgets/editstyle.cpp +++ b/src/notationscene/widgets/editstyle.cpp @@ -100,6 +100,7 @@ static const QStringList ALL_PAGE_CODES { "chord-symbols", "fretboard-diagrams", "tablature-styles", + "jianpu", "text-styles" }; @@ -153,6 +154,7 @@ static const QStringList ALL_TEXT_STYLE_SUBPAGE_CODES { "fretboard-diagram-fret-number", "harp-pedal-diagram", "harp-pedal-text-diagram", + "jianpu-number", "text-line", "system-text-line", "note-line", @@ -703,6 +705,13 @@ void EditStyle::classBegin() { StyleId::tabShowTiedFret, false, tabShowTiedFrets, 0 }, { StyleId::tabParenthesizeTiedFret, false, tabParenthFrets, 0 }, { StyleId::parenthesizeTiedFretIfArticulation, false, tabParenthArticulation, 0 }, + + { StyleId::jianpuNumberVerticalDistance, false, jianpuNumberVerticalDistance, jianpuNumberVerticalDistanceReset }, + { StyleId::jianpuOctaveDotDiameter, false, jianpuOctaveDotDiameter, jianpuOctaveDotDiameterReset }, + { StyleId::jianpuOctaveDotDistance, false, jianpuOctaveDotDistance, jianpuOctaveDotDistanceReset }, + { StyleId::jianpuDiminutionBeamThickness, false, jianpuDiminutionBeamThickness, jianpuDiminutionBeamThicknessReset }, + { StyleId::jianpuDiminutionBeamDistance, false, jianpuDiminutionBeamDistance, jianpuDiminutionBeamDistanceReset }, + { StyleId::jianpuDurationLineThickness, false, jianpuDurationLineThickness, jianpuDurationLineThicknessReset }, }; // ==================================================== diff --git a/src/notationscene/widgets/editstyle.ui b/src/notationscene/widgets/editstyle.ui index e1a83721f556..ee62dd2ec429 100644 --- a/src/notationscene/widgets/editstyle.ui +++ b/src/notationscene/widgets/editstyle.ui @@ -252,6 +252,11 @@ Tablature + + + Jianpu + + Text styles @@ -12780,6 +12785,246 @@ followed by dashes + + + + + + Jianpu + + + false + + + + + + Vertical distance: + + + jianpuNumberVerticalDistance + + + + + + + false + + + sp + + + 9.000000000000000 + + + 0.010000000000000 + + + + + + + + + + + + + + Octave dot diameter: + + + jianpuOctaveDotDiameter + + + + + + + false + + + sp + + + 9.000000000000000 + + + 0.010000000000000 + + + + + + + + + + + + + + Octave dot distance: + + + jianpuOctaveDotDistance + + + + + + + false + + + sp + + + 9.000000000000000 + + + 0.010000000000000 + + + + + + + + + + + + + + Diminution beam thickness: + + + jianpuDiminutionBeamThickness + + + + + + + false + + + sp + + + 9.000000000000000 + + + 0.010000000000000 + + + + + + + + + + + + + + Diminution beam distance: + + + jianpuDiminutionBeamDistance + + + + + + + false + + + sp + + + 9.000000000000000 + + + 0.010000000000000 + + + + + + + + + + + + + + Duration line thickness: + + + jianpuDurationLineThickness + + + + + + + false + + + sp + + + 9.000000000000000 + + + 0.010000000000000 + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + + + + + Qt::Orientation::Vertical + + + + 0 + 0 + + + + + + @@ -14078,6 +14323,12 @@ followed by dashes resetTextStyleFrameBorderRadius buttonTogglePagelist resetStylesButton + jianpuNumberVerticalDistance + jianpuOctaveDotDiameter + jianpuOctaveDotDistance + jianpuDiminutionBeamThickness + jianpuDiminutionBeamDistance + jianpuDurationLineThickness diff --git a/src/notationscene/widgets/editstyleutils.cpp b/src/notationscene/widgets/editstyleutils.cpp index a6570572e8ad..e6e1caf4e3a4 100644 --- a/src/notationscene/widgets/editstyleutils.cpp +++ b/src/notationscene/widgets/editstyleutils.cpp @@ -395,6 +395,9 @@ QString EditStyleUtils::subPageCodeForElement(const EngravingItem* element) case TextStyleType::PALM_MUTE: return "palm-mute"; + case TextStyleType::JIANPU_NUMBER: + return "jianpu-number"; + case TextStyleType::USER1: return "user1"; From 1a8e8b0a2b2d11e6ab4731fed444f03103e6abd1 Mon Sep 17 00:00:00 2001 From: Song <8042214+songzhenke@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:45:12 +1000 Subject: [PATCH 09/24] Fix coderabbital code review comments (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * code review: Propagate track/score to DurationLine siblings as well. * code review: Fix parent-type wiring for new Jianpu factory cases * code review: Initialize and propagate OctaveDot ownership metadata consistently * code review: Preserve grace-note scaling in Jianpu beam metrics. * code review: Potential null pointer dereference on item->measure * code review: Reading bbox before it is initialized. * code review: Guard LayoutData before dereference in new draw paths * code review: Add the missing ElementType::DURATION_LINE dispatch and Clear the clef layout state before returning for Jianpu * code review: Use the configured octave-dot diameter only once * code review: Remove explicit Jianpu property handlers—readProperties() already handles them * code review: Keep the Jianpu frame metrics in spatium units. * code review: Fix duplicate widget name in Jianpu labels. * code review: Add accessibility metadata for Jianpu reset buttons. * code review: Clear generated Jianpu helpers when the staff is no longer Jianpu * code review: Forward-declare OctaveDot in this header. --- src/engraving/dom/chordrest.cpp | 7 +- src/engraving/dom/factory.cpp | 4 +- src/engraving/dom/note.cpp | 10 ++- src/engraving/rendering/score/beamlayout.cpp | 10 +-- src/engraving/rendering/score/chordlayout.cpp | 6 ++ src/engraving/rendering/score/restlayout.cpp | 8 ++- src/engraving/rendering/score/tdraw.cpp | 13 +++- src/engraving/rendering/score/tlayout.cpp | 7 +- src/engraving/rendering/score/tlayout.h | 1 + src/engraving/style/style.cpp | 12 ---- src/engraving/style/styledef.cpp | 8 +-- src/engraving/style/styledef.h | 2 +- src/notationscene/widgets/editstyle.cpp | 2 +- src/notationscene/widgets/editstyle.ui | 70 ++++++++++++++----- 14 files changed, 112 insertions(+), 48 deletions(-) diff --git a/src/engraving/dom/chordrest.cpp b/src/engraving/dom/chordrest.cpp index 27507985adb6..a32aa0468257 100644 --- a/src/engraving/dom/chordrest.cpp +++ b/src/engraving/dom/chordrest.cpp @@ -859,6 +859,9 @@ void ChordRest::processSiblings(std::function func) for (EngravingItem* e : m_el) { func(e); } + for (DurationLine* dl : m_durationLines) { + func(dl); + } } //--------------------------------------------------------- @@ -1336,7 +1339,9 @@ bool ChordRest::hasPrecedingJumpItem() const void ChordRest::resizeDurationLinesTo(size_t newSize) { while (m_durationLines.size() < newSize) { - m_durationLines.push_back(new DurationLine(score()->dummy())); + DurationLine* dl = new DurationLine(score()->dummy()); + dl->setTrack(track()); + m_durationLines.push_back(dl); } while (m_durationLines.size() > newSize) { diff --git a/src/engraving/dom/factory.cpp b/src/engraving/dom/factory.cpp index 9a63ca1bb56c..c42da8ec0322 100644 --- a/src/engraving/dom/factory.cpp +++ b/src/engraving/dom/factory.cpp @@ -251,8 +251,8 @@ EngravingItem* Factory::doCreateItem(ElementType type, EngravingItem* parent) case ElementType::PARTIAL_TIE: return new PartialTie(parent->isNote() ? toNote(parent) : dummy->note()); case ElementType::PARTIAL_LYRICSLINE: return new PartialLyricsLine(parent); case ElementType::PARENTHESIS: return new Parenthesis(parent); - case ElementType::DURATION_LINE: return new DurationLine(parent->isChord() ? toChord(parent) : dummy->chord()); - case ElementType::OCTAVE_DOT: return new OctaveDot(parent->isChord() ? toChord(parent) : dummy->chord()); + case ElementType::DURATION_LINE: return new DurationLine(parent->isChordRest() ? toChordRest(parent) : dummy->chord()); + case ElementType::OCTAVE_DOT: return new OctaveDot(parent->isNote() ? toNote(parent) : dummy->note()); case ElementType::TEXTLINE_BASE: case ElementType::TEXTLINE_SEGMENT: diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index 4d9fd6a913f0..e006999c45d3 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -2299,6 +2299,9 @@ void Note::setTrack(track_idx_t val) for (NoteDot* dot : m_dots) { dot->setTrack(val); } + for (OctaveDot* dot : m_octaveDots) { + dot->setTrack(val); + } } //--------------------------------------------------------- @@ -3276,6 +3279,9 @@ void Note::setScore(Score* s) for (NoteDot* dot : m_dots) { dot->setScore(s); } + for (OctaveDot* dot : m_octaveDots) { + dot->setScore(s); + } for (EngravingItem* el : m_el) { el->setScore(s); } @@ -4144,7 +4150,9 @@ bool Note::transpose(Interval interval, bool useDoubleSharpsFlats) void Note::resizeOctaveDotsTo(size_t newSize) { while (m_octaveDots.size() < newSize) { - m_octaveDots.push_back(new OctaveDot(score()->dummy())); + OctaveDot* dot = new OctaveDot(score()->dummy()); + dot->setTrack(track()); + m_octaveDots.push_back(dot); } while (m_octaveDots.size() > newSize) { delete m_octaveDots.back(); diff --git a/src/engraving/rendering/score/beamlayout.cpp b/src/engraving/rendering/score/beamlayout.cpp index 83b0e902bc7c..e3155bd25a9e 100644 --- a/src/engraving/rendering/score/beamlayout.cpp +++ b/src/engraving/rendering/score/beamlayout.cpp @@ -338,9 +338,11 @@ void BeamLayout::layout2(Beam* item, const LayoutContext& ctx, const std::vector item->setStartAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), startCr, ChordBeamAnchorType::Start)); item->setEndAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), endCr, ChordBeamAnchorType::End)); + double graceScale = 1.0; if (item->isGrace()) { - item->setBeamDist(item->beamDist() * ctx.conf().styleD(Sid::graceNoteMag)); - item->setBeamWidth(item->beamWidth() * ctx.conf().styleD(Sid::graceNoteMag)); + graceScale = ctx.conf().styleD(Sid::graceNoteMag); + item->setBeamDist(item->beamDist() * graceScale); + item->setBeamWidth(item->beamWidth() * graceScale); } int fragmentIndex = item->directionIdx(); @@ -373,8 +375,8 @@ void BeamLayout::layout2(Beam* item, const LayoutContext& ctx, const std::vector item->setStartAnchor(item->ldata()->startAnchor); item->setEndAnchor(item->ldata()->endAnchor); item->setSlope(0.0); - item->setBeamWidth(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->mag()); - item->setBeamDist(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->mag()); + item->setBeamWidth(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->mag() * graceScale); + item->setBeamDist(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->mag() * graceScale); } else if (!item->isBesideTabStaff()) { BeamTremoloLayout::setupLData(item, item->mutldata(), ctx); BeamTremoloLayout::calculateAnchors(item, item->mutldata(), ctx, chordRests, item->notePositions()); diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 03c7e5bb9e5c..1a8c255d244f 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1704,6 +1704,8 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) bool isJianpu = staff->isJianpuStaff(tick); if (!isJianpu) { + muse::DeleteAll(item->durationLines()); + item->durationLines().clear(); return; } @@ -1751,6 +1753,9 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) bool staffVisible = !staff->isLinesInvisible(tick); bool isJianpu = staff->isJianpuStaff(tick); if (!isJianpu) { + for (Note* note : item->notes()) { + note->resizeOctaveDotsTo(0); + } return; } @@ -2452,6 +2457,7 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ for (Chord* chord : posInfo.chords) { for (Chord* grace : chord->graceNotes()) { AccidentalsLayout::layoutAccidentals({ grace }, ctx); + ChordLayout::layoutOctaveDots(grace, ctx); } ChordLayout::layoutDurationLines(chord, ctx); ChordLayout::layoutOctaveDots(chord, ctx); diff --git a/src/engraving/rendering/score/restlayout.cpp b/src/engraving/rendering/score/restlayout.cpp index af6cdf8dc4ea..12baa3e75b50 100644 --- a/src/engraving/rendering/score/restlayout.cpp +++ b/src/engraving/rendering/score/restlayout.cpp @@ -179,14 +179,18 @@ void RestLayout::fillShape(const Rest* item, Rest::LayoutData* ldata, const Layo void RestLayout::layoutRestForJianpu(const Rest* item, Rest::LayoutData* ldata, const LayoutContext& ctx) { - Fraction tick = item->measure()->ticks(); + const Measure* measure = item->measure(); + if (!measure) { + return; + } + Fraction tick = measure->ticks(); const Staff* staff = item->staff(); const StaffType* st = staff->staffTypeForElement(item); // Jianpu Y origin is at the center of the number. // It always starts half a spatium above, aligned with the middle of the measure line. const double hy = -item->spatium() * .5 * item->mag(); - const double hx = ldata->bbox().x(); + const double hx = 0.0; ldata->setPos(hx, hy); staff_idx_t idx = item->staffIdx() + item->staffMove(); diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index 254129f4bd75..ed6ac9f192d7 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -1254,6 +1254,10 @@ void TDraw::draw(const DurationLine* item, Painter* painter, const PaintOptions& TRACE_DRAW_ITEM; const DurationLine::LayoutData* ldata = item->ldata(); + IF_ASSERT_FAILED(ldata) { + return; + } + painter->setPen(Pen(item->curColor(opt), ldata->lineWidth, PenStyle::SolidLine, PenCapStyle::FlatCap)); painter->drawLine(LineF(0.0, 0.0, item->len(), 0.0)); } @@ -2478,7 +2482,12 @@ void TDraw::draw(const OctaveDot* item, Painter* painter, const PaintOptions& op { TRACE_DRAW_ITEM; - double rad = item->ldata()->radius; + const OctaveDot::LayoutData* ldata = item->ldata(); + IF_ASSERT_FAILED(ldata) { + return; + } + + double rad = ldata->radius; double x = item->len() * 0.5; // draw dot in the middle painter->setPen(Pen(item->curColor(opt))); @@ -2738,7 +2747,7 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) double lineDistance = item->style().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->magS(); double lineThickness = item->style().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->magS(); double dotDistance = item->style().styleAbsolute(Sid::jianpuOctaveDotDistance) * item->magS(); - double rad = item->style().styleAbsolute(Sid::jianpuOctaveDotDiameter) * item->magS(); + double rad = item->style().styleAbsolute(Sid::jianpuOctaveDotRadius) * item->magS(); if (item->lineIndex() >= 0) { jianpuY = bbox.height() + bbox.y(); diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index b485a69504b9..346eed238823 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -255,6 +255,9 @@ void TLayout::layoutItem(EngravingItem* item, LayoutContext& ctx) case ElementType::DEAD_SLAPPED: layoutDeadSlapped(item_cast(item), static_cast(ldata)); break; + case ElementType::DURATION_LINE: + layoutDurationLine(item_cast(item), ctx); + break; case ElementType::DYNAMIC: layoutDynamic(item_cast(item), static_cast(ldata), ctx.conf()); break; @@ -1733,6 +1736,8 @@ void TLayout::layoutClef(const Clef* item, Clef::LayoutData* ldata, const Layout const Fraction tickPrev = tick - Fraction::eps(); if (item->staff()->isJianpuStaff(tick)) { + ldata->setBbox(RectF()); + ldata->symId = SymId::noSym; return; // Jianpu do not need to show clef } @@ -4278,7 +4283,7 @@ void TLayout::layoutOctaveDot(OctaveDot* item, LayoutContext& ctx) double mag = item->note()->mag(); ldata->setMag(mag); - ldata->radius = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDiameter) * mag; + ldata->radius = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotRadius) * mag; double diameter = ldata->radius * 2; ldata->setBbox(0, -ldata->radius, item->len(), diameter); diff --git a/src/engraving/rendering/score/tlayout.h b/src/engraving/rendering/score/tlayout.h index 5bbe7b73f49d..c30294acdd7c 100644 --- a/src/engraving/rendering/score/tlayout.h +++ b/src/engraving/rendering/score/tlayout.h @@ -140,6 +140,7 @@ class Lyrics; class LyricsLine; class LyricsLineSegment; +class OctaveDot; class Ottava; class OttavaSegment; diff --git a/src/engraving/style/style.cpp b/src/engraving/style/style.cpp index 631d5cf4cceb..2e0dcf975849 100644 --- a/src/engraving/style/style.cpp +++ b/src/engraving/style/style.cpp @@ -640,18 +640,6 @@ void MStyle::read(XmlReader& e, compat::ReadChordListHook* readChordListHook, in const StyleDef::StyleValue& s = *i; set(s.sid, compat::CompatUtils::convertPre470FrameRadius(e.readDouble())); } - } else if (tag == "JianpuNumberVerticalDistance") { - set(Sid::jianpuNumberVerticalDistance, Spatium(e.readDouble())); - } else if (tag == "JianpuOctaveDotDiameter") { - set(Sid::jianpuOctaveDotDiameter, Spatium(e.readDouble())); - } else if (tag == "JianpuOctaveDotDistance") { - set(Sid::jianpuOctaveDotDistance, Spatium(e.readDouble())); - } else if (tag == "JianpuDiminutionBeamThickness") { - set(Sid::jianpuDiminutionBeamThickness, Spatium(e.readDouble())); - } else if (tag == "JianpuDiminutionBeamDistance") { - set(Sid::jianpuDiminutionBeamDistance, Spatium(e.readDouble())); - } else if (tag == "JianpuDurationLineThickness") { - set(Sid::jianpuDurationLineThickness, Spatium(e.readDouble())); } else if (!readProperties(e)) { e.unknown(); } diff --git a/src/engraving/style/styledef.cpp b/src/engraving/style/styledef.cpp index 693fb7cf9f3a..b48eb3d3df11 100644 --- a/src/engraving/style/styledef.cpp +++ b/src/engraving/style/styledef.cpp @@ -1096,9 +1096,9 @@ const std::array StyleDef::styleValue styleDef(jianpuNumberAlign, Align(AlignH::LEFT, AlignV::BASELINE)), styleDef(jianpuNumberOffset, PointF()), styleDef(jianpuNumberFrameType, int(FrameType::NO_FRAME)), - styleDef(jianpuNumberFramePadding, 0.2), - styleDef(jianpuNumberFrameWidth, 0.1), - styleDef(jianpuNumberFrameRound, 0), + styleDef(jianpuNumberFramePadding, 0.2_sp), + styleDef(jianpuNumberFrameWidth, 0.1_sp), + styleDef(jianpuNumberFrameRound, 0.0_sp), styleDef(jianpuNumberFrameFgColor, PropertyValue::fromValue(Color::BLACK)), styleDef(jianpuNumberFrameBgColor, PropertyValue::fromValue(Color::transparent)), @@ -2228,7 +2228,7 @@ const std::array StyleDef::styleValue styleDef(palmMuteEndFilledArrowWidth, 0.85_sp), styleDef(jianpuNumberVerticalDistance, 0.5_sp), - styleDef(jianpuOctaveDotDiameter, 0.1_sp), + styleDef(jianpuOctaveDotRadius, 0.1_sp), styleDef(jianpuOctaveDotDistance, 0.3_sp), styleDef(jianpuDiminutionBeamThickness, 0.1_sp), styleDef(jianpuDiminutionBeamDistance, 0.3_sp), diff --git a/src/engraving/style/styledef.h b/src/engraving/style/styledef.h index b44c8f4c7e2b..0392b6885e6e 100644 --- a/src/engraving/style/styledef.h +++ b/src/engraving/style/styledef.h @@ -2246,7 +2246,7 @@ enum class Sid : short { palmMuteEndFilledArrowWidth, jianpuNumberVerticalDistance, - jianpuOctaveDotDiameter, + jianpuOctaveDotRadius, jianpuOctaveDotDistance, jianpuDiminutionBeamThickness, jianpuDiminutionBeamDistance, diff --git a/src/notationscene/widgets/editstyle.cpp b/src/notationscene/widgets/editstyle.cpp index 2b5d3788f4ac..b6a2026e6035 100644 --- a/src/notationscene/widgets/editstyle.cpp +++ b/src/notationscene/widgets/editstyle.cpp @@ -707,7 +707,7 @@ void EditStyle::classBegin() { StyleId::parenthesizeTiedFretIfArticulation, false, tabParenthArticulation, 0 }, { StyleId::jianpuNumberVerticalDistance, false, jianpuNumberVerticalDistance, jianpuNumberVerticalDistanceReset }, - { StyleId::jianpuOctaveDotDiameter, false, jianpuOctaveDotDiameter, jianpuOctaveDotDiameterReset }, + { StyleId::jianpuOctaveDotRadius, false, jianpuOctaveDotRadius, jianpuOctaveDotRadiusReset }, { StyleId::jianpuOctaveDotDistance, false, jianpuOctaveDotDistance, jianpuOctaveDotDistanceReset }, { StyleId::jianpuDiminutionBeamThickness, false, jianpuDiminutionBeamThickness, jianpuDiminutionBeamThicknessReset }, { StyleId::jianpuDiminutionBeamDistance, false, jianpuDiminutionBeamDistance, jianpuDiminutionBeamDistanceReset }, diff --git a/src/notationscene/widgets/editstyle.ui b/src/notationscene/widgets/editstyle.ui index ee62dd2ec429..768fbca914fc 100644 --- a/src/notationscene/widgets/editstyle.ui +++ b/src/notationscene/widgets/editstyle.ui @@ -12797,7 +12797,7 @@ followed by dashes - + Vertical distance: @@ -12824,23 +12824,29 @@ followed by dashes + + Reset to default + + + Reset 'Vertical distance' value + - + - + - Octave dot diameter: + Octave dot radius: - jianpuOctaveDotDiameter + jianpuOctaveDotRadius - + false @@ -12856,14 +12862,20 @@ followed by dashes - + + + Reset to default + + + Reset 'Octave dot radius' value + - + - + Octave dot distance: @@ -12890,13 +12902,19 @@ followed by dashes + + Reset to default + + + Reset 'Octave dot distance' value + - + - + Diminution beam thickness: @@ -12923,13 +12941,19 @@ followed by dashes + + Reset to default + + + Reset 'Diminution beam thickness' value + - + - + Diminution beam distance: @@ -12956,13 +12980,19 @@ followed by dashes + + Reset to default + + + Reset 'Diminution beam distance' value + - + - + Duration line thickness: @@ -12989,8 +13019,14 @@ followed by dashes + + Reset to default + + + Reset 'Duration line thickness' value + - + @@ -14324,7 +14360,7 @@ followed by dashes buttonTogglePagelist resetStylesButton jianpuNumberVerticalDistance - jianpuOctaveDotDiameter + jianpuOctaveDotRadius jianpuOctaveDotDistance jianpuDiminutionBeamThickness jianpuDiminutionBeamDistance From 670b7a8cf36d7de681f9697b974f6a8a2e10f8da Mon Sep 17 00:00:00 2001 From: Song <8042214+songzhenke@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:22:38 +1000 Subject: [PATCH 10/24] Fix coderabbital code review comments 2 (#6) * code review: Parent generated octave dots to the owning note. * code review: Clear the clef shape on the hidden/Jianpu early-return paths. * code review: Apply the wider Jianpu ledger-line width above the staff too. --- src/engraving/dom/note.cpp | 2 +- src/engraving/rendering/score/tlayout.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index e006999c45d3..68484e4fa979 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -4150,7 +4150,7 @@ bool Note::transpose(Interval interval, bool useDoubleSharpsFlats) void Note::resizeOctaveDotsTo(size_t newSize) { while (m_octaveDots.size() < newSize) { - OctaveDot* dot = new OctaveDot(score()->dummy()); + OctaveDot* dot = new OctaveDot(this); dot->setTrack(track()); m_octaveDots.push_back(dot); } diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index 346eed238823..2d356e94a41e 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -1736,6 +1736,7 @@ void TLayout::layoutClef(const Clef* item, Clef::LayoutData* ldata, const Layout const Fraction tickPrev = tick - Fraction::eps(); if (item->staff()->isJianpuStaff(tick)) { + ldata->setShape(Shape()); ldata->setBbox(RectF()); ldata->symId = SymId::noSym; return; // Jianpu do not need to show clef @@ -1764,6 +1765,7 @@ void TLayout::layoutClef(const Clef* item, Clef::LayoutData* ldata, const Layout // if clef not to show or not compatible with staff group if (!show) { + ldata->setShape(Shape()); ldata->setBbox(RectF()); ldata->symId = SymId::noSym; LOGD("invisible clef at tick %d(%d) staff %zu", @@ -4708,7 +4710,7 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) double lw = ctx.conf().styleAbsolute(Sid::ledgerLineWidth); // jianpu will show the 5-line staff with more extra length - if (isJianpu && lineIdx > 0) { + if (isJianpu) { x -= 2 * extraLen; w += 4 * extraLen; } From 123bfeadbe03720664ae1663ec11a18a501e639e Mon Sep 17 00:00:00 2001 From: Song <8042214+songzhenke@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:36:24 +1000 Subject: [PATCH 11/24] Fix Jianpu grace note: magnitude scaling, octave dot offset, diminution beam offset (#7) * Github build warning: declaration of 'staffType' hides previous local declaration * Fix octave dot position issue for grace notes * fix Y position issue for grace-note including its octave dot and beam --- .../rendering/score/beamtremololayout.cpp | 6 +++--- src/engraving/rendering/score/chordlayout.cpp | 21 +++++++++++++------ src/engraving/rendering/score/tdraw.cpp | 12 ++++------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/engraving/rendering/score/beamtremololayout.cpp b/src/engraving/rendering/score/beamtremololayout.cpp index 26032cf4477b..a4563e92f042 100644 --- a/src/engraving/rendering/score/beamtremololayout.cpp +++ b/src/engraving/rendering/score/beamtremololayout.cpp @@ -650,9 +650,9 @@ bool BeamTremoloLayout::calculateAnchors(const BeamBase* item, BeamBase::LayoutD ldata->startAnchor = chordBeamAnchor(ldata, startCr, ChordBeamAnchorType::Start); ldata->endAnchor = chordBeamAnchor(ldata, endCr, ChordBeamAnchorType::End); - double heigh = item->staff()->staffType()->jianpuBoxH() * item->mag(); - double distance = ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->mag(); - double offsetY = heigh * .5 + distance; // Jianpu Y origin is at the center of the number. + const double height = item->staff()->staffTypeForElement(item)->jianpuBoxH() * startCr->mag(); + const double distance = ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * startCr->mag(); + const double offsetY = height * .5 + distance; // Jianpu Y origin is at the center of the number. ldata->startAnchor += PointF(0, offsetY); ldata->endAnchor += PointF(0, offsetY); return true; diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 1a8c255d244f..7c45466e993e 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -131,7 +131,7 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) // https://github.com/musescore/MuseScore/issues/8970 std::vector chordAccidentals; - const bool isJianpuStaff = item->staff() && item->staff()->isJianpuStaff(item->tick()); + const bool isJianpuStaff = item->isJianpuStaff(); auto notes = item->notes(); if (isJianpuStaff) { @@ -143,13 +143,22 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) // Jianpu Y origin is at the center of the number. // It always starts half a spatium above, aligned with the middle of the measure line. double jianpuOffsetY = upnote ? -upnote->spatium() * .5 * mag_ : 0.0; + + // Grace notes are placed above the main notehead, so start with an offset. + if (item->isGrace() && item->staff()) { + // Use the staff scale (mag_) rather than the grace-note scale (item->mag()) so the + // offset matches the normal note's box height, keeping the gap consistent. + jianpuOffsetY -= item->staff()->staffTypeForElement(item)->jianpuBoxH() * mag_; + } + + const double itemMag = item->mag(); for (Note* note : notes) { Note::LayoutData* ldata = note->mutldata(); TLayout::layoutNote(note, ldata); if (isJianpuStaff) { std::vector dots = note->octaveDots(); - double dotDistance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * mag_; + const double dotDistance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * itemMag; // Move the current note down if it is has octave dots above it if (note != upnote && !dots.empty() && dots.back()->above()) { @@ -167,14 +176,14 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) if (note == upnote) { Beam* beam = item->beam(); if (beam) { - jianpuOffsetY += ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * mag_; - jianpuOffsetY -= ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * mag_; + jianpuOffsetY += ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * itemMag; + jianpuOffsetY -= ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * itemMag; jianpuOffsetY += beam->ldata()->bbox().height(); } } jianpuOffsetY += ldata->bbox().height(); - jianpuOffsetY += ctx.conf().styleAbsolute(Sid::jianpuNumberVerticalDistance) * mag_; + jianpuOffsetY += ctx.conf().styleAbsolute(Sid::jianpuNumberVerticalDistance) * itemMag; } double x1 = note->pos().x() + chordX; @@ -1789,7 +1798,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) } double hw = note->headWidth(); - double minX = note->pos().x() + note->bboxXShift(); + double minX = note->bboxXShift(); double maxX = minX + hw; note->resizeOctaveDotsTo(dots); diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index 1fc327b6e8d5..945c0236beee 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -2365,7 +2365,6 @@ void TDraw::draw(const Note* item, Painter* painter, const PaintOptions& opt) const auto config = item->configuration(); const StaffType* staffType = item->staff() ? item->staff()->staffTypeForElement(item) : nullptr; - const bool isJianpu = item->staff() && item->staff()->isJianpuStaff(item->tick()); const bool isTabStaff = staffType && staffType->isTabStaff(); const bool negativeFret = isTabStaff && item->negativeFretUsed(); const bool useCriticalColor = negativeFret && !item->deadNote() && !opt.isPrinting; @@ -2396,17 +2395,14 @@ void TDraw::draw(const Note* item, Painter* painter, const PaintOptions& opt) const double yOffset = tab->fretFontYOffset(); painter->drawText(PointF(startPosX, yOffset * item->magS()), item->fretString()); } - // NOT tablature - else if (isJianpu) { - const Staff* staff = item->staff(); - const StaffType* staffType = staff->staffTypeForElement(item); - + // Jianpu + else if (staffType && item->isJianpuStaff()) { Font f(staffType->jianpuFont()); f.setPointSizeF(f.pointSizeF() * item->magS()); painter->setFont(f); FontMetrics fm(f); - const double fw = fm.width(item->jianpuDigit()) / item->magS(); + const double fw = fm.width(item->jianpuDigit()); const double bw = ldata->bbox().width(); const double startPosX = ldata->bbox().x() + (bw - fw) * .5; const double startPosY = ldata->bbox().bottom(); @@ -2738,7 +2734,7 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) painter->setFont(f); FontMetrics fm(f); - const double jianpuWidth = fm.width(item->jianpuDigit()) / item->magS(); + const double jianpuWidth = fm.width(item->jianpuDigit()); const double jianpuHeight = staffType->jianpuBoxH() * item->magS(); auto bbox = item->ldata()->bbox(); From 9561cdb7cc8c379db2ffc8b73cb49f40715dff51 Mon Sep 17 00:00:00 2001 From: Song <8042214+songzhenke@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:40:52 +1000 Subject: [PATCH 12/24] add vtest jianpu-1 (#3) * add vtest jianpu-01 * add vtest jianpu1-4 * add style to scmx * fix crash when shadow note is out of page * add more vtest * add vtest json * support beam above and slur for Not Angka (#8) * support beam above and slur for Not Angka * ensure dot beam note distance * temporarly remove jianpu vtests --- .../rendering/score/beamtremololayout.cpp | 42 +++++++++++++++++-- src/engraving/rendering/score/chordlayout.cpp | 31 ++++++++------ .../rendering/score/slurtielayout.cpp | 21 ++++++---- src/engraving/rendering/score/tdraw.cpp | 1 - src/engraving/rendering/score/tlayout.cpp | 11 +++-- src/engraving/style/styledef.cpp | 1 + src/engraving/style/styledef.h | 1 + src/notationscene/widgets/editstyle.cpp | 2 + src/notationscene/widgets/editstyle.ui | 42 ++++++++++++++++++- 9 files changed, 122 insertions(+), 30 deletions(-) diff --git a/src/engraving/rendering/score/beamtremololayout.cpp b/src/engraving/rendering/score/beamtremololayout.cpp index a4563e92f042..ca4d1530bf14 100644 --- a/src/engraving/rendering/score/beamtremololayout.cpp +++ b/src/engraving/rendering/score/beamtremololayout.cpp @@ -28,6 +28,7 @@ #include "../dom/chordrest.h" #include "../dom/chord.h" #include "../dom/note.h" +#include "../dom/octavedot.h" #include "../dom/score.h" #include "../dom/staff.h" #include "../dom/stem.h" @@ -625,10 +626,22 @@ bool BeamTremoloLayout::calculateAnchors(const BeamBase* item, BeamBase::LayoutD bool hasChordAboveBeam = false; bool hasChordBelowBeam = false; + const bool isJianpuStaff = item->isJianpuStaff(); + double beamDotStack = 0.0; + bool beamAbove = false; + double beamDistance = 0.0; + double dotDistance = 0.0; + for (auto chordRest : chordRests) { if (!startCr) { startCr = chordRest; + if (isJianpuStaff) { + beamAbove = ctx.conf().styleV(Sid::jianpuDiminutionBeamPlacement).value() == PlacementV::ABOVE; + beamDistance = ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * startCr->mag(); + dotDistance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * startCr->mag(); + } } + endCr = chordRest; if (chordRest->isChord()) { if (!startChord) { @@ -644,15 +657,36 @@ bool BeamTremoloLayout::calculateAnchors(const BeamBase* item, BeamBase::LayoutD } else { hasChordAboveBeam = true; } + + if (isJianpuStaff && beamAbove) { + // For Jianpu staffs with beams above, we need to calculate the maximum possible stack + // of beams and above octave dots for any single chord, so that we can offset the beam + // accordingly to avoid collisions with the dots. + int aboveDots = 0; + const Chord* chord = chordRest->isChord() ? toChord(chordRest) : nullptr; + if (chord) { + std::vector dots = chord->upNote()->octaveDots(); + if (!dots.empty() && dots.front()->above()) { + aboveDots = static_cast(dots.size()); + } + } + + const int beamLines = strokeCount(ldata, chordRest); + const double beamStack = beamLines * beamDistance; + const double dotStack = aboveDots > 0 ? aboveDots * dotDistance : 0.0; + beamDotStack = std::max(beamDotStack, beamStack + dotStack); + } } - if (item->isJianpuStaff()) { + if (isJianpuStaff) { ldata->startAnchor = chordBeamAnchor(ldata, startCr, ChordBeamAnchorType::Start); ldata->endAnchor = chordBeamAnchor(ldata, endCr, ChordBeamAnchorType::End); - const double height = item->staff()->staffTypeForElement(item)->jianpuBoxH() * startCr->mag(); - const double distance = ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * startCr->mag(); - const double offsetY = height * .5 + distance; // Jianpu Y origin is at the center of the number. + // Jianpu Y origin is at the center of the number. + const double halfHeight = item->staff()->staffTypeForElement(item)->jianpuBoxH() * startCr->mag() * .5; + // ABOVE: offset up by height/2 + stacked beam and dot + // BELOW (default): offset down by height/2 + distance + const double offsetY = beamAbove ? -(halfHeight + beamDotStack) : halfHeight + beamDistance; ldata->startAnchor += PointF(0, offsetY); ldata->endAnchor += PointF(0, offsetY); return true; diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 7c45466e993e..62337ac4cc64 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -81,6 +81,18 @@ using namespace mu::engraving::rendering::score; static constexpr Spatium STAFFTYPE_TAB_DEFAULTDOTDIST_X = 0.75_sp; +static double jianpuBeamOffset(const Chord* chord, const LayoutContext& ctx) +{ + const Beam* beam = chord->beam(); + if (!beam || ctx.conf().styleV(Sid::jianpuDiminutionBeamPlacement).value() != PlacementV::BELOW) { + return 0.0; + } + + const int lines = BeamTremoloLayout::strokeCount(beam->ldata(), chord); + const double distance = ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance); + return (distance * lines) * chord->magS(); +} + void ChordLayout::layout(Chord* item, LayoutContext& ctx) { if (item->notes().empty()) { @@ -158,6 +170,10 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) if (isJianpuStaff) { std::vector dots = note->octaveDots(); + // Jianpu spacing rules for octave dots, beams, and notes: + // - Dot-to-note uses dot centerline to note boundary distance. + // - Beam-to-note uses beam centerline to note boundary distance. + // - Dot-to-dot and beam-to-dot/beam use centerline-to-centerline distance. const double dotDistance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * itemMag; // Move the current note down if it is has octave dots above it @@ -173,13 +189,9 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) jianpuOffsetY += dots.size() * dotDistance; } + // Add extra offset for beam if it is upnote if (note == upnote) { - Beam* beam = item->beam(); - if (beam) { - jianpuOffsetY += ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * itemMag; - jianpuOffsetY -= ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * itemMag; - jianpuOffsetY += beam->ldata()->bbox().height(); - } + jianpuOffsetY += jianpuBeamOffset(item, ctx); } jianpuOffsetY += ldata->bbox().height(); @@ -1785,12 +1797,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) offsetY = height * .5 + distance; if (note == item->upNote()) { // The octave dot should be under the jianpu beam - Beam* beam = item->beam(); - if (beam) { - offsetY += ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->magS(); - offsetY -= ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->magS(); - offsetY += beam->ldata()->bbox().height(); - } + offsetY += jianpuBeamOffset(item, ctx); } } else { note->resizeOctaveDotsTo(0); diff --git a/src/engraving/rendering/score/slurtielayout.cpp b/src/engraving/rendering/score/slurtielayout.cpp index 8fbc2a8748f1..3be7b7031a28 100644 --- a/src/engraving/rendering/score/slurtielayout.cpp +++ b/src/engraving/rendering/score/slurtielayout.cpp @@ -384,8 +384,8 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) } bool useTablature = item->staff() && item->staff()->isTabStaff(item->endCR()->tick()); - bool useJianpu = item->staff() && item->staff()->isJianpuStaff(item->endCR()->tick()); - bool staffHasStems = true; // assume staff uses stems + bool useJianpu = item->isJianpuStaff(); + bool staffHasStems = useJianpu ? false : true; // assume staff uses stems except jianpu const StaffType* stt = 0; if (useTablature) { stt = item->staff()->staffType(item->tick()); @@ -556,8 +556,15 @@ void SlurTieLayout::slurPos(Slur* item, SlurTiePos* sp, LayoutContext& ctx) break; } - // Slur for jianpu is always horizontal above chords - double jianpuY = std::min(scr->ldata()->bbox().top(), ecr->ldata()->bbox().top()); + // Slur for jianpu is always horizontal + double jianpuY = 0.0; + if (useJianpu) { + if (item->up()) { + jianpuY = std::min(scr->ldata()->bbox().top(), ecr->ldata()->bbox().top()); + } else { + jianpuY = std::max(scr->ldata()->bbox().bottom(), ecr->ldata()->bbox().bottom()); + } + } // // default position: @@ -2638,9 +2645,9 @@ void SlurTieLayout::computeUp(Slur* slur, LayoutContext& ctx) break; } - if (chord1 && chord1->staff()->isJianpuStaff(chord1->tick())) { - // Jianpu slurs always go up - slur->setUp(true); + if (chord1 && chord1->isJianpuStaff()) { + // In Jianpu mode, slurs follow the opposite side of the diminution beams. + slur->setUp(ctx.conf().style().styleV(Sid::jianpuDiminutionBeamPlacement).value() != PlacementV::ABOVE); break; } diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index 945c0236beee..c719b824540d 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -2749,7 +2749,6 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) jianpuY = bbox.height() + bbox.y(); jianpuY -= jianpuHeight; jianpuY -= dotDistance * abs(item->jianpuOctaveDots()) + lineDistance * item->jianpuDiminutionLines(); - jianpuY -= rad * 2; } painter->save(); diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index 6292bd620fe5..a206ff181c5e 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -4760,11 +4760,14 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) if (item->isRest()) { item->setJianpuDigit(String(u"0")); } else { - Position pos; + item->setJianpuDigit(String()); Score* score = item->score(); - score->getPosition(&pos, item->pos(), item->track()); - bool error = false; - NoteVal nval = score->noteValForPosition(pos, item->accidentalType(), error); + Position pos; + bool error = !(score && score->getPosition(&pos, item->pos(), item->track()) && pos.segment); + NoteVal nval; + if (!error) { + nval = score->noteValForPosition(pos, item->accidentalType(), error); + } if (!error) { bool concertPitch = ctx.conf().styleB(Sid::concertPitch); int tpc = nval.tpc(concertPitch); diff --git a/src/engraving/style/styledef.cpp b/src/engraving/style/styledef.cpp index dd30a6f024ba..d17f95f82073 100644 --- a/src/engraving/style/styledef.cpp +++ b/src/engraving/style/styledef.cpp @@ -2243,6 +2243,7 @@ const std::array StyleDef::styleValue styleDef(jianpuOctaveDotDistance, 0.3_sp), styleDef(jianpuDiminutionBeamThickness, 0.1_sp), styleDef(jianpuDiminutionBeamDistance, 0.3_sp), + styleDef(jianpuDiminutionBeamPlacement, PlacementV::BELOW), styleDef(jianpuDurationLineThickness, 0.1_sp), } }; diff --git a/src/engraving/style/styledef.h b/src/engraving/style/styledef.h index 8b77dfabdde9..71c8b9dced61 100644 --- a/src/engraving/style/styledef.h +++ b/src/engraving/style/styledef.h @@ -2261,6 +2261,7 @@ enum class Sid : short { jianpuOctaveDotDistance, jianpuDiminutionBeamThickness, jianpuDiminutionBeamDistance, + jianpuDiminutionBeamPlacement, jianpuDurationLineThickness, STYLES }; diff --git a/src/notationscene/widgets/editstyle.cpp b/src/notationscene/widgets/editstyle.cpp index b6a2026e6035..8f3c149c5153 100644 --- a/src/notationscene/widgets/editstyle.cpp +++ b/src/notationscene/widgets/editstyle.cpp @@ -711,6 +711,7 @@ void EditStyle::classBegin() { StyleId::jianpuOctaveDotDistance, false, jianpuOctaveDotDistance, jianpuOctaveDotDistanceReset }, { StyleId::jianpuDiminutionBeamThickness, false, jianpuDiminutionBeamThickness, jianpuDiminutionBeamThicknessReset }, { StyleId::jianpuDiminutionBeamDistance, false, jianpuDiminutionBeamDistance, jianpuDiminutionBeamDistanceReset }, + { StyleId::jianpuDiminutionBeamPlacement, false, jianpuDiminutionBeamPlacement, jianpuDiminutionBeamPlacementReset }, { StyleId::jianpuDurationLineThickness, false, jianpuDurationLineThickness, jianpuDurationLineThicknessReset }, }; @@ -776,6 +777,7 @@ void EditStyle::classBegin() tempoTextPlacement, staffTextPlacement, rehearsalMarkPlacement, + jianpuDiminutionBeamPlacement, }; for (QComboBox* cb : verticalPlacementComboBoxes) { diff --git a/src/notationscene/widgets/editstyle.ui b/src/notationscene/widgets/editstyle.ui index 768fbca914fc..ad919ac7fcd3 100644 --- a/src/notationscene/widgets/editstyle.ui +++ b/src/notationscene/widgets/editstyle.ui @@ -12992,6 +12992,43 @@ followed by dashes + + + Diminution beam placement: + + + jianpuDiminutionBeamPlacement + + + + + + + + Above + + + + + Below + + + + + + + + Reset to default + + + Reset 'Diminution beam placement' value + + + + + + + Duration line thickness: @@ -13001,7 +13038,7 @@ followed by dashes - + false @@ -13017,7 +13054,7 @@ followed by dashes - + Reset to default @@ -14364,6 +14401,7 @@ followed by dashes jianpuOctaveDotDistance jianpuDiminutionBeamThickness jianpuDiminutionBeamDistance + jianpuDiminutionBeamPlacement jianpuDurationLineThickness From ff0de9df44a810c72569ce0802b4daa991921186 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Tue, 23 Jun 2026 00:38:13 +1000 Subject: [PATCH 13/24] Support beam above/below for shdownote --- src/engraving/rendering/score/tdraw.cpp | 72 +++++++++++++++++-------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index c719b824540d..67332029c8d4 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -2740,46 +2740,76 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) auto bbox = item->ldata()->bbox(); double jianpuX = (noteheadWidth - jianpuWidth) * .5; double jianpuY = bbox.y(); - double lineDistance = item->style().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->magS(); - double lineThickness = item->style().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->magS(); - double dotDistance = item->style().styleAbsolute(Sid::jianpuOctaveDotDistance) * item->magS(); - double rad = item->style().styleAbsolute(Sid::jianpuOctaveDotRadius) * item->magS(); + const bool beamAbove = item->style().styleV(Sid::jianpuDiminutionBeamPlacement).value() == PlacementV::ABOVE; + const double beamDistance = item->style().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->magS(); + const double beamThickness = item->style().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->magS(); + const double dotDistance = item->style().styleAbsolute(Sid::jianpuOctaveDotDistance) * item->magS(); + const double dotRadius = item->style().styleAbsolute(Sid::jianpuOctaveDotRadius) * item->magS(); if (item->lineIndex() >= 0) { jianpuY = bbox.height() + bbox.y(); jianpuY -= jianpuHeight; - jianpuY -= dotDistance * abs(item->jianpuOctaveDots()) + lineDistance * item->jianpuDiminutionLines(); + jianpuY -= dotDistance * abs(item->jianpuOctaveDots()); + jianpuY -= beamDistance * item->jianpuDiminutionLines(); } + // Save painter state before drawing beams and dots, as they may change pen width painter->save(); pen.setWidthF(1.0); painter->setPen(pen); painter->setBrush(Brush(pen.color())); - double dotX = noteheadWidth * .5; - for (int i = 0; i < -item->jianpuOctaveDots(); i++) { - painter->drawEllipse(PointF(dotX, jianpuY + rad), rad, rad); - jianpuY += dotDistance; + auto drawOctaveDots = [&jianpuY, ¬eheadWidth, &dotDistance, &dotRadius, &item, &painter]() { + for (int i = 0; i < abs(item->jianpuOctaveDots()); i++) { + jianpuY += dotDistance; + painter->drawEllipse(PointF(noteheadWidth * .5, jianpuY), dotRadius, dotRadius); + } + }; + + auto drawBeam = [&jianpuY, ¬eheadWidth, &beamDistance, &beamThickness, &item, &pen, &painter]() { + painter->save(); + pen.setWidthF(beamThickness); + painter->setPen(pen); + for (int i = 0; i < item->jianpuDiminutionLines(); i++) { + jianpuY += beamDistance; + painter->drawLine(LineF(0.0, jianpuY, noteheadWidth, jianpuY)); + } + painter->restore(); + }; + + // Draw beams above the jianpu digit + bool hasBeamAbove = false; + double toNoteDistance = 0; + if (beamAbove && item->jianpuDiminutionLines()) { + hasBeamAbove = true; + jianpuY -= beamDistance; // Pre offset for 1st beam + toNoteDistance = beamDistance; + drawBeam(); + } + + // Draw octave dots above the jianpu digit + if (item->jianpuOctaveDots() < 0) { + jianpuY -= hasBeamAbove ? 0.0 : dotDistance; // Pre offset for 1st dot if no beam + toNoteDistance = dotDistance; + drawOctaveDots(); } - jianpuY += jianpuHeight; + // Draw jianpu digit + jianpuY += toNoteDistance; // Spacing for beams or dots to the notehead + jianpuY += jianpuHeight; // Spacing from jianpu digit painter->drawText(jianpuX, jianpuY, item->jianpuDigit()); - jianpuY += item->jianpuDiminutionLines() ? lineDistance : dotDistance; - pen.setWidthF(lineThickness); - painter->setPen(pen); - for (int i = 0; i < item->jianpuDiminutionLines(); i++) { - painter->drawLine(LineF(0.0, jianpuY, noteheadWidth, jianpuY)); - jianpuY += lineDistance; + // Draw beams below the jianpu digit + if (!beamAbove && item->jianpuDiminutionLines()) { + drawBeam(); } - pen.setWidthF(1.0); - painter->setPen(pen); - for (int i = 0; i < item->jianpuOctaveDots(); i++) { - painter->drawEllipse(PointF(dotX, jianpuY + rad), rad, rad); - jianpuY += dotDistance; + // Draw octave dots below the jianpu digit + if (item->jianpuOctaveDots() > 0) { + drawOctaveDots(); } + // Restore painter state after drawing beams and dots painter->restore(); } From ab409442ab2538aada61861dbbf943141300556f Mon Sep 17 00:00:00 2001 From: songzhenk Date: Wed, 26 Aug 2026 22:09:09 +1000 Subject: [PATCH 14/24] cleanup and add unit test --- src/engraving/dom/chordrest.cpp | 3 +- src/engraving/dom/durationline.cpp | 26 ++- src/engraving/dom/durationline.h | 6 +- src/engraving/dom/durationtype.cpp | 3 + src/engraving/dom/note.cpp | 1 + src/engraving/dom/octavedot.cpp | 10 +- src/engraving/dom/octavedot.h | 2 +- src/engraving/tests/CMakeLists.txt | 2 + src/engraving/tests/durationline_tests.cpp | 233 +++++++++++++++++++++ src/engraving/tests/durationtype_tests.cpp | 37 ++++ src/engraving/tests/octavedot_tests.cpp | 226 ++++++++++++++++++++ 11 files changed, 527 insertions(+), 22 deletions(-) create mode 100644 src/engraving/tests/durationline_tests.cpp create mode 100644 src/engraving/tests/octavedot_tests.cpp diff --git a/src/engraving/dom/chordrest.cpp b/src/engraving/dom/chordrest.cpp index b86d620dfdf3..7fc2173713e5 100644 --- a/src/engraving/dom/chordrest.cpp +++ b/src/engraving/dom/chordrest.cpp @@ -1392,7 +1392,8 @@ bool ChordRest::hasPrecedingJumpItem() const void ChordRest::resizeDurationLinesTo(size_t newSize) { while (m_durationLines.size() < newSize) { - DurationLine* dl = new DurationLine(score()->dummy()); + DurationLine* dl = new DurationLine(this); + dl->setParent(this); dl->setTrack(track()); m_durationLines.push_back(dl); } diff --git a/src/engraving/dom/durationline.cpp b/src/engraving/dom/durationline.cpp index c730161d7dec..2e811ba4d87d 100644 --- a/src/engraving/dom/durationline.cpp +++ b/src/engraving/dom/durationline.cpp @@ -41,11 +41,6 @@ DurationLine::DurationLine(EngravingItem* s) : EngravingItem(ElementType::DURATION_LINE, s) { setSelectable(false); - m_len = 0.; -} - -DurationLine::~DurationLine() -{ } //--------------------------------------------------------- @@ -54,9 +49,20 @@ DurationLine::~DurationLine() PointF DurationLine::pagePos() const { - System* system = chordRest()->measure()->system(); - double yp = y() + system->staff(staffIdx())->y() + system->y(); - return PointF(pageX(), yp); + if (ChordRest* chordRest = this->chordRest()) { + if (Measure* measure = chordRest->measure()) { + if (System* system = measure->system()) { + if (system->staves().size() > staffIdx()) { + if (SysStaff* staff = system->staff(staffIdx())) { + double yp = y() + staff->y() + system->y(); + return PointF(pageX(), yp); + } + } + } + } + } + + return EngravingItem::pagePos(); } //--------------------------------------------------------- @@ -65,7 +71,9 @@ PointF DurationLine::pagePos() const void DurationLine::spatiumChanged(double oldValue, double newValue) { - m_len = (m_len / oldValue) * newValue; + if (oldValue != 0.0) { + m_len = (m_len / oldValue) * newValue; + } } //--------------------------------------------------------- diff --git a/src/engraving/dom/durationline.h b/src/engraving/dom/durationline.h index 678eac617164..132d99f43f47 100644 --- a/src/engraving/dom/durationline.h +++ b/src/engraving/dom/durationline.h @@ -26,13 +26,11 @@ #include "engravingitem.h" namespace mu::engraving { -class Chord; - //--------------------------------------------------------- // @@ DurationLine /// Graphic representation of a duration line. //! -//! parent: Chord +//! parent: ChordRest //! x-origin: Chord //! y-origin: SStaff //--------------------------------------------------------- @@ -44,7 +42,7 @@ class DurationLine final : public EngravingItem public: DurationLine(EngravingItem*); - ~DurationLine(); + ~DurationLine() override = default; DurationLine& operator=(const DurationLine&) = delete; DurationLine* clone() const override { return new DurationLine(*this); } diff --git a/src/engraving/dom/durationtype.cpp b/src/engraving/dom/durationtype.cpp index 3a81efa12292..eae0e882b50e 100644 --- a/src/engraving/dom/durationtype.cpp +++ b/src/engraving/dom/durationtype.cpp @@ -826,6 +826,9 @@ bool TDuration::isValid(Fraction f) //--------------------------------------------------------- // augmentation lines - number of Line to lengthen duration +// +// The undotted duration uses base - 1 lines. Each dot adds half +// the line count represented by the preceding duration value. //--------------------------------------------------------- int TDuration::augmentationLines() const { diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index 54d1f1ca67e4..15acaa0e9d2a 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -4275,6 +4275,7 @@ void Note::resizeOctaveDotsTo(size_t newSize) { while (m_octaveDots.size() < newSize) { OctaveDot* dot = new OctaveDot(this); + dot->setParent(this); dot->setTrack(track()); m_octaveDots.push_back(dot); } diff --git a/src/engraving/dom/octavedot.cpp b/src/engraving/dom/octavedot.cpp index c583a17ab6ef..9a40b0e7af5a 100644 --- a/src/engraving/dom/octavedot.cpp +++ b/src/engraving/dom/octavedot.cpp @@ -40,12 +40,6 @@ OctaveDot::OctaveDot(EngravingItem* s) : EngravingItem(ElementType::OCTAVE_DOT, s) { setSelectable(false); - m_len = 0.; - m_above = false; -} - -OctaveDot::~OctaveDot() -{ } //--------------------------------------------------------- @@ -54,7 +48,9 @@ OctaveDot::~OctaveDot() void OctaveDot::spatiumChanged(double oldValue, double newValue) { - m_len = (m_len / oldValue) * newValue; + if (oldValue != 0.0) { + m_len = (m_len / oldValue) * newValue; + } } //--------------------------------------------------------- diff --git a/src/engraving/dom/octavedot.h b/src/engraving/dom/octavedot.h index b6d334010e25..62cce75af4e2 100644 --- a/src/engraving/dom/octavedot.h +++ b/src/engraving/dom/octavedot.h @@ -44,7 +44,7 @@ class OctaveDot final : public EngravingItem public: OctaveDot(EngravingItem*); - ~OctaveDot(); + ~OctaveDot() override = default; OctaveDot& operator=(const OctaveDot&) = delete; OctaveDot* clone() const override { return new OctaveDot(*this); } diff --git a/src/engraving/tests/CMakeLists.txt b/src/engraving/tests/CMakeLists.txt index 7e11705cc2ab..0bd6d1de2706 100644 --- a/src/engraving/tests/CMakeLists.txt +++ b/src/engraving/tests/CMakeLists.txt @@ -59,6 +59,7 @@ set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/hideemptystaves_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/implodeexplode_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/instrumentchange_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/durationline_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/join_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/keysig_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/layoutelements_tests.cpp @@ -68,6 +69,7 @@ set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/midi/midirenderer_bend_tests.cpp #${CMAKE_CURRENT_LIST_DIR}/midimapping_tests.cpp doesn't compile and needs actualization ${CMAKE_CURRENT_LIST_DIR}/note_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/octavedot_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/parts_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/partialtie_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/pitchwheelrender_tests.cpp diff --git a/src/engraving/tests/durationline_tests.cpp b/src/engraving/tests/durationline_tests.cpp new file mode 100644 index 000000000000..eef299a30809 --- /dev/null +++ b/src/engraving/tests/durationline_tests.cpp @@ -0,0 +1,233 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited 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 . + */ + +#include + +#include + +#include "types/translatablestring.h" + +#include "engraving/dom/chordrest.h" +#include "engraving/dom/durationline.h" +#include "engraving/dom/engravingitem.h" +#include "engraving/dom/masterscore.h" +#include "engraving/dom/segment.h" + +#include "utils/scorerw.h" + +using namespace mu::engraving; + +static const String JIANPU_DURATION_LINES_DATA_DIR("beam_data/"); + +namespace { +struct ChordRestLocator { + Fraction tick; + track_idx_t track = 0; +}; + +ChordRest* findJianpuChordRestWithDurationLines(MasterScore* score) +{ + for (Segment* seg = score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { + for (track_idx_t track = 0; track < score->ntracks(); ++track) { + EngravingItem* item = seg->element(track); + if (!item || !item->isChordRest()) { + continue; + } + + ChordRest* chordRest = toChordRest(item); + if (!chordRest->isJianpuStaff() || chordRest->durationLines().empty()) { + continue; + } + + return chordRest; + } + } + + return nullptr; +} + +ChordRest* findChordRestByLocator(MasterScore* score, const ChordRestLocator& locator) +{ + for (Segment* seg = score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { + for (track_idx_t track = 0; track < score->ntracks(); ++track) { + EngravingItem* item = seg->element(track); + if (!item || !item->isChordRest()) { + continue; + } + + ChordRest* chordRest = toChordRest(item); + if (chordRest->track() == locator.track && chordRest->tick() == locator.tick) { + return chordRest; + } + } + } + + return nullptr; +} + +void expectDurationLinesOwnedByChordRest(const ChordRest* chordRest) +{ + ASSERT_FALSE(chordRest->durationLines().empty()); + + for (DurationLine* durationLine : chordRest->durationLines()) { + ASSERT_NE(durationLine, nullptr); + EXPECT_EQ(durationLine->explicitParent(), chordRest); + EXPECT_EQ(durationLine->track(), chordRest->track()); + } +} +} + +class Engraving_DurationLineTests : public ::testing::Test +{ +}; + +TEST_F(Engraving_DurationLineTests, propertiesAndSpatiumScaling) +{ + MasterScore* score = ScoreRW::readScore(JIANPU_DURATION_LINES_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + ChordRest* chordRest = findJianpuChordRestWithDurationLines(score); + ASSERT_TRUE(chordRest); + ASSERT_FALSE(chordRest->durationLines().empty()); + + DurationLine* durationLine = chordRest->durationLines().front(); + ASSERT_TRUE(durationLine); + EXPECT_EQ(durationLine->chordRest(), chordRest); + EXPECT_DOUBLE_EQ(durationLine->mag(), chordRest->mag()); + + durationLine->setLen(2.0); + EXPECT_DOUBLE_EQ(durationLine->len(), 2.0); + + durationLine->spatiumChanged(2.0, 3.0); + EXPECT_DOUBLE_EQ(durationLine->len(), 3.0); + + durationLine->spatiumChanged(0.0, 3.0); + EXPECT_DOUBLE_EQ(durationLine->len(), 3.0); + + delete score; +} + +TEST_F(Engraving_DurationLineTests, detachedDurationLineUsesLocalPosition) +{ + MasterScore* score = ScoreRW::readScore(JIANPU_DURATION_LINES_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + ChordRest* chordRest = findJianpuChordRestWithDurationLines(score); + ASSERT_TRUE(chordRest); + + { + // EngravingObject requires a non-null constructor parent, so detach it afterwards. + // Scoped so it is destroyed (and unlinked from chordRest) before the score is deleted. + DurationLine durationLine(chordRest); + durationLine.resetExplicitParent(); + durationLine.setPos(PointF(2.0, 3.0)); + + EXPECT_EQ(durationLine.pagePos(), PointF(2.0, 3.0)); + } + + delete score; +} + +TEST_F(Engraving_DurationLineTests, jianpuUndoRedoRemoveChordRestKeepsDurationLinesOwnership) +{ + MasterScore* score = ScoreRW::readScore(JIANPU_DURATION_LINES_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + ChordRest* original = findJianpuChordRestWithDurationLines(score); + ASSERT_TRUE(original); + expectDurationLinesOwnedByChordRest(original); + + const ChordRestLocator locator { original->tick(), original->track() }; + + score->startCmd(muse::TranslatableString::untranslatable("Jianpu duration lines tests")); + score->undoRemoveElement(original); + score->endCmd(); + + EXPECT_EQ(findChordRestByLocator(score, locator), nullptr); + + score->undoRedo(true, nullptr); + + ChordRest* restored = findChordRestByLocator(score, locator); + ASSERT_TRUE(restored); + expectDurationLinesOwnedByChordRest(restored); + + score->undoRedo(false, nullptr); + EXPECT_EQ(findChordRestByLocator(score, locator), nullptr); + + delete score; +} + +TEST_F(Engraving_DurationLineTests, jianpuCloneResizeDurationLinesUsesCloneAsOwner) +{ + MasterScore* score = ScoreRW::readScore(JIANPU_DURATION_LINES_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + ChordRest* source = findJianpuChordRestWithDurationLines(score); + ASSERT_TRUE(source); + ASSERT_FALSE(source->durationLines().empty()); + + DurationLine* sourceDurationLine = source->durationLines().front(); + + EngravingItem* clonedItem = source->clone(); + ASSERT_TRUE(clonedItem); + ASSERT_TRUE(clonedItem->isChordRest()); + + ChordRest* cloned = toChordRest(clonedItem); + const size_t lineCount = std::max(1, source->durationLines().size()); + cloned->resizeDurationLinesTo(lineCount); + expectDurationLinesOwnedByChordRest(cloned); + ASSERT_FALSE(cloned->durationLines().empty()); + + DurationLine* clonedDurationLine = cloned->durationLines().front(); + clonedDurationLine->setLen(2.5); + EXPECT_DOUBLE_EQ(clonedDurationLine->len(), 2.5); + EXPECT_NE(clonedDurationLine, sourceDurationLine); + + delete cloned; + delete score; +} + +TEST_F(Engraving_DurationLineTests, jianpuSaveReloadKeepsDurationLinesOwnership) +{ + String tempFile(u"jianpu-durationlines-save-reload-test.mscx"); + + MasterScore* score = ScoreRW::readScore(JIANPU_DURATION_LINES_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + ChordRest* beforeSave = findJianpuChordRestWithDurationLines(score); + ASSERT_TRUE(beforeSave); + expectDurationLinesOwnedByChordRest(beforeSave); + + const ChordRestLocator locator { beforeSave->tick(), beforeSave->track() }; + + ASSERT_TRUE(ScoreRW::saveScore(score, tempFile)); + delete score; + + MasterScore* reloaded = ScoreRW::readScore(tempFile, true); + ASSERT_TRUE(reloaded); + + ChordRest* afterReload = findChordRestByLocator(reloaded, locator); + ASSERT_TRUE(afterReload); + expectDurationLinesOwnedByChordRest(afterReload); + + delete reloaded; +} diff --git a/src/engraving/tests/durationtype_tests.cpp b/src/engraving/tests/durationtype_tests.cpp index be8cfa7694a3..1b971a258301 100644 --- a/src/engraving/tests/durationtype_tests.cpp +++ b/src/engraving/tests/durationtype_tests.cpp @@ -23,6 +23,7 @@ #include #include "engraving/dom/chord.h" +#include "engraving/dom/durationtype.h" #include "engraving/dom/masterscore.h" #include "engraving/dom/measure.h" #include "engraving/dom/segment.h" @@ -45,6 +46,8 @@ private slots: void doubleDuration(); void decDurationDotted(); void incDurationDotted(); + void augmentationLines(); + void diminutionLines(); }; // Simple tests for command "half-duration" (default shortcut "Q"). @@ -170,3 +173,37 @@ TEST_F(Engraving_DurationTypeTests, incDurationDotted) delete score; } + +TEST_F(Engraving_DurationTypeTests, augmentationLines) +{ + auto durationLines = [](DurationType type, int dots) { + TDuration duration(type); + duration.setDots(dots); + return duration.augmentationLines(); + }; + + EXPECT_EQ(durationLines(DurationType::V_HALF, 0), 1); + EXPECT_EQ(durationLines(DurationType::V_HALF, 1), 2); + EXPECT_EQ(durationLines(DurationType::V_HALF, 2), 2); + EXPECT_EQ(durationLines(DurationType::V_WHOLE, 0), 3); + EXPECT_EQ(durationLines(DurationType::V_WHOLE, 1), 5); + EXPECT_EQ(durationLines(DurationType::V_BREVE, 0), 7); + EXPECT_EQ(durationLines(DurationType::V_LONG, 0), 15); +} + +TEST_F(Engraving_DurationTypeTests, diminutionLines) +{ + EXPECT_EQ(TDuration(DurationType::V_LONG).diminutionLines(), 0); + EXPECT_EQ(TDuration(DurationType::V_BREVE).diminutionLines(), 0); + EXPECT_EQ(TDuration(DurationType::V_WHOLE).diminutionLines(), 0); + EXPECT_EQ(TDuration(DurationType::V_HALF).diminutionLines(), 0); + EXPECT_EQ(TDuration(DurationType::V_QUARTER).diminutionLines(), 0); + EXPECT_EQ(TDuration(DurationType::V_EIGHTH).diminutionLines(), 1); + EXPECT_EQ(TDuration(DurationType::V_16TH).diminutionLines(), 2); + EXPECT_EQ(TDuration(DurationType::V_32ND).diminutionLines(), 3); + EXPECT_EQ(TDuration(DurationType::V_64TH).diminutionLines(), 4); + EXPECT_EQ(TDuration(DurationType::V_128TH).diminutionLines(), 5); + EXPECT_EQ(TDuration(DurationType::V_256TH).diminutionLines(), 6); + EXPECT_EQ(TDuration(DurationType::V_512TH).diminutionLines(), 7); + EXPECT_EQ(TDuration(DurationType::V_1024TH).diminutionLines(), 8); +} diff --git a/src/engraving/tests/octavedot_tests.cpp b/src/engraving/tests/octavedot_tests.cpp new file mode 100644 index 000000000000..49b03b8e8c55 --- /dev/null +++ b/src/engraving/tests/octavedot_tests.cpp @@ -0,0 +1,226 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited 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 . + */ + +#include + +#include "types/translatablestring.h" + +#include "engraving/dom/chord.h" +#include "engraving/dom/engravingitem.h" +#include "engraving/dom/masterscore.h" +#include "engraving/dom/note.h" +#include "engraving/dom/octavedot.h" +#include "engraving/dom/segment.h" + +#include "utils/scorerw.h" + +using namespace mu::engraving; + +static const String JIANPU_OCTAVE_DOTS_DATA_DIR("beam_data/"); + +namespace { +struct NoteLocator { + Fraction tick; + track_idx_t track = 0; + int pitch = -1; +}; + +Note* findJianpuNoteInChord(MasterScore* score) +{ + for (Segment* seg = score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { + for (track_idx_t track = 0; track < score->ntracks(); ++track) { + EngravingItem* item = seg->element(track); + if (!item || !item->isChord()) { + continue; + } + + Chord* chord = toChord(item); + if (!chord->isJianpuStaff() || chord->notes().empty()) { + continue; + } + + return chord->notes().front(); + } + } + + return nullptr; +} + +Note* findNoteByLocator(MasterScore* score, const NoteLocator& locator) +{ + for (Segment* seg = score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { + EngravingItem* item = seg->element(locator.track); + if (!item || !item->isChord()) { + continue; + } + + Chord* chord = toChord(item); + if (chord->tick() != locator.tick) { + continue; + } + + for (Note* note : chord->notes()) { + if (locator.pitch == -1 || note->pitch() == locator.pitch) { + return note; + } + } + } + + return nullptr; +} + +void expectOctaveDotsOwnedByNote(const Note* note) +{ + ASSERT_FALSE(note->octaveDots().empty()); + + for (OctaveDot* octaveDot : note->octaveDots()) { + ASSERT_NE(octaveDot, nullptr); + EXPECT_EQ(octaveDot->explicitParent(), note); + EXPECT_EQ(octaveDot->track(), note->track()); + } +} +} + +class Engraving_OctaveDotTests : public ::testing::Test +{ +}; + +TEST_F(Engraving_OctaveDotTests, propertiesAndSpatiumScaling) +{ + MasterScore* score = ScoreRW::readScore(JIANPU_OCTAVE_DOTS_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + Note* note = findJianpuNoteInChord(score); + ASSERT_TRUE(note); + + note->resizeOctaveDotsTo(1); + ASSERT_FALSE(note->octaveDots().empty()); + + OctaveDot* octaveDot = note->octaveDots().front(); + EXPECT_EQ(octaveDot->note(), note); + + octaveDot->setLen(2.0); + octaveDot->setAbove(true); + EXPECT_DOUBLE_EQ(octaveDot->len(), 2.0); + EXPECT_TRUE(octaveDot->above()); + + octaveDot->spatiumChanged(2.0, 3.0); + EXPECT_DOUBLE_EQ(octaveDot->len(), 3.0); + + octaveDot->spatiumChanged(0.0, 3.0); + EXPECT_DOUBLE_EQ(octaveDot->len(), 3.0); + + delete score; +} + +TEST_F(Engraving_OctaveDotTests, jianpuUndoRedoRemoveNoteKeepsOctaveDotsOwnership) +{ + MasterScore* score = ScoreRW::readScore(JIANPU_OCTAVE_DOTS_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + Note* original = findJianpuNoteInChord(score); + ASSERT_TRUE(original); + + const NoteLocator locator { original->chord()->tick(), original->track(), original->pitch() }; + + // Force octave dots to be allocated + original->resizeOctaveDotsTo(1); + expectOctaveDotsOwnedByNote(original); + + Chord* chord = original->chord(); + score->startCmd(muse::TranslatableString::untranslatable("Jianpu octave dots tests")); + score->undoRemoveElement(chord); + score->endCmd(); + + EXPECT_EQ(findNoteByLocator(score, locator), nullptr); + + score->undoRedo(true, nullptr); + + Note* restored = findNoteByLocator(score, locator); + ASSERT_TRUE(restored); + expectOctaveDotsOwnedByNote(restored); + + score->undoRedo(false, nullptr); + EXPECT_EQ(findNoteByLocator(score, locator), nullptr); + + delete score; +} + +TEST_F(Engraving_OctaveDotTests, jianpuCloneNoteKeepsOctaveDotsOwnership) +{ + MasterScore* score = ScoreRW::readScore(JIANPU_OCTAVE_DOTS_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + Note* source = findJianpuNoteInChord(score); + ASSERT_TRUE(source); + + // Force octave dots to be allocated on source + source->resizeOctaveDotsTo(1); + expectOctaveDotsOwnedByNote(source); + + EngravingItem* clonedItem = source->clone(); + ASSERT_TRUE(clonedItem); + ASSERT_TRUE(clonedItem->isNote()); + + Note* cloned = toNote(clonedItem); + cloned->resizeOctaveDotsTo(1); + expectOctaveDotsOwnedByNote(cloned); + + OctaveDot* clonedOctaveDot = cloned->octaveDots().front(); + clonedOctaveDot->setLen(2.5); + clonedOctaveDot->setAbove(true); + EXPECT_DOUBLE_EQ(clonedOctaveDot->len(), 2.5); + EXPECT_TRUE(clonedOctaveDot->above()); + EXPECT_NE(clonedOctaveDot, source->octaveDots().front()); + + delete cloned; + delete score; +} + +TEST_F(Engraving_OctaveDotTests, jianpuSaveReloadKeepsOctaveDotsOwnership) +{ + String tempFile(u"jianpu-octavedots-save-reload-test.mscx"); + + MasterScore* score = ScoreRW::readScore(JIANPU_OCTAVE_DOTS_DATA_DIR + u"jianpuBeam.mscx"); + ASSERT_TRUE(score); + + Note* beforeSave = findJianpuNoteInChord(score); + ASSERT_TRUE(beforeSave); + + const NoteLocator locator { beforeSave->chord()->tick(), beforeSave->track(), beforeSave->pitch() }; + + // Force octave dots to be allocated + beforeSave->resizeOctaveDotsTo(1); + expectOctaveDotsOwnedByNote(beforeSave); + + ASSERT_TRUE(ScoreRW::saveScore(score, tempFile)); + delete score; + + MasterScore* reloaded = ScoreRW::readScore(tempFile, true); + ASSERT_TRUE(reloaded); + + Note* afterReload = findNoteByLocator(reloaded, locator); + ASSERT_TRUE(afterReload); + expectOctaveDotsOwnedByNote(afterReload); + + delete reloaded; +} From c3a7e3149a33bdf1672904fe1a4e0167679b7673 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Thu, 27 Aug 2026 22:21:58 +1000 Subject: [PATCH 15/24] fix build error after merge from main --- src/engraving/dom/chordrest.cpp | 2 +- src/engraving/dom/durationline.cpp | 4 ++-- src/engraving/dom/note.cpp | 2 +- src/engraving/dom/octavedot.cpp | 2 +- src/engraving/rendering/score/chordlayout.cpp | 4 ++-- src/engraving/rendering/score/restlayout.cpp | 2 +- src/engraving/tests/durationline_tests.cpp | 4 ++-- src/engraving/tests/octavedot_tests.cpp | 2 +- src/engraving/types/types.h | 2 +- src/engraving/types/typesconv.cpp | 3 ++- 10 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/engraving/dom/chordrest.cpp b/src/engraving/dom/chordrest.cpp index abf4bd9e6d32..a43de55f1544 100644 --- a/src/engraving/dom/chordrest.cpp +++ b/src/engraving/dom/chordrest.cpp @@ -1393,7 +1393,7 @@ void ChordRest::resizeDurationLinesTo(size_t newSize) { while (m_durationLines.size() < newSize) { DurationLine* dl = new DurationLine(this); - dl->setParent(this); + dl->setOwnershipParent(this); dl->setTrack(track()); m_durationLines.push_back(dl); } diff --git a/src/engraving/dom/durationline.cpp b/src/engraving/dom/durationline.cpp index 2e811ba4d87d..d51c82cdbf15 100644 --- a/src/engraving/dom/durationline.cpp +++ b/src/engraving/dom/durationline.cpp @@ -82,7 +82,7 @@ void DurationLine::spatiumChanged(double oldValue, double newValue) double DurationLine::mag() const { - auto* parent = explicitParent(); + auto* parent = ownershipParent(); if (parent && parent->isRest()) { return toRest(parent)->mag(); } @@ -99,7 +99,7 @@ double DurationLine::mag() const ChordRest* DurationLine::chordRest() const { - auto* parent = explicitParent(); + auto* parent = ownershipParent(); if (parent && parent->isChordRest()) { return toChordRest(parent); } else { diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index e8afb92348cc..084d9268adfa 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -4275,7 +4275,7 @@ void Note::resizeOctaveDotsTo(size_t newSize) { while (m_octaveDots.size() < newSize) { OctaveDot* dot = new OctaveDot(this); - dot->setParent(this); + dot->setOwnershipParent(this); dot->setTrack(track()); m_octaveDots.push_back(dot); } diff --git a/src/engraving/dom/octavedot.cpp b/src/engraving/dom/octavedot.cpp index 9a40b0e7af5a..9835c7355264 100644 --- a/src/engraving/dom/octavedot.cpp +++ b/src/engraving/dom/octavedot.cpp @@ -59,7 +59,7 @@ void OctaveDot::spatiumChanged(double oldValue, double newValue) Note* OctaveDot::note() const { - auto* parent = explicitParent(); + auto* parent = ownershipParent(); if (parent && parent->isNote()) { return toNote(parent); } else { diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index e2cb6cac6613..336a30dcdf8e 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1742,7 +1742,7 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) double lengtheningY = -item->spatium() * .5; for (int i = 0; i < lines; ++i) { DurationLine* dl = item->durationLines()[i]; - dl->setParent(item); + dl->setOwnershipParent(item); dl->setTrack(track); dl->setVisible(staffVisible); dl->setLen(hw); @@ -1804,7 +1804,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) note->resizeOctaveDotsTo(dots); for (int i = 0; i < dots; ++i) { OctaveDot* dot = note->octaveDots()[i]; - dot->setParent(note); + dot->setOwnershipParent(note); dot->setTrack(track); dot->setVisible(staffVisible); dot->setAbove(octave > baseOctave); diff --git a/src/engraving/rendering/score/restlayout.cpp b/src/engraving/rendering/score/restlayout.cpp index 254582e8b4e6..3ec864053c94 100644 --- a/src/engraving/rendering/score/restlayout.cpp +++ b/src/engraving/rendering/score/restlayout.cpp @@ -216,7 +216,7 @@ void RestLayout::layoutRestForJianpu(const Rest* item, Rest::LayoutData* ldata, const_cast(item)->resizeDurationLinesTo(lines); for (int i = 0; i < lines; ++i) { DurationLine* dl = item->durationLines()[i]; - dl->setParent(const_cast(item)); + dl->setOwnershipParent(const_cast(item)); dl->setTrack(track); dl->setVisible(staffVisible); dl->setLen(width); diff --git a/src/engraving/tests/durationline_tests.cpp b/src/engraving/tests/durationline_tests.cpp index eef299a30809..fab589b7986b 100644 --- a/src/engraving/tests/durationline_tests.cpp +++ b/src/engraving/tests/durationline_tests.cpp @@ -90,7 +90,7 @@ void expectDurationLinesOwnedByChordRest(const ChordRest* chordRest) for (DurationLine* durationLine : chordRest->durationLines()) { ASSERT_NE(durationLine, nullptr); - EXPECT_EQ(durationLine->explicitParent(), chordRest); + EXPECT_EQ(durationLine->ownershipParent(), chordRest); EXPECT_EQ(durationLine->track(), chordRest->track()); } } @@ -138,7 +138,7 @@ TEST_F(Engraving_DurationLineTests, detachedDurationLineUsesLocalPosition) // EngravingObject requires a non-null constructor parent, so detach it afterwards. // Scoped so it is destroyed (and unlinked from chordRest) before the score is deleted. DurationLine durationLine(chordRest); - durationLine.resetExplicitParent(); + durationLine.setOwnershipParent(nullptr); durationLine.setPos(PointF(2.0, 3.0)); EXPECT_EQ(durationLine.pagePos(), PointF(2.0, 3.0)); diff --git a/src/engraving/tests/octavedot_tests.cpp b/src/engraving/tests/octavedot_tests.cpp index 49b03b8e8c55..5c86d0104639 100644 --- a/src/engraving/tests/octavedot_tests.cpp +++ b/src/engraving/tests/octavedot_tests.cpp @@ -94,7 +94,7 @@ void expectOctaveDotsOwnedByNote(const Note* note) for (OctaveDot* octaveDot : note->octaveDots()) { ASSERT_NE(octaveDot, nullptr); - EXPECT_EQ(octaveDot->explicitParent(), note); + EXPECT_EQ(octaveDot->ownershipParent(), note); EXPECT_EQ(octaveDot->track(), note->track()); } } diff --git a/src/engraving/types/types.h b/src/engraving/types/types.h index 87606a76687e..44973a64bae4 100644 --- a/src/engraving/types/types.h +++ b/src/engraving/types/types.h @@ -1228,7 +1228,7 @@ constexpr int STAFF_GROUP_MAX = int(StaffGroup::TAB) + 1; // out of enum to avoi // keep in sync with the _presets initialization in StaffType::initStaffTypes() and _defaultPreset for legacy scores enum class StaffTypes : signed char { - STANDARD, + STANDARD, JIANPU, PERC_1LINE, PERC_2LINE, PERC_3LINE, PERC_5LINE, TAB_6SIMPLE, TAB_6COMMON, TAB_6FULL, TAB_4SIMPLE, TAB_4COMMON, TAB_4FULL, diff --git a/src/engraving/types/typesconv.cpp b/src/engraving/types/typesconv.cpp index ca1bccb85d67..4985d8c709e8 100644 --- a/src/engraving/types/typesconv.cpp +++ b/src/engraving/types/typesconv.cpp @@ -3250,8 +3250,9 @@ StaffGroup TConv::fromXml(const AsciiStringView& tag, StaffGroup def) return findTypeByXmlTag(STAFFGROUP_TYPES, tag, def); } -static const std::array, 27> STAFFTYPES_ITEMS = { { +static const std::array, 28> STAFFTYPES_ITEMS = { { { StaffTypes::STANDARD, "stdNormal", muse::TranslatableString("engraving/stafftypes", "Standard") }, + { StaffTypes::JIANPU, "stdJianpu", muse::TranslatableString("engraving/stafftypes", "Jianpu") }, { StaffTypes::PERC_1LINE, "perc1Line", muse::TranslatableString("engraving/stafftypes", "Perc. 1 line") }, { StaffTypes::PERC_2LINE, "perc2Line", muse::TranslatableString("engraving/stafftypes", "Perc. 2 lines") }, { StaffTypes::PERC_3LINE, "perc3Line", muse::TranslatableString("engraving/stafftypes", "Perc. 3 lines") }, From 0f082d9571dfd5df3d8ac4660faa0b0f0c07ae67 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Tue, 20 Oct 2026 20:16:19 +1100 Subject: [PATCH 16/24] Fix build issue after merge from main --- src/engraving/dom/durationline.cpp | 4 ++-- src/engraving/dom/durationline.h | 2 +- src/engraving/dom/factory.cpp | 4 ++-- src/engraving/dom/octavedot.cpp | 4 ++-- src/engraving/dom/octavedot.h | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/engraving/dom/durationline.cpp b/src/engraving/dom/durationline.cpp index d51c82cdbf15..07912f0fd342 100644 --- a/src/engraving/dom/durationline.cpp +++ b/src/engraving/dom/durationline.cpp @@ -37,8 +37,8 @@ namespace mu::engraving { // DurationLine //--------------------------------------------------------- -DurationLine::DurationLine(EngravingItem* s) - : EngravingItem(ElementType::DURATION_LINE, s) +DurationLine::DurationLine(DummyParentOr parent) + : EngravingItem(ElementType::DURATION_LINE, parent) { setSelectable(false); } diff --git a/src/engraving/dom/durationline.h b/src/engraving/dom/durationline.h index 132d99f43f47..cb968da55b6e 100644 --- a/src/engraving/dom/durationline.h +++ b/src/engraving/dom/durationline.h @@ -41,7 +41,7 @@ class DurationLine final : public EngravingItem DECLARE_CLASSOF(ElementType::DURATION_LINE) public: - DurationLine(EngravingItem*); + DurationLine(DummyParentOr); ~DurationLine() override = default; DurationLine& operator=(const DurationLine&) = delete; diff --git a/src/engraving/dom/factory.cpp b/src/engraving/dom/factory.cpp index a35063e719b0..e558cb990c18 100644 --- a/src/engraving/dom/factory.cpp +++ b/src/engraving/dom/factory.cpp @@ -267,8 +267,8 @@ EngravingItem* Factory::doCreateItem(ElementType type, DummyParentOr(parentItem, dummy)); case ElementType::PARTIAL_LYRICSLINE: return new PartialLyricsLine(parent); case ElementType::PARENTHESIS: return new Parenthesis(parent); - case ElementType::DURATION_LINE: return new DurationLine(parent->isChordRest() ? toChordRest(parent) : dummy->chord()); - case ElementType::OCTAVE_DOT: return new OctaveDot(parent->isNote() ? toNote(parent) : dummy->note()); + case ElementType::DURATION_LINE: return new DurationLine(parentOfTypeOrDummy(parentItem, dummy)); + case ElementType::OCTAVE_DOT: return new OctaveDot(parentOfTypeOrDummy(parentItem, dummy)); case ElementType::TEXTLINE_BASE: case ElementType::TEXTLINE_SEGMENT: diff --git a/src/engraving/dom/octavedot.cpp b/src/engraving/dom/octavedot.cpp index 9835c7355264..f17d92d6f96a 100644 --- a/src/engraving/dom/octavedot.cpp +++ b/src/engraving/dom/octavedot.cpp @@ -36,8 +36,8 @@ namespace mu::engraving { // OctaveDot //--------------------------------------------------------- -OctaveDot::OctaveDot(EngravingItem* s) - : EngravingItem(ElementType::OCTAVE_DOT, s) +OctaveDot::OctaveDot(DummyParentOr parent) + : EngravingItem(ElementType::OCTAVE_DOT, parent) { setSelectable(false); } diff --git a/src/engraving/dom/octavedot.h b/src/engraving/dom/octavedot.h index 62cce75af4e2..eeb222320171 100644 --- a/src/engraving/dom/octavedot.h +++ b/src/engraving/dom/octavedot.h @@ -43,7 +43,7 @@ class OctaveDot final : public EngravingItem DECLARE_CLASSOF(ElementType::OCTAVE_DOT) public: - OctaveDot(EngravingItem*); + OctaveDot(DummyParentOr); ~OctaveDot() override = default; OctaveDot& operator=(const OctaveDot&) = delete; From ccbf5f57cf5489bc915ac53370e5e2f25ece23ef Mon Sep 17 00:00:00 2001 From: Song <8042214+songzhenke@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:13:49 +1100 Subject: [PATCH 17/24] Add tonic setting for jianpu (#11) * add tonic for jianpu * replace hardcode octave based note C3 by score key * add jianpu tonic setting to key signature properties instead of score wide style * show jianpu label 1=C to replace sharp and flat * default to major when staff mode is unknown or none * display jianpu accidetal instead of standard note accidental * fix coding format * rename setting name in properties * fix jianpu vertical display for multi voice * fix jianpu vertical display for muti voice - handle dot and beam * fix jianpu vertical display for muti voice - use up note * fix jianpu vertical display for muti voice - handle slur * fix jianpu octave dot for custom tonic * add new setting for voice vertical distance * qml use defined jianpu tonic mode in engraving --- src/engraving/dom/engravingobject.h | 1 + src/engraving/dom/key.cpp | 4 + src/engraving/dom/key.h | 12 + src/engraving/dom/keysig.cpp | 33 +- src/engraving/dom/keysig.h | 8 + src/engraving/dom/note.cpp | 1 + src/engraving/dom/note.h | 3 + src/engraving/dom/pitchspelling.cpp | 162 ++++++++++ src/engraving/dom/pitchspelling.h | 8 + src/engraving/dom/property.cpp | 3 + src/engraving/dom/property.h | 3 + src/engraving/dom/shadownote.h | 3 + src/engraving/rendering/score/chordlayout.cpp | 290 +++++++++++++++++- src/engraving/rendering/score/chordlayout.h | 2 + src/engraving/rendering/score/restlayout.cpp | 7 + .../rendering/score/slurtielayout.cpp | 5 + .../rendering/score/systemlayout.cpp | 9 + src/engraving/rendering/score/tdraw.cpp | 46 ++- src/engraving/rendering/score/tlayout.cpp | 57 +++- src/engraving/rw/read500/tread.cpp | 6 + src/engraving/rw/write/twrite.cpp | 6 + src/engraving/style/styledef.cpp | 1 + src/engraving/style/styledef.h | 1 + src/engraving/tests/keysig_tests.cpp | 168 ++++++++++ src/engraving/tests/note_tests.cpp | 88 ++++++ src/engraving/types/types.h | 6 + src/notationscene/widgets/editstyle.cpp | 1 + src/notationscene/widgets/editstyle.ui | 75 +++-- .../keysignatures/KeySignatureSettings.qml | 66 ++++ .../keysignaturesettingsmodel.cpp | 52 +++- .../keysignatures/keysignaturesettingsmodel.h | 17 + .../PropertiesPanel/types/keysignaturetypes.h | 7 + 32 files changed, 1101 insertions(+), 50 deletions(-) diff --git a/src/engraving/dom/engravingobject.h b/src/engraving/dom/engravingobject.h index ca8375673939..2838f8ea655c 100644 --- a/src/engraving/dom/engravingobject.h +++ b/src/engraving/dom/engravingobject.h @@ -511,6 +511,7 @@ class EngravingObject CONVERT(Tapping, TAPPING) CONVERT(TappingHalfSlur, TAPPING_HALF_SLUR) CONVERT(TappingHalfSlurSegment, TAPPING_HALF_SLUR_SEGMENT) + CONVERT(OctaveDot, OCTAVE_DOT) #undef CONVERT virtual bool isEngravingItem() const { return false; } // overridden in element.h diff --git a/src/engraving/dom/key.cpp b/src/engraving/dom/key.cpp index fd3f1026ea37..9b141df4a8fb 100644 --- a/src/engraving/dom/key.cpp +++ b/src/engraving/dom/key.cpp @@ -231,6 +231,10 @@ bool KeySigEvent::operator==(const KeySigEvent& e) const if (e.m_custom != m_custom || e.m_mode != m_mode || e.m_forInstrumentChange != m_forInstrumentChange) { return false; } + if (e.m_jianpuNumbering != m_jianpuNumbering || e.m_jianpuTonicKey != m_jianpuTonicKey + || e.m_jianpuTonicMode != m_jianpuTonicMode) { + return false; + } if (m_custom && !isAtonal()) { if (e.m_customKeyDefs.size() != m_customKeyDefs.size()) { return false; diff --git a/src/engraving/dom/key.h b/src/engraving/dom/key.h index b0a025339b16..47acba91b270 100644 --- a/src/engraving/dom/key.h +++ b/src/engraving/dom/key.h @@ -77,6 +77,14 @@ class KeySigEvent KeyMode mode() const { return m_mode; } void setMode(KeyMode m) { m_mode = m; } bool custom() const { return m_custom; } + + // Jianpu tonic numbering: per key-signature override of the digit/mode mapping + JianpuTonicMode jianpuNumbering() const { return m_jianpuNumbering; } + void setJianpuNumbering(JianpuTonicMode v) { m_jianpuNumbering = v; } + Key jianpuTonicKey() const { return m_jianpuTonicKey; } + void setJianpuTonicKey(Key v) { m_jianpuTonicKey = v; } + KeyMode jianpuTonicMode() const { return m_jianpuTonicMode; } + void setJianpuTonicMode(KeyMode v) { m_jianpuTonicMode = v; } bool isValid() const { return m_key != Key::INVALID; } bool isAtonal() const { return m_mode == KeyMode::NONE; } void setForInstrumentChange(bool forInstrumentChange) { m_forInstrumentChange = forInstrumentChange; } @@ -97,6 +105,10 @@ class KeySigEvent bool m_custom = false; bool m_forInstrumentChange = false; std::vector m_customKeyDefs; + + JianpuTonicMode m_jianpuNumbering = JianpuTonicMode::FOLLOW_SCORE_KEY; + Key m_jianpuTonicKey = Key::C; + KeyMode m_jianpuTonicMode = KeyMode::MAJOR; }; //--------------------------------------------------------- diff --git a/src/engraving/dom/keysig.cpp b/src/engraving/dom/keysig.cpp index 78f5fbfa349f..d15766da98f2 100644 --- a/src/engraving/dom/keysig.cpp +++ b/src/engraving/dom/keysig.cpp @@ -195,6 +195,9 @@ EngravingObject* KeySig::propertyDelegate(Pid propertyId) const case Pid::KEY_CONCERT: case Pid::SHOW_COURTESY: case Pid::KEYSIG_MODE: + case Pid::JIANPU_NUMBERING: + case Pid::JIANPU_TONIC_KEY: + case Pid::JIANPU_TONIC_MODE: { Segment* thisSeg = segment(); Segment* nextKSSeg = thisSeg ? thisSeg->next1(SegmentType::KeySig) : nullptr; @@ -228,6 +231,12 @@ PropertyValue KeySig::getProperty(Pid propertyId) const return showCourtesy(); case Pid::KEYSIG_MODE: return mode(); + case Pid::JIANPU_NUMBERING: + return int(jianpuNumbering()); + case Pid::JIANPU_TONIC_KEY: + return int(jianpuTonicKey()); + case Pid::JIANPU_TONIC_MODE: + return jianpuTonicMode(); case Pid::IS_COURTESY: return _isCourtesy; default: @@ -264,12 +273,24 @@ bool KeySig::setProperty(Pid propertyId, const PropertyValue& v) setShowCourtesy(v.toBool()); break; case Pid::KEYSIG_MODE: - if (generated()) { - return false; - } + // Editable even when generated (e.g. a natural key signature with no visible + // accidentals): the mode is stored in the staff's persistent key map, not + // in a drawn symbol, so there is nothing unsafe about setting it here. setMode(v.value()); staff()->setKey(tick(), keySigEvent()); break; + case Pid::JIANPU_NUMBERING: + setJianpuNumbering(JianpuTonicMode(v.toInt())); + staff()->setKey(tick(), keySigEvent()); + break; + case Pid::JIANPU_TONIC_KEY: + setJianpuTonicKey(Key(v.toInt())); + staff()->setKey(tick(), keySigEvent()); + break; + case Pid::JIANPU_TONIC_MODE: + setJianpuTonicMode(v.value()); + staff()->setKey(tick(), keySigEvent()); + break; case Pid::IS_COURTESY: _isCourtesy = v.toBool(); break; @@ -320,6 +341,12 @@ PropertyValue KeySig::propertyDefault(Pid id) const return true; case Pid::KEYSIG_MODE: return KeyMode::UNKNOWN; + case Pid::JIANPU_NUMBERING: + return int(JianpuTonicMode::FOLLOW_SCORE_KEY); + case Pid::JIANPU_TONIC_KEY: + return int(Key::C); + case Pid::JIANPU_TONIC_MODE: + return KeyMode::MAJOR; case Pid::IS_COURTESY: return false; default: diff --git a/src/engraving/dom/keysig.h b/src/engraving/dom/keysig.h index 71bbf4a02870..1388745b042e 100644 --- a/src/engraving/dom/keysig.h +++ b/src/engraving/dom/keysig.h @@ -71,6 +71,13 @@ class KeySig final : public EngravingItem KeyMode mode() const { return m_sig.mode(); } void setMode(KeyMode v) { m_sig.setMode(v); } + JianpuTonicMode jianpuNumbering() const { return m_sig.jianpuNumbering(); } + void setJianpuNumbering(JianpuTonicMode v) { m_sig.setJianpuNumbering(v); } + Key jianpuTonicKey() const { return m_sig.jianpuTonicKey(); } + void setJianpuTonicKey(Key v) { m_sig.setJianpuTonicKey(v); } + KeyMode jianpuTonicMode() const { return m_sig.jianpuTonicMode(); } + void setJianpuTonicMode(KeyMode v) { m_sig.setJianpuTonicMode(v); } + PointF staffOffset() const override; bool hideNaturals() const { return m_hideNaturals; } @@ -95,6 +102,7 @@ class KeySig final : public EngravingItem struct LayoutData : public EngravingItem::LayoutData { std::vector keySymbols; + String jianpuLabel; // Jianpu key label (e.g. "1=C"); drawn instead of keySymbols when non-empty }; DECLARE_LAYOUTDATA_METHODS(KeySig) diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index b720022b60f3..b8368de96fe6 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -799,6 +799,7 @@ Note::Note(const Note& n, bool link) } m_mark = n.m_mark; m_jianpuDigit = n.m_jianpuDigit; + m_jianpuAccidental = n.m_jianpuAccidental; setDropTarget(false); } diff --git a/src/engraving/dom/note.h b/src/engraving/dom/note.h index c4cac2957dcd..1fe82d38138b 100644 --- a/src/engraving/dom/note.h +++ b/src/engraving/dom/note.h @@ -270,6 +270,8 @@ class Note final : public EngravingItem String jianpuDigit() const { return m_jianpuDigit; } void setJianpuDigit(const String& s) { m_jianpuDigit = s; } + String jianpuAccidental() const { return m_jianpuAccidental; } + void setJianpuAccidental(const String& s) { m_jianpuAccidental = s; } void add(EngravingItem*) override; void remove(EngravingItem*) override; @@ -567,6 +569,7 @@ class Note final : public EngravingItem String m_fretString; String m_jianpuDigit; + String m_jianpuAccidental; std::vector m_octaveDots; std::vector m_lineAttachPoints; diff --git a/src/engraving/dom/pitchspelling.cpp b/src/engraving/dom/pitchspelling.cpp index afafdf6da55b..48d020110d34 100644 --- a/src/engraving/dom/pitchspelling.cpp +++ b/src/engraving/dom/pitchspelling.cpp @@ -452,6 +452,168 @@ int pitch2absStepByKey(int pitch, int tpc, Key key, int& alter) return octave * STEP_DELTA_OCTAVE + step; } +static int jianpuModeDegreeOffset(KeyMode mode) +{ + switch (mode) { + case KeyMode::DORIAN: return 1; + case KeyMode::PHRYGIAN: return 2; + case KeyMode::LYDIAN: return 3; + case KeyMode::MIXOLYDIAN: return 4; + case KeyMode::AEOLIAN: return 5; + case KeyMode::MINOR: return 5; + case KeyMode::LOCRIAN: return 6; + case KeyMode::UNKNOWN: + case KeyMode::NONE: + case KeyMode::MAJOR: + case KeyMode::IONIAN: + return 0; + } + return 0; +} + +static const int* jianpuModeIntervals(KeyMode mode) +{ + static const int ionian[] = { 0, 2, 4, 5, 7, 9, 11 }; + static const int dorian[] = { 0, 2, 3, 5, 7, 9, 10 }; + static const int phrygian[] = { 0, 1, 3, 5, 7, 8, 10 }; + static const int lydian[] = { 0, 2, 4, 6, 7, 9, 11 }; + static const int mixolydian[] = { 0, 2, 4, 5, 7, 9, 10 }; + static const int aeolian[] = { 0, 2, 3, 5, 7, 8, 10 }; + static const int locrian[] = { 0, 1, 3, 5, 6, 8, 10 }; + + switch (mode) { + case KeyMode::DORIAN: return dorian; + case KeyMode::PHRYGIAN: return phrygian; + case KeyMode::LYDIAN: return lydian; + case KeyMode::MIXOLYDIAN: return mixolydian; + case KeyMode::AEOLIAN: return aeolian; + case KeyMode::MINOR: return aeolian; + case KeyMode::LOCRIAN: return locrian; + case KeyMode::UNKNOWN: + case KeyMode::NONE: + case KeyMode::MAJOR: + case KeyMode::IONIAN: + return ionian; + } + return ionian; +} + +static int normalizeJianpuAlteration(int alteration) +{ + while (alteration > 6) { + alteration -= 12; + } + while (alteration < -6) { + alteration += 12; + } + return alteration; +} + +int jianpuTonicTpc(Key relativeMajorKey, KeyMode mode) +{ + static const int majorScaleSemitones[] = { 0, 2, 4, 5, 7, 9, 11 }; + + const int degreeOffset = jianpuModeDegreeOffset(mode); + const int relativeMajorTpc = key2Tpc(relativeMajorKey); + const int relativeMajorStep = tpc2step(relativeMajorTpc); + const int tonicStep = (relativeMajorStep + degreeOffset) % STEP_DELTA_OCTAVE; + const int relativeMajorPitch = (majorScaleSemitones[relativeMajorStep] + int(tpc2alter(relativeMajorTpc)) + 12) % 12; + const int tonicPitch = (relativeMajorPitch + majorScaleSemitones[degreeOffset]) % 12; + const int tonicAlteration = normalizeJianpuAlteration(tonicPitch - majorScaleSemitones[tonicStep]); + + return step2tpc(tonicStep, AccidentalVal(tonicAlteration)); +} + +void tpc2Jianpu(int tpc, int tonicTpc, KeyMode mode, String& accName, String& stepName) +{ + static const int majorScaleSemitones[] = { 0, 2, 4, 5, 7, 9, 11 }; + static const String stepNames = u"1234567"; + + tpc = clampEnharmonic(tpc); + tonicTpc = clampEnharmonic(tonicTpc); + const int tonicStep = tpc2step(tonicTpc); + const int noteStep = tpc2step(tpc); + const int scaleDegree = (noteStep - tonicStep + STEP_DELTA_OCTAVE) % STEP_DELTA_OCTAVE; + const int jianpuDegree = (jianpuModeDegreeOffset(mode) + scaleDegree) % STEP_DELTA_OCTAVE; + stepName = stepNames.at(jianpuDegree); + + const int* modeIntervals = jianpuModeIntervals(mode); + const int tonicPitch = (majorScaleSemitones[tonicStep] + int(tpc2alter(tonicTpc)) + 12) % 12; + const int expectedPitch = (tonicPitch + modeIntervals[scaleDegree]) % 12; + const int expectedAlteration = normalizeJianpuAlteration(expectedPitch - majorScaleSemitones[noteStep]); + const int alteration = int(tpc2alter(tpc)) - expectedAlteration; + + accName = String(); + const Char accidental = alteration < 0 ? Char(u'b') : Char(u'#'); + for (int i = 0; i < std::abs(alteration); ++i) { + accName.append(accidental); + } +} + +int pitch2JianpuOctave(int pitch, int tpc, int tonicTpc, KeyMode mode) +{ + tonicTpc = clampEnharmonic(tonicTpc); + tpc = clampEnharmonic(tpc); + const int noteOctave = (clampPitchOctaved(pitch) - int(tpc2alter(tpc))) / PITCH_DELTA_OCTAVE; + const int absStep = noteOctave * STEP_DELTA_OCTAVE + tpc2step(tpc); + const int tonicAbsStep = 4 * STEP_DELTA_OCTAVE + tpc2step(tonicTpc); + const int degreeOneOffset = (STEP_DELTA_OCTAVE - jianpuModeDegreeOffset(mode)) % STEP_DELTA_OCTAVE; + const int relativeStep = absStep - tonicAbsStep - degreeOneOffset; + return relativeStep >= 0 ? relativeStep / STEP_DELTA_OCTAVE + : (relativeStep - STEP_DELTA_OCTAVE + 1) / STEP_DELTA_OCTAVE; +} + +int pitch2JianpuOctave(int pitch, int tpc, Key key, KeyMode mode) +{ + return pitch2JianpuOctave(pitch, tpc, key2Tpc(key), mode); +} + +void jianpuKeyMapping(const KeySigEvent& ks, KeyMode& mode, int& tonicTpc) +{ + if (ks.jianpuNumbering() == JianpuTonicMode::CUSTOM) { + mode = ks.jianpuTonicMode(); + tonicTpc = key2Tpc(ks.jianpuTonicKey()); + } else { + mode = ks.mode(); + tonicTpc = jianpuTonicTpc(ks.key(), mode); + } +} + +//--------------------------------------------------------- +// jianpuAccidentalMark +// Converts the ASCII accidental spelling returned by tpc2Jianpu (e.g. +// "#", "bb") into the display form used next to a Jianpu digit. +//--------------------------------------------------------- + +String jianpuAccidentalMark(const String& accName) +{ + String mark = accName; + mark.replace(u"b", u"♭"); + mark.replace(u"#", u"♯"); + return mark; +} + +//--------------------------------------------------------- +// jianpuKeyLabel +// Computes the Jianpu key-signature label (e.g. "1=C", "6=A"). An unset +// (UNKNOWN) or atonal (NONE) mode in Follow-score-key numbering falls +// back to the same major mapping already used for note digits, since the +// key signature alone can't distinguish a major key from its relative minor. +//--------------------------------------------------------- + +String jianpuKeyLabel(const KeySigEvent& ks) +{ + KeyMode mode; + int tonicTpc; + jianpuKeyMapping(ks, mode, tonicTpc); + + const int degree = jianpuModeDegreeOffset(mode) + 1; + String tonicName = tpc2name(tonicTpc, NoteSpellingType::STANDARD, NoteCaseType::AUTO); + tonicName = jianpuAccidentalMark(tonicName); + + return String(u"%1=%2").arg(String::number(degree), tonicName); +} + //--------------------------------------------------------- // absStep2pitchByKey // the default pitch for the given absolute step in the given key diff --git a/src/engraving/dom/pitchspelling.h b/src/engraving/dom/pitchspelling.h index 7f26120bff61..581c89e18db7 100644 --- a/src/engraving/dom/pitchspelling.h +++ b/src/engraving/dom/pitchspelling.h @@ -24,6 +24,7 @@ #include "types/string.h" #include "../types/types.h" +#include "key.h" namespace mu::engraving { class MidiNote; @@ -90,8 +91,15 @@ extern int tpc2step(int tpc); extern int tpc2stepByKey(int tpc, Key, int& alter); extern int tpc2alterByKey(int tpc, Key); extern int pitch2absStepByKey(int pitch, int tpc, Key, int& alter); +extern int pitch2JianpuOctave(int pitch, int tpc, Key key, KeyMode mode); +extern int pitch2JianpuOctave(int pitch, int tpc, int tonicTpc, KeyMode mode); extern int absStep2pitchByKey(int step, Key); extern int tpc2degree(int tpc, Key key); +extern int jianpuTonicTpc(Key relativeMajorKey, KeyMode mode); +extern void tpc2Jianpu(int tpc, int tonicTpc, KeyMode mode, String& accName, String& stepName); +extern void jianpuKeyMapping(const KeySigEvent& ks, KeyMode& mode, int& tonicTpc); +extern String jianpuKeyLabel(const KeySigEvent& ks); +extern String jianpuAccidentalMark(const String& accName); extern int tpcInterval(int startTpc, int interval, int alter); extern int step2pitchInterval(int step, int alter); extern String tpc2Function(int tpc, Key key); diff --git a/src/engraving/dom/property.cpp b/src/engraving/dom/property.cpp index e4ebf9762bad..173983d8798e 100644 --- a/src/engraving/dom/property.cpp +++ b/src/engraving/dom/property.cpp @@ -69,6 +69,9 @@ static constexpr PropertyMetaData propertyList[] = { { Pid::SHOW_IF_ENTIRE_SYSTEM_EMPTY, P_TYPE::BOOL, PropertyGroup::APPEARANCE, false, "showIfEntireSystemEmpty", QT_TRANSLATE_NOOP("engraving/propertyName", "show if entire system empty") }, { Pid::SHOW_COURTESY, P_TYPE::BOOL, PropertyGroup::APPEARANCE, false, "showCourtesySig", QT_TRANSLATE_NOOP("engraving/propertyName", "show courtesy") }, { Pid::KEYSIG_MODE, P_TYPE::KEY_MODE, PropertyGroup::APPEARANCE, false, "keysig_mode", QT_TRANSLATE_NOOP("engraving/propertyName", "key signature mode") }, + { Pid::JIANPU_NUMBERING, P_TYPE::INT, PropertyGroup::APPEARANCE, false, "jianpuNumbering", QT_TRANSLATE_NOOP("engraving/propertyName", "Jianpu numbering") }, + { Pid::JIANPU_TONIC_KEY, P_TYPE::INT, PropertyGroup::APPEARANCE, false, "jianpuTonicKey", QT_TRANSLATE_NOOP("engraving/propertyName", "Jianpu tonal center") }, + { Pid::JIANPU_TONIC_MODE, P_TYPE::KEY_MODE, PropertyGroup::APPEARANCE, false, "jianpuTonicMode", QT_TRANSLATE_NOOP("engraving/propertyName", "Jianpu tonic mode") }, { Pid::SLUR_STYLE_TYPE, P_TYPE::SLUR_STYLE_TYPE, PropertyGroup::APPEARANCE, false, "lineType", QT_TRANSLATE_NOOP("engraving/propertyName", "line type") }, { Pid::PITCH, P_TYPE::INT, PropertyGroup::NONE, true, "pitch", QT_TRANSLATE_NOOP("engraving/propertyName", "pitch") }, { Pid::CENT_OFFSET, P_TYPE::REAL, PropertyGroup::NONE, true, "centOffset", QT_TRANSLATE_NOOP("engraving/propertyName", "cent offset") }, diff --git a/src/engraving/dom/property.h b/src/engraving/dom/property.h index c7ecf2445e71..e58e3adb3f74 100644 --- a/src/engraving/dom/property.h +++ b/src/engraving/dom/property.h @@ -81,6 +81,9 @@ enum class Pid : short { SHOW_IF_ENTIRE_SYSTEM_EMPTY, SHOW_COURTESY, KEYSIG_MODE, + JIANPU_NUMBERING, + JIANPU_TONIC_KEY, + JIANPU_TONIC_MODE, SLUR_STYLE_TYPE, PITCH, CENT_OFFSET, diff --git a/src/engraving/dom/shadownote.h b/src/engraving/dom/shadownote.h index 05d40c5e573f..987740c4c8e1 100644 --- a/src/engraving/dom/shadownote.h +++ b/src/engraving/dom/shadownote.h @@ -83,6 +83,8 @@ class ShadowNote final : public EngravingItem String jianpuDigit() const { return m_jianpuDigit; } void setJianpuDigit(const String& s) { m_jianpuDigit = s; } + String jianpuAccidental() const { return m_jianpuAccidental; } + void setJianpuAccidental(const String& s) { m_jianpuAccidental = s; } int jianpuDiminutionLines() const { return m_jianpuDiminutionLines; } void setJianpuDiminutionLines(int lines) { m_jianpuDiminutionLines = lines; } @@ -100,6 +102,7 @@ class ShadowNote final : public EngravingItem std::set m_articulationIds; bool m_beyondScore = false; String m_jianpuDigit; + String m_jianpuAccidental; int m_jianpuDiminutionLines = 0; int m_jianpuOctaveDots = 0; }; diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 382eb4c52e9b..8fb08dec94ac 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -20,6 +20,7 @@ * along with this program. If not, see . */ #include +#include #include "chordlayout.h" @@ -45,6 +46,7 @@ #include "dom/page.h" #include "dom/parenthesis.h" #include "dom/part.h" +#include "dom/pitchspelling.h" #include "dom/rest.h" #include "dom/score.h" #include "dom/segment.h" @@ -80,10 +82,10 @@ using namespace mu::engraving::rendering::score; static constexpr Spatium STAFFTYPE_TAB_DEFAULTDOTDIST_X = 0.75_sp; -static double jianpuBeamOffset(const Chord* chord, const LayoutContext& ctx) +static double jianpuBeamOffset(const Chord* chord, PlacementV placement, const LayoutContext& ctx) { const Beam* beam = chord->beam(); - if (!beam || ctx.conf().styleV(Sid::jianpuDiminutionBeamPlacement).value() != PlacementV::BELOW) { + if (!beam || ctx.conf().styleV(Sid::jianpuDiminutionBeamPlacement).value() != placement) { return 0.0; } @@ -190,7 +192,7 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) // Add extra offset for beam if it is upnote if (note == upnote) { - jianpuOffsetY += jianpuBeamOffset(item, ctx); + jianpuOffsetY += jianpuBeamOffset(item, PlacementV::BELOW, ctx); } jianpuOffsetY += ldata->bbox().height(); @@ -1735,7 +1737,7 @@ void ChordLayout::layoutDurationLines(Chord* item, LayoutContext& ctx) double hw = note->headWidth(); double hx = note->pos().x() + note->bboxXShift(); - double lengtheningY = -item->spatium() * .5; + double lengtheningY = note->pos().y(); for (int i = 0; i < lines; ++i) { DurationLine* dl = item->durationLines()[i]; dl->setOwnershipParent(item); @@ -1771,22 +1773,25 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) const StaffType* st = staff->staffTypeForElement(item); double height = st->jianpuBoxH() * item->magS(); - int baseOctave = 3; // Default base octave for Jianpu is C3 + KeySigEvent ks = staff->keySigEvent(tick); + KeyMode mode; + int tonicTpc; + jianpuKeyMapping(ks, mode, tonicTpc); for (Note* note : item->notes()) { int dots = 0; double offsetY = 0; double distance = ctx.conf().styleAbsolute(Sid::jianpuOctaveDotDistance) * item->magS(); - int octave = note->octave(); - if (octave > baseOctave) { - dots = octave - baseOctave; + int octave = pitch2JianpuOctave(note->epitch(), note->tpc(), tonicTpc, mode); + if (octave > 0) { + dots = octave; offsetY = -(height * .5 + dots * distance); - } else if (octave < baseOctave) { - dots = baseOctave - octave; + } else if (octave < 0) { + dots = -octave; offsetY = height * .5 + distance; if (note == item->upNote()) { // The octave dot should be under the jianpu beam - offsetY += jianpuBeamOffset(item, ctx); + offsetY += jianpuBeamOffset(item, PlacementV::BELOW, ctx); } } else { note->resizeOctaveDotsTo(0); @@ -1803,7 +1808,7 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) dot->setOwnershipParent(note); dot->setTrack(track); dot->setVisible(staffVisible); - dot->setAbove(octave > baseOctave); + dot->setAbove(octave > 0); dot->setLen(maxX - minX); dot->setPos(minX, offsetY + i * distance); } @@ -1814,6 +1819,262 @@ void ChordLayout::layoutOctaveDots(Chord* item, LayoutContext& ctx) } } +//--------------------------------------------------------- +// layoutVoiceOffsets +// Stacks each voice's jianpu number column into its own vertical band (voice 1 on top), +// since jianpu doesn't use stem direction to separate voices. No-op for other staff types. +// Scoped to the whole system so band height stays consistent across the line. +//--------------------------------------------------------- + +void ChordLayout::layoutVoiceOffsets(System* system, staff_idx_t staffIdx, LayoutContext& ctx) +{ + const Staff* staff = ctx.dom().staff(staffIdx); + if (!staff || !staff->show()) { + return; + } + const Measure* firstMeasure = system ? system->firstMeasure() : nullptr; + if (!firstMeasure || !staff->isJianpuStaff(firstMeasure->tick())) { + return; + } + + // the note's own position, ignoring octave dots/beams - this is what every chord in a + // voice must be aligned to, so dots on some notes but not others don't throw off the line + auto itemBaselineTop = [](EngravingItem* e) -> double { + if (e->isChord()) { + Note* upNote = toChord(e)->upNote(); + return upNote->ldata()->pos().y(); + } + return e->ldata()->pos().y(); + }; + // slurs are laid out after this pass, so estimate their reach (mirrors slurPos/computeShoulderHeight) + auto crSystemX = [system](const ChordRest* cr, bool isStart) -> double { + if (cr->measure()->system() != system) { + return isStart ? 0.0 : system->width(); + } + return cr->x() + cr->segment()->x() + cr->measure()->x(); + }; + auto includeSlurExtents = [&ctx, &crSystemX](Chord* chord, double& top, double& bottom) { + std::set slurs; + for (Spanner* spanner : chord->startingSpanners()) { + if (spanner->isSlur()) { + slurs.insert(toSlur(spanner)); + } + } + for (Spanner* spanner : chord->endingSpanners()) { + if (spanner->isSlur()) { + slurs.insert(toSlur(spanner)); + } + } + + const double sp = chord->spatium(); + for (Slur* slur : slurs) { + if (!slur->startCR() || !slur->endCR() || !slur->addToSkyline()) { + continue; + } + SlurTieLayout::computeUp(slur, ctx); + const double length = std::max(crSystemX(slur->endCR(), false) - crSystemX(slur->startCR(), true), 0.0); + const double arc = 0.75 * std::sqrt(length / sp / 4.0) * sp; + const double reach = 0.9 * sp * chord->intrinsicMag() + arc + ctx.conf().styleAbsolute(Sid::slurMidWidth); + if (slur->up()) { + top -= reach; + } else { + bottom += reach; + } + } + }; + auto itemExtentTop = [&ctx, &includeSlurExtents](EngravingItem* e) -> double { + if (e->isChord()) { + Chord* chord = toChord(e); + double top = DBL_MAX; + double bottom = -DBL_MAX; + for (Note* note : chord->notes()) { + const double y = note->ldata()->pos().y(); + top = std::min(top, y + note->ldata()->bbox().top()); + for (OctaveDot* dot : note->octaveDots()) { + if (dot->above()) { + top = std::min(top, y + dot->ldata()->pos().y() + dot->ldata()->bbox().top()); + } + } + } + includeSlurExtents(chord, top, bottom); + top -= jianpuBeamOffset(chord, PlacementV::ABOVE, ctx); + return top; + } + return e->ldata()->pos().y() + e->ldata()->bbox().top(); + }; + auto itemExtentBottom = [&ctx, &includeSlurExtents](EngravingItem* e) -> double { + if (e->isChord()) { + Chord* chord = toChord(e); + double bottom = -DBL_MAX; + double top = DBL_MAX; + bool upNoteHasBelowDot = false; + for (Note* note : chord->notes()) { + const double y = note->ldata()->pos().y(); + bottom = std::max(bottom, y + note->ldata()->bbox().bottom()); + for (OctaveDot* dot : note->octaveDots()) { + if (!dot->above()) { + bottom = std::max(bottom, y + dot->ldata()->pos().y() + dot->ldata()->bbox().bottom()); + if (note == chord->upNote()) { + upNoteHasBelowDot = true; + } + } + } + } + includeSlurExtents(chord, top, bottom); + // a diminution beam placed below the chord needs extra clearance beneath it, unless + // the upnote's below-dot is already positioned under the beam (see layoutOctaveDots) + if (!upNoteHasBelowDot) { + bottom += jianpuBeamOffset(chord, PlacementV::BELOW, ctx); + } + return bottom; + } + return e->ldata()->pos().y() + e->ldata()->bbox().bottom(); + }; + std::set shiftedSlurSegments; + auto shiftChordSlurs = [&system, &shiftedSlurSegments](Chord* chord, double dy) { + for (const std::set& spanners : { chord->startingSpanners(), chord->endingSpanners() }) { + for (Spanner* spanner : spanners) { + if (!spanner->isSlur()) { + continue; + } + for (SpannerSegment* segment : toSlur(spanner)->spannerSegments()) { + if (segment->system() == system && shiftedSlurSegments.insert(segment).second) { + segment->move(PointF(0.0, dy)); + } + } + } + } + }; + auto shiftItem = [&shiftChordSlurs](EngravingItem* e, double dy) { + if (e->isChord()) { + Chord* chord = toChord(e); + for (Note* note : chord->notes()) { + note->mutldata()->moveY(dy); + } + shiftChordSlurs(chord, dy); + for (Chord* grace : chord->graceNotes()) { + for (Note* note : grace->notes()) { + note->mutldata()->moveY(dy); + } + shiftChordSlurs(grace, dy); + // chord bbox is derived from its shape; slurPos() reads it later + fillShape(grace, grace->mutldata()); + } + fillShape(chord, chord->mutldata()); + // duration lines are positioned relative to the chord, not the note, so they + // don't follow the note shift above and need to be moved explicitly + for (DurationLine* dl : chord->durationLines()) { + dl->mutldata()->moveY(dy); + } + } else if (e->isRest()) { + Rest* rest = toRest(e); + rest->mutldata()->moveY(dy); + for (DurationLine* dl : rest->durationLines()) { + dl->mutldata()->moveY(dy); + } + } + }; + + // chord-rests belonging to a given voice, grouped by the segment they occur in + std::map > > groupsByVoice; + // how far each voice's content can extend above/below its own baseline (octave dots, beams) + std::map maxAbove; + std::map maxBelow; + + for (MeasureBase* mb : system->measures()) { + if (!mb->isMeasure()) { + continue; + } + for (Segment& segment : toMeasure(mb)->segments()) { + if (!segment.isChordRestType()) { + continue; + } + + std::map > itemsByVoiceHere; + for (EngravingItem* e : segment.elist()) { + if (e && e->isChordRest() && e->vStaffIdx() == staffIdx) { + itemsByVoiceHere[e->voice()].push_back(e); + } + } + + for (auto& pair : itemsByVoiceHere) { + double baseline = DBL_MAX; + double extentTop = DBL_MAX; + double extentBottom = -DBL_MAX; + for (EngravingItem* e : pair.second) { + baseline = std::min(baseline, itemBaselineTop(e)); + extentTop = std::min(extentTop, itemExtentTop(e)); + extentBottom = std::max(extentBottom, itemExtentBottom(e)); + } + if (baseline == DBL_MAX) { + continue; + } + maxAbove[pair.first] = std::max(maxAbove[pair.first], baseline - extentTop); + maxBelow[pair.first] = std::max(maxBelow[pair.first], extentBottom - baseline); + groupsByVoice[pair.first].push_back(std::move(pair.second)); + } + } + } + + if (groupsByVoice.size() < 2) { + return; + } + + // anchor absolute band positions to the topmost voice's natural (unshifted) baseline + double referenceTop = DBL_MAX; + for (EngravingItem* e : groupsByVoice.begin()->second.front()) { + referenceTop = std::min(referenceTop, itemBaselineTop(e)); + } + if (referenceTop == DBL_MAX) { + return; + } + + const double voiceDist = ctx.conf().styleAbsolute(Sid::jianpuVoiceVerticalDistance); + + // each voice's baseline sits far enough below the previous one to clear both the previous + // voice's downward extent (dots/beam below) and this voice's own upward extent (dots above) + std::map bandTargetTop; + double nextBaseline = referenceTop; + double prevBelow = 0.0; + bool firstVoice = true; + for (auto& pair : maxAbove) { + const voice_idx_t voice = pair.first; + if (!firstVoice) { + nextBaseline += prevBelow + voiceDist + pair.second; + } + bandTargetTop[voice] = nextBaseline; + prevBelow = maxBelow.at(voice); + firstVoice = false; + } + + std::set shiftedSegments; + for (auto& pair : groupsByVoice) { + const double targetTop = bandTargetTop.at(pair.first); + for (const std::vector& segmentItems : pair.second) { + double top = DBL_MAX; + for (EngravingItem* e : segmentItems) { + top = std::min(top, itemBaselineTop(e)); + } + if (top == DBL_MAX) { + continue; + } + const double dy = targetTop - top; + if (muse::RealIsNull(dy)) { + continue; + } + for (EngravingItem* e : segmentItems) { + shiftItem(e, dy); + shiftedSegments.insert(toChordRest(e)->segment()); + } + } + } + + // slur collision avoidance and skylines read the segment staff shape + for (Segment* segment : shiftedSegments) { + segment->createShape(staffIdx); + } +} + //--------------------------------------------------------- // layoutSegmentElements //--------------------------------------------------------- @@ -2409,6 +2670,7 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ const Staff* staff = ctx.dom().staff(staffIdx); const bool isTab = staff->isTabStaff(segment->tick()); + const bool isJianpu = staff->isJianpuStaff(segment->tick()); const track_idx_t startTrack = staffIdx * VOICES; const track_idx_t endTrack = startTrack + VOICES; const Fraction tick = segment->tick(); @@ -2421,7 +2683,9 @@ void ChordLayout::layoutChords1(LayoutContext& ctx, Segment* segment, staff_idx_ const track_idx_t partStartTrack = partTrackRangeOrDefault.startTrack; const track_idx_t partEndTrack = partTrackRangeOrDefault.endTrack; - if (isTab) { + if (isTab || isJianpu) { + // Jianpu notes show their accidentals as part of the digit label instead + // of the standard Accidental glyph, so skip drawing the latter. skipAccidentals(segment, startTrack, endTrack); } diff --git a/src/engraving/rendering/score/chordlayout.h b/src/engraving/rendering/score/chordlayout.h index c15e16d973b0..af707cda66ce 100644 --- a/src/engraving/rendering/score/chordlayout.h +++ b/src/engraving/rendering/score/chordlayout.h @@ -40,6 +40,7 @@ class Score; class Segment; class Staff; class Slur; +class System; } namespace mu::engraving::rendering::score { @@ -115,6 +116,7 @@ class ChordLayout static void layoutDurationLines(Chord* item, LayoutContext& ctx); static void layoutOctaveDots(Chord* item, LayoutContext& ctx); + static void layoutVoiceOffsets(System* system, staff_idx_t staffIdx, LayoutContext& ctx); static void crossMeasureSetup(Chord* chord, bool on, LayoutContext& ctx); diff --git a/src/engraving/rendering/score/restlayout.cpp b/src/engraving/rendering/score/restlayout.cpp index d1fee8dd09f9..a97edddb7bcc 100644 --- a/src/engraving/rendering/score/restlayout.cpp +++ b/src/engraving/rendering/score/restlayout.cpp @@ -230,6 +230,13 @@ void RestLayout::layoutRestForJianpu(const Rest* item, Rest::LayoutData* ldata, void RestLayout::resolveVerticalRestConflicts(LayoutContext& ctx, Segment* segment, staff_idx_t staffIdx) { + const Staff* staffForIdx = ctx.dom().staff(staffIdx); + if (staffForIdx && staffForIdx->isJianpuStaff(segment->tick())) { + // jianpu rests are stacked into per-voice bands (see ChordLayout::layoutVoiceOffsets) + // instead of using standard notation's notehead/rest shape collision avoidance + return; + } + std::vector rests; std::vector chords; diff --git a/src/engraving/rendering/score/slurtielayout.cpp b/src/engraving/rendering/score/slurtielayout.cpp index 7c217c1df159..25fee38d1172 100644 --- a/src/engraving/rendering/score/slurtielayout.cpp +++ b/src/engraving/rendering/score/slurtielayout.cpp @@ -1335,6 +1335,11 @@ Shape SlurTieLayout::getSegmentShape(SlurSegment* slurSeg, Segment* seg, ChordRe return true; } + // Ignore octave dots + if (item->isOctaveDot()) { + return true; + } + return false; }); diff --git a/src/engraving/rendering/score/systemlayout.cpp b/src/engraving/rendering/score/systemlayout.cpp index 01dbd09beba7..dd899f14388f 100644 --- a/src/engraving/rendering/score/systemlayout.cpp +++ b/src/engraving/rendering/score/systemlayout.cpp @@ -241,6 +241,10 @@ System* SystemLayout::collectSystem(LayoutContext& ctx) system->removeLastMeasure(); ctx.mutState().curMeasure()->setSystem(oldSystem); } + // re-align voice offsets for the evicted measures' real system, in case it never gets rebuilt in this pass + for (staff_idx_t staffIdx = 0; staffIdx < ctx.dom().nstaves(); ++staffIdx) { + ChordLayout::layoutVoiceOffsets(oldSystem, staffIdx, ctx); + } if (sharedTrackMapChanged) { // The last added measure caused sharedTrackMap to change. Now we are removing that measure so we must restore it. @@ -469,6 +473,11 @@ System* SystemLayout::collectSystem(LayoutContext& ctx) } } + // re-align voice offsets now that the system's final measure set is known + for (staff_idx_t staffIdx = 0; staffIdx < ctx.dom().nstaves(); ++staffIdx) { + ChordLayout::layoutVoiceOffsets(system, staffIdx, ctx); + } + layoutSystemElements(system, ctx); SystemLayout::layout2(system, ctx); // compute staff distances diff --git a/src/engraving/rendering/score/tdraw.cpp b/src/engraving/rendering/score/tdraw.cpp index 4d99bbc87399..e99427a6f111 100644 --- a/src/engraving/rendering/score/tdraw.cpp +++ b/src/engraving/rendering/score/tdraw.cpp @@ -2137,6 +2137,19 @@ void TDraw::draw(const KeySig* item, Painter* painter, const PaintOptions& opt) const KeySig::LayoutData* ldata = item->ldata(); painter->setPen(item->curColor(opt)); + + if (item->staff() && item->staff()->isJianpuStaff(item->tick())) { + if (ldata->jianpuLabel.isEmpty()) { + return; + } + const StaffType* staffType = item->staff()->staffTypeForElement(item); + Font f(staffType->jianpuFont()); + f.setPointSizeF(f.pointSizeF() * item->mag()); + painter->setFont(f); + painter->drawText(PointF(ldata->bbox().x(), ldata->bbox().bottom()), ldata->jianpuLabel); + return; + } + double _spatium = item->spatium(); double step = _spatium * (item->staff() ? item->staff()->staffTypeForElement(item)->lineDistance().val() * 0.5 : 0.5); int lines = item->staff() ? item->staff()->staffTypeForElement(item)->lines() : 5; @@ -2354,6 +2367,33 @@ void TDraw::draw(const MMRestRange* item, Painter* painter, const PaintOptions& drawTextBase(item, painter, opt); } +//--------------------------------------------------------- +// drawJianpuAccidental +// Draws a Jianpu accidental mark smaller and above-left of the digit, +// without shifting the digit itself (which stays aligned with beams/dots). +//--------------------------------------------------------- + +static void drawJianpuAccidental(Painter* painter, const Font& digitFont, const FontMetrics& digitFm, const String& accidental, + double digitStartX, double baselineY) +{ + if (accidental.isEmpty()) { + return; + } + + static constexpr double JIANPU_ACCIDENTAL_MAG = 0.7; + Font accFont(digitFont); + accFont.setPointSizeF(digitFont.pointSizeF() * JIANPU_ACCIDENTAL_MAG); + painter->setFont(accFont); + + FontMetrics accFm(accFont); + const double gap = accFont.pointSizeF() * 0.1; + const double accX = digitStartX - accFm.width(accidental) - gap; + const double accY = baselineY - digitFm.ascent() * 0.5; + painter->drawText(PointF(accX, accY), accidental); + + painter->setFont(digitFont); +} + void TDraw::draw(const Note* item, Painter* painter, const PaintOptions& opt) { TRACE_DRAW_ITEM; @@ -2407,6 +2447,7 @@ void TDraw::draw(const Note* item, Painter* painter, const PaintOptions& opt) const double bw = ldata->bbox().width(); const double startPosX = ldata->bbox().x() + (bw - fw) * .5; const double startPosY = ldata->bbox().bottom(); + drawJianpuAccidental(painter, f, fm, item->jianpuAccidental(), startPosX, startPosY); painter->drawText(PointF(startPosX, startPosY), item->jianpuDigit()); } else { // skip drawing, if second note of a cross-measure value @@ -2674,9 +2715,9 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) bool jianpu = item->staff() && item->staff()->isJianpuStaff(item->tick()); bool up = item->computeUp(); - // Draw the accidental + // Draw the accidental (Jianpu shows its accidental as part of the digit label instead) SymId acc = Accidental::subtype2symbol(item->accidentalType()); - if (acc != SymId::noSym) { + if (!jianpu && acc != SymId::noSym) { PointF posAcc; posAcc.rx() -= item->symWidth(acc) + item->style().styleAbsolute(Sid::accidentalNoteDistance) * item->mag(); item->drawSymbol(acc, painter, posAcc); @@ -2830,6 +2871,7 @@ void TDraw::draw(const ShadowNote* item, Painter* painter, const PaintOptions&) // Draw jianpu digit jianpuY += toNoteDistance; // Spacing for beams or dots to the notehead jianpuY += jianpuHeight; // Spacing from jianpu digit + drawJianpuAccidental(painter, f, fm, item->jianpuAccidental(), jianpuX, jianpuY); painter->drawText(jianpuX, jianpuY, item->jianpuDigit()); // Draw beams below the jianpu digit diff --git a/src/engraving/rendering/score/tlayout.cpp b/src/engraving/rendering/score/tlayout.cpp index dce8804863da..c7950402b0b0 100644 --- a/src/engraving/rendering/score/tlayout.cpp +++ b/src/engraving/rendering/score/tlayout.cpp @@ -536,8 +536,9 @@ void TLayout::layoutAccidental(const Accidental* item, Accidental::LayoutData* l ldata->syms.clear(); // TODO: remove Accidental in layout - // don't show accidentals for tab or slash notation - if (item->onTabStaff() || (item->note() && item->note()->fixed())) { + // don't show accidentals for tab or slash notation; Jianpu shows its + // accidental as part of the digit label instead of this glyph + if (item->onTabStaff() || item->isJianpuStaff() || (item->note() && item->note()->fixed())) { ldata->setIsSkipDraw(true); return; } @@ -3515,6 +3516,7 @@ void TLayout::layoutKeySig(const KeySig* item, KeySig::LayoutData* ldata, const ldata->setBbox(RectF()); ldata->keySymbols.clear(); + ldata->jianpuLabel = String(); const Staff* staff = item->staff(); const StaffType* stVisibility = staff ? staff->staffType(item->tick()) : nullptr; @@ -3523,9 +3525,26 @@ void TLayout::layoutKeySig(const KeySig* item, KeySig::LayoutData* ldata, const } const StaffType* st = item->staffType(); + double spatium = item->spatium(); + + if (staff && staff->isJianpuStaff(item->tick())) { + // Jianpu staves show a numbering label (e.g. "1=C") in place of the standard ♭/♯ symbols + ldata->jianpuLabel = jianpuKeyLabel(item->keySigEvent()); + + const double height = (st ? st->jianpuBoxH() : spatium) * item->mag(); + double width = 0.0; + if (!ldata->jianpuLabel.isEmpty()) { + Font font(st ? st->jianpuFont() : Font()); + font.setPointSizeF(font.pointSizeF() * item->mag()); + width = FontMetrics(font).width(ldata->jianpuLabel); + } + ldata->setPosY(item->staffOffsetY()); + ldata->setBbox(0.0, -height * 0.5, width, height); + return; + } + const Segment* s = item->segment(); track_idx_t track = item->track(); - double spatium = item->spatium(); double step = spatium * (st ? st->lineDistance().val() * 0.5 : 0.5); // determine current clef for this staff @@ -4194,9 +4213,18 @@ void TLayout::layoutNote(const Note* item, Note::LayoutData* ldata) const StaffType* jianpu = st->staffTypeForElement(item); KeySigEvent ks = st->keySigEvent(item->chord()->tick()); + KeyMode mode; + int tonicTpc; + jianpuKeyMapping(ks, mode, tonicTpc); String accName, stepName; - tpc2Function(item->tpc(), ks.key(), accName, stepName); - const_cast(item)->setJianpuDigit(String(u"%1").arg(stepName)); + tpc2Jianpu(item->tpc(), tonicTpc, mode, accName, stepName); + Tie* tieBack = item->tieBackNonPartial(); + if (tieBack && tieBack->startNote()->tpc() == item->tpc()) { + // tied continuation: accidental was already shown on the tied-from note + accName = String(); + } + const_cast(item)->setJianpuDigit(stepName); + const_cast(item)->setJianpuAccidental(jianpuAccidentalMark(accName)); double width = item->headWidth(); double height = jianpu->jianpuBoxH() * item->magS(); @@ -4805,9 +4833,9 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) } } - // Layout accidental + // Layout accidental (Jianpu shows its accidental as part of the digit label instead) SymId acc = Accidental::subtype2symbol(item->accidentalType()); - if (acc != SymId::noSym) { + if (!isJianpu && acc != SymId::noSym) { RectF symRect = item->symBbox(acc); double accWidth = symRect.width() + ctx.conf().styleAbsolute(Sid::accidentalNoteDistance) * mag; double dh = 0.0; @@ -4834,8 +4862,10 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) // jianpu digit if (item->isRest()) { item->setJianpuDigit(String(u"0")); + item->setJianpuAccidental(String()); } else { item->setJianpuDigit(String()); + item->setJianpuAccidental(String()); Score* score = item->score(); Position pos; bool error = !(score && score->getPosition(&pos, item->pos(), item->track()) && pos.segment); @@ -4850,15 +4880,16 @@ void TLayout::layoutShadowNote(ShadowNote* item, LayoutContext& ctx) if (tpc != Tpc::TPC_INVALID) { String accName, stepName; KeySigEvent ks = staff->keySigEvent(item->tick()); - tpc2Function(tpc, ks.key(), accName, stepName); - item->setJianpuDigit(String(u"%1").arg(stepName)); + KeyMode mode; + int tonicTpc; + jianpuKeyMapping(ks, mode, tonicTpc); + tpc2Jianpu(tpc, tonicTpc, mode, accName, stepName); + item->setJianpuDigit(stepName); + item->setJianpuAccidental(jianpuAccidentalMark(accName)); Interval transpose = item->part()->instrument(item->tick())->transpose(); - int alteration = static_cast(tpc2alter(tpc)); int epitch = nval.pitch - transpose.chromatic; - int octave = (epitch - alteration) / 12 - 1; // See Note::octave - int baseOctave = 3; // Default base octave for Jianpu is C3 - dots = baseOctave - octave; + dots = -pitch2JianpuOctave(epitch, tpc, tonicTpc, mode); } } } diff --git a/src/engraving/rw/read500/tread.cpp b/src/engraving/rw/read500/tread.cpp index a4b300ae7953..2807f5c4c6a0 100644 --- a/src/engraving/rw/read500/tread.cpp +++ b/src/engraving/rw/read500/tread.cpp @@ -1377,6 +1377,12 @@ void TRead::read(KeySig* s, XmlReader& e, ReadContext& ctx) sig.setCustom(true); } else if (tag == "mode") { sig.setMode(TConv::fromXml(e.readAsciiText(), KeyMode::UNKNOWN)); + } else if (tag == "jianpuNumbering") { + sig.setJianpuNumbering(JianpuTonicMode(e.readInt())); + } else if (tag == "jianpuTonicKey") { + sig.setJianpuTonicKey(Key(e.readInt())); + } else if (tag == "jianpuTonicMode") { + sig.setJianpuTonicMode(TConv::fromXml(e.readAsciiText(), KeyMode::MAJOR)); } else if (tag == "subtype") { subtype = e.readInt(); } else if (tag == "forInstrumentChange") { diff --git a/src/engraving/rw/write/twrite.cpp b/src/engraving/rw/write/twrite.cpp index 1c5d617b6b29..e440c551f7c7 100644 --- a/src/engraving/rw/write/twrite.cpp +++ b/src/engraving/rw/write/twrite.cpp @@ -2307,6 +2307,12 @@ void TWrite::write(const KeySig* item, XmlWriter& xml, WriteContext& ctx) xml.tag("mode", TConv::toXml(item->mode())); } + if (item->jianpuNumbering() != JianpuTonicMode::FOLLOW_SCORE_KEY) { + xml.tag("jianpuNumbering", int(item->jianpuNumbering())); + xml.tag("jianpuTonicKey", int(item->jianpuTonicKey())); + xml.tag("jianpuTonicMode", TConv::toXml(item->jianpuTonicMode())); + } + writeProperty(item, xml, Pid::SHOW_COURTESY); writeProperty(item, xml, Pid::IS_COURTESY); diff --git a/src/engraving/style/styledef.cpp b/src/engraving/style/styledef.cpp index cd1ee9190102..7fbb1a3b864f 100644 --- a/src/engraving/style/styledef.cpp +++ b/src/engraving/style/styledef.cpp @@ -2275,6 +2275,7 @@ const std::array StyleDef::styleValue styleDef(palmMuteEndFilledArrowWidth, 0.85_sp), styleDef(jianpuNumberVerticalDistance, 0.5_sp), + styleDef(jianpuVoiceVerticalDistance, 0.8_sp), styleDef(jianpuOctaveDotRadius, 0.1_sp), styleDef(jianpuOctaveDotDistance, 0.3_sp), styleDef(jianpuDiminutionBeamThickness, 0.1_sp), diff --git a/src/engraving/style/styledef.h b/src/engraving/style/styledef.h index c23f98b0a51a..8fb759741686 100644 --- a/src/engraving/style/styledef.h +++ b/src/engraving/style/styledef.h @@ -2293,6 +2293,7 @@ enum class Sid : short { palmMuteEndFilledArrowWidth, jianpuNumberVerticalDistance, + jianpuVoiceVerticalDistance, jianpuOctaveDotRadius, jianpuOctaveDotDistance, jianpuDiminutionBeamThickness, diff --git a/src/engraving/tests/keysig_tests.cpp b/src/engraving/tests/keysig_tests.cpp index a80d1c015570..f2ac12746eb3 100644 --- a/src/engraving/tests/keysig_tests.cpp +++ b/src/engraving/tests/keysig_tests.cpp @@ -27,6 +27,7 @@ #include "engraving/dom/masterscore.h" #include "engraving/dom/measure.h" #include "engraving/dom/part.h" +#include "engraving/dom/pitchspelling.h" #include "engraving/dom/segment.h" #include "engraving/dom/staff.h" #include "engraving/editing/editkeysig.h" @@ -267,3 +268,170 @@ TEST_F(Engraving_KeySigTests, linkedPrecedingKeyDeletion) } } } + +//--------------------------------------------------------- +// KeySigEvent equality must take the per-keysignature Jianpu tonic +// override fields into account, since they are independent of key/mode/custom. +//--------------------------------------------------------- +TEST_F(Engraving_KeySigTests, jianpuKeySigEventEquality) +{ + KeySigEvent a; + a.setConcertKey(Key::G); + KeySigEvent b = a; + EXPECT_TRUE(a == b); + + b.setJianpuNumbering(JianpuTonicMode::CUSTOM); + EXPECT_FALSE(a == b); + EXPECT_TRUE(a != b); + + KeySigEvent c = a; + c.setJianpuTonicKey(Key::D); + EXPECT_FALSE(a == c); + + KeySigEvent d = a; + d.setJianpuTonicMode(KeyMode::DORIAN); + EXPECT_FALSE(a == d); +} + +//--------------------------------------------------------- +// jianpuKeyMapping should mirror the staff's key/mode when FOLLOW_SCORE_KEY +// is selected, and use the explicit override fields when CUSTOM is selected. +//--------------------------------------------------------- +TEST_F(Engraving_KeySigTests, jianpuKeyMapping) +{ + KeySigEvent ks; + ks.setConcertKey(Key::D); + ks.setMode(KeyMode::DORIAN); + + KeyMode mode; + int tonicTpc; + + jianpuKeyMapping(ks, mode, tonicTpc); + EXPECT_EQ(mode, KeyMode::DORIAN); + EXPECT_EQ(tonicTpc, jianpuTonicTpc(Key::D, KeyMode::DORIAN)); + + ks.setJianpuNumbering(JianpuTonicMode::CUSTOM); + ks.setJianpuTonicKey(Key::A_B); + ks.setJianpuTonicMode(KeyMode::LYDIAN); + + jianpuKeyMapping(ks, mode, tonicTpc); + EXPECT_EQ(mode, KeyMode::LYDIAN); + EXPECT_EQ(tonicTpc, key2Tpc(Key::A_B)); +} + +//--------------------------------------------------------- +// jianpuKeyLabel should produce the "=" label used for the +// Jianpu key signature, falling back to the major mapping when the mode +// is UNKNOWN or NONE (mode is never guessed from the music). +//--------------------------------------------------------- +TEST_F(Engraving_KeySigTests, jianpuKeyLabel) +{ + // Follow mode, unmarked major/minor (UNKNOWN): falls back to major + KeySigEvent unknown; + unknown.setConcertKey(Key::C); + EXPECT_EQ(jianpuKeyLabel(unknown), u"1=C"); + + // Follow mode, unmarked, one sharp (UNKNOWN): falls back to G major + KeySigEvent oneSharp; + oneSharp.setConcertKey(Key::G); + EXPECT_EQ(jianpuKeyLabel(oneSharp), u"1=G"); + + // Follow mode, atonal (NONE): falls back to major (fixed at C) + KeySigEvent atonal; + atonal.setConcertKey(Key::C); + atonal.setMode(KeyMode::NONE); + EXPECT_EQ(jianpuKeyLabel(atonal), u"1=C"); + + // Follow mode, C major + KeySigEvent cMajor; + cMajor.setConcertKey(Key::C); + cMajor.setMode(KeyMode::MAJOR); + EXPECT_EQ(jianpuKeyLabel(cMajor), u"1=C"); + + // Follow mode, A minor (same key signature as C major) + KeySigEvent aMinor; + aMinor.setConcertKey(Key::C); + aMinor.setMode(KeyMode::MINOR); + EXPECT_EQ(jianpuKeyLabel(aMinor), u"6=A"); + + // Follow mode, C Dorian (relative major is Bb major) + KeySigEvent cDorian; + cDorian.setConcertKey(Key::B_B); + cDorian.setMode(KeyMode::DORIAN); + EXPECT_EQ(jianpuKeyLabel(cDorian), u"2=C"); + + // Custom numbering always shows a label, regardless of the score's mode + KeySigEvent custom; + custom.setConcertKey(Key::G); + custom.setJianpuNumbering(JianpuTonicMode::CUSTOM); + custom.setJianpuTonicKey(Key::C); + custom.setJianpuTonicMode(KeyMode::MAJOR); + EXPECT_EQ(jianpuKeyLabel(custom), u"1=C"); +} + +//--------------------------------------------------------- +// The three new Jianpu Pids must round-trip through KeySig::setProperty / +// getProperty and report the documented defaults via propertyDefault. +//--------------------------------------------------------- +TEST_F(Engraving_KeySigTests, jianpuKeySigProperties) +{ + MasterScore* score = ScoreRW::readScore(KEYSIG_DATA_DIR + u"keysigMode.mscx"); + EXPECT_TRUE(score); + Measure* m1 = score->firstMeasure(); + KeySig* ke = toKeySig(m1->findSegment(SegmentType::KeySig, m1->tick())->element(0)); + + EXPECT_EQ(ke->propertyDefault(Pid::JIANPU_NUMBERING).toInt(), int(JianpuTonicMode::FOLLOW_SCORE_KEY)); + EXPECT_EQ(Key(ke->propertyDefault(Pid::JIANPU_TONIC_KEY).toInt()), Key::C); + EXPECT_EQ(ke->propertyDefault(Pid::JIANPU_TONIC_MODE).value(), KeyMode::MAJOR); + + EXPECT_EQ(ke->getProperty(Pid::JIANPU_NUMBERING).toInt(), int(JianpuTonicMode::FOLLOW_SCORE_KEY)); + + ke->setProperty(Pid::JIANPU_NUMBERING, int(JianpuTonicMode::CUSTOM)); + ke->setProperty(Pid::JIANPU_TONIC_KEY, int(Key::E_B)); + ke->setProperty(Pid::JIANPU_TONIC_MODE, KeyMode::PHRYGIAN); + + EXPECT_EQ(ke->getProperty(Pid::JIANPU_NUMBERING).toInt(), int(JianpuTonicMode::CUSTOM)); + EXPECT_EQ(Key(ke->getProperty(Pid::JIANPU_TONIC_KEY).toInt()), Key::E_B); + EXPECT_EQ(ke->getProperty(Pid::JIANPU_TONIC_MODE).value(), KeyMode::PHRYGIAN); + + delete score; +} + +//--------------------------------------------------------- +// End-to-end check that TLayout::layoutKeySig actually populates +// KeySig::LayoutData::jianpuLabel on a real Jianpu staff: the major +// fallback label for the untouched (mode == UNKNOWN) header key sig, a +// label once Mode is set, and a label for Custom numbering. +//--------------------------------------------------------- +TEST_F(Engraving_KeySigTests, jianpuKeyLabelLayout) +{ + MasterScore* score = ScoreRW::readScore(u"beam_data/jianpuBeam.mscx"); + EXPECT_TRUE(score); + + Measure* m1 = score->firstMeasure(); + EXPECT_TRUE(m1); + Segment* keySigSeg = m1->findSegment(SegmentType::KeySig, m1->tick()); + EXPECT_TRUE(keySigSeg); + KeySig* ke = toKeySig(keySigSeg->element(0)); + EXPECT_TRUE(ke); + EXPECT_TRUE(ke->staff()->isJianpuStaff(ke->tick())); + + // Untouched header key sig: mode defaults to UNKNOWN, falls back to major + EXPECT_EQ(ke->ldata()->jianpuLabel, u"1=C"); + + ke->setProperty(Pid::KEYSIG_MODE, KeyMode::MAJOR); + score->doLayout(); + keySigSeg = m1->findSegment(SegmentType::KeySig, m1->tick()); + ke = toKeySig(keySigSeg->element(0)); + EXPECT_EQ(ke->ldata()->jianpuLabel, u"1=C"); + + ke->setProperty(Pid::JIANPU_NUMBERING, int(JianpuTonicMode::CUSTOM)); + ke->setProperty(Pid::JIANPU_TONIC_KEY, int(Key::D)); + ke->setProperty(Pid::JIANPU_TONIC_MODE, KeyMode::MAJOR); + score->doLayout(); + keySigSeg = m1->findSegment(SegmentType::KeySig, m1->tick()); + ke = toKeySig(keySigSeg->element(0)); + EXPECT_EQ(ke->ldata()->jianpuLabel, u"1=D"); + + delete score; +} diff --git a/src/engraving/tests/note_tests.cpp b/src/engraving/tests/note_tests.cpp index 48f5224a9edc..44b7c37299a2 100644 --- a/src/engraving/tests/note_tests.cpp +++ b/src/engraving/tests/note_tests.cpp @@ -504,6 +504,94 @@ TEST_F(Engraving_NoteTests, tpcDegrees) //QCOMPARE(tpc2degree(Tpc::TPC_B_S, Key::C_S), 7); } +TEST_F(Engraving_NoteTests, jianpuOctave) +{ + EXPECT_EQ(pitch2JianpuOctave(36, Tpc::TPC_C, Key::C, KeyMode::MAJOR), -1); + EXPECT_EQ(pitch2JianpuOctave(48, Tpc::TPC_C, Key::C, KeyMode::MAJOR), 0); + EXPECT_EQ(pitch2JianpuOctave(60, Tpc::TPC_C, Key::C, KeyMode::MAJOR), 1); + EXPECT_EQ(pitch2JianpuOctave(72, Tpc::TPC_C, Key::C, KeyMode::MAJOR), 2); + + EXPECT_EQ(pitch2JianpuOctave(43, Tpc::TPC_G, Key::G, KeyMode::MAJOR), -1); + EXPECT_EQ(pitch2JianpuOctave(48, Tpc::TPC_C, Key::G, KeyMode::MAJOR), -1); + EXPECT_EQ(pitch2JianpuOctave(55, Tpc::TPC_G, Key::G, KeyMode::MAJOR), 0); + EXPECT_EQ(pitch2JianpuOctave(60, Tpc::TPC_C, Key::G, KeyMode::MAJOR), 0); + EXPECT_EQ(pitch2JianpuOctave(66, Tpc::TPC_F_S, Key::G, KeyMode::MAJOR), 0); + EXPECT_EQ(pitch2JianpuOctave(67, Tpc::TPC_G, Key::G, KeyMode::MAJOR), 1); + EXPECT_EQ(pitch2JianpuOctave(79, Tpc::TPC_G, Key::G, KeyMode::MAJOR), 2); +} + +TEST_F(Engraving_NoteTests, jianpuModeMapping) +{ + String accidental; + String degree; + + EXPECT_EQ(jianpuTonicTpc(Key::B_B, KeyMode::DORIAN), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::A_B, KeyMode::PHRYGIAN), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::G, KeyMode::LYDIAN), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::F, KeyMode::MIXOLYDIAN), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::E_B, KeyMode::AEOLIAN), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::C, KeyMode::IONIAN), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::D_B, KeyMode::LOCRIAN), Tpc::TPC_C); + + EXPECT_EQ(jianpuTonicTpc(Key::E_B, KeyMode::MINOR), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::C, KeyMode::MAJOR), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::C, KeyMode::UNKNOWN), Tpc::TPC_C); + EXPECT_EQ(jianpuTonicTpc(Key::C, KeyMode::NONE), Tpc::TPC_C); + + const int cDorianTonic = jianpuTonicTpc(Key::B_B, KeyMode::DORIAN); + tpc2Jianpu(Tpc::TPC_C, cDorianTonic, KeyMode::DORIAN, accidental, degree); + EXPECT_EQ(degree, u"2"); + EXPECT_TRUE(accidental.empty()); + + tpc2Jianpu(Tpc::TPC_E_B, cDorianTonic, KeyMode::DORIAN, accidental, degree); + EXPECT_EQ(degree, u"4"); + EXPECT_TRUE(accidental.empty()); + + tpc2Jianpu(Tpc::TPC_E, cDorianTonic, KeyMode::DORIAN, accidental, degree); + EXPECT_EQ(degree, u"4"); + EXPECT_EQ(accidental, u"#"); + + const int cMajorTonic = key2Tpc(Key::C); + tpc2Jianpu(Tpc::TPC_F_S, cMajorTonic, KeyMode::MAJOR, accidental, degree); + EXPECT_EQ(degree, u"4"); + EXPECT_EQ(accidental, u"#"); + tpc2Jianpu(Tpc::TPC_G_B, cMajorTonic, KeyMode::MAJOR, accidental, degree); + EXPECT_EQ(degree, u"5"); + EXPECT_EQ(accidental, u"b"); + + const int aMinorTonic = jianpuTonicTpc(Key::C, KeyMode::MINOR); + EXPECT_EQ(aMinorTonic, Tpc::TPC_A); + tpc2Jianpu(Tpc::TPC_A, aMinorTonic, KeyMode::MINOR, accidental, degree); + EXPECT_EQ(degree, u"6"); + EXPECT_TRUE(accidental.empty()); + tpc2Jianpu(Tpc::TPC_C, aMinorTonic, KeyMode::MINOR, accidental, degree); + EXPECT_EQ(degree, u"1"); + EXPECT_TRUE(accidental.empty()); + + EXPECT_EQ(pitch2JianpuOctave(58, Tpc::TPC_B_B, cDorianTonic, KeyMode::DORIAN), 0); + EXPECT_EQ(pitch2JianpuOctave(60, Tpc::TPC_C, cDorianTonic, KeyMode::DORIAN), 0); + EXPECT_EQ(pitch2JianpuOctave(57, Tpc::TPC_A, aMinorTonic, KeyMode::MINOR), -1); + EXPECT_EQ(pitch2JianpuOctave(59, Tpc::TPC_B, aMinorTonic, KeyMode::MINOR), -1); + EXPECT_EQ(pitch2JianpuOctave(60, Tpc::TPC_C, aMinorTonic, KeyMode::MINOR), 0); + EXPECT_EQ(pitch2JianpuOctave(69, Tpc::TPC_A, aMinorTonic, KeyMode::MINOR), 0); + EXPECT_EQ(pitch2JianpuOctave(72, Tpc::TPC_C, aMinorTonic, KeyMode::MINOR), 1); +} + +// KeyMode::UNKNOWN is the mode stored for any ordinary key signature that never had an +// explicit mode assigned (i.e. almost every major/minor key signature in real scores, +// since the "mode" XML tag is only written when it differs from UNKNOWN). It must be +// treated the same as KeyMode::MAJOR so that FOLLOW_SCORE_KEY tracks the actual key. +TEST_F(Engraving_NoteTests, jianpuUnknownModeTracksScoreKey) +{ + EXPECT_EQ(jianpuTonicTpc(Key::G, KeyMode::UNKNOWN), jianpuTonicTpc(Key::G, KeyMode::MAJOR)); + EXPECT_EQ(jianpuTonicTpc(Key::G, KeyMode::UNKNOWN), Tpc::TPC_G); + EXPECT_EQ(jianpuTonicTpc(Key::E_B, KeyMode::UNKNOWN), jianpuTonicTpc(Key::E_B, KeyMode::MAJOR)); + EXPECT_EQ(jianpuTonicTpc(Key::E_B, KeyMode::UNKNOWN), Tpc::TPC_E_B); + + // Atonal (custom) key signatures have no key to derive a tonic from. + EXPECT_EQ(jianpuTonicTpc(Key::C, KeyMode::NONE), Tpc::TPC_C); +} + TEST_F(Engraving_NoteTests, alteredUnison) { MasterScore* score = ScoreRW::readScore(NOTE_DATA_DIR + u"altered-unison.mscx"); diff --git a/src/engraving/types/types.h b/src/engraving/types/types.h index de330f007090..ba0e7d757ba3 100644 --- a/src/engraving/types/types.h +++ b/src/engraving/types/types.h @@ -1185,6 +1185,12 @@ enum class CourtesyBarlineMode : char { DOUBLE_BEFORE_COURTESY = 2, }; +// How the tonic ("1") of a Jianpu staff is determined +enum class JianpuTonicMode : char { + FOLLOW_SCORE_KEY = 0, // tonic follows the actual key signature + CUSTOM = 1 // tonic and mode are selected independently +}; + enum class FermataType : signed char { Undefined = -1, VeryShort, diff --git a/src/notationscene/widgets/editstyle.cpp b/src/notationscene/widgets/editstyle.cpp index cf54a36944a6..2a974cf083e0 100644 --- a/src/notationscene/widgets/editstyle.cpp +++ b/src/notationscene/widgets/editstyle.cpp @@ -721,6 +721,7 @@ void EditStyle::classBegin() { StyleId::parenthesizeTiedFretIfArticulation, false, tabParenthArticulation, 0 }, { StyleId::jianpuNumberVerticalDistance, false, jianpuNumberVerticalDistance, jianpuNumberVerticalDistanceReset }, + { StyleId::jianpuVoiceVerticalDistance, false, jianpuVoiceVerticalDistance, jianpuVoiceVerticalDistanceReset }, { StyleId::jianpuOctaveDotRadius, false, jianpuOctaveDotRadius, jianpuOctaveDotRadiusReset }, { StyleId::jianpuOctaveDotDistance, false, jianpuOctaveDotDistance, jianpuOctaveDotDistanceReset }, { StyleId::jianpuDiminutionBeamThickness, false, jianpuDiminutionBeamThickness, jianpuDiminutionBeamThicknessReset }, diff --git a/src/notationscene/widgets/editstyle.ui b/src/notationscene/widgets/editstyle.ui index adeb2226a175..6a83e4e80547 100644 --- a/src/notationscene/widgets/editstyle.ui +++ b/src/notationscene/widgets/editstyle.ui @@ -12983,7 +12983,7 @@ followed by dashes - Vertical distance: + Number vertical distance: jianpuNumberVerticalDistance @@ -13020,6 +13020,45 @@ followed by dashes + + + Voice vertical distance: + + + jianpuVoiceVerticalDistance + + + + + + + false + + + sp + + + 9.000000000000000 + + + 0.010000000000000 + + + + + + + Reset to default + + + Reset 'Voice vertical distance' value + + + + + + + Octave dot radius: @@ -13029,7 +13068,7 @@ followed by dashes - + false @@ -13045,7 +13084,7 @@ followed by dashes - + Reset to default @@ -13058,7 +13097,7 @@ followed by dashes - + Octave dot distance: @@ -13068,7 +13107,7 @@ followed by dashes - + false @@ -13084,7 +13123,7 @@ followed by dashes - + Reset to default @@ -13097,7 +13136,7 @@ followed by dashes - + Diminution beam thickness: @@ -13107,7 +13146,7 @@ followed by dashes - + false @@ -13123,7 +13162,7 @@ followed by dashes - + Reset to default @@ -13136,7 +13175,7 @@ followed by dashes - + Diminution beam distance: @@ -13146,7 +13185,7 @@ followed by dashes - + false @@ -13162,7 +13201,7 @@ followed by dashes - + Reset to default @@ -13175,7 +13214,7 @@ followed by dashes - + Diminution beam placement: @@ -13185,7 +13224,7 @@ followed by dashes - + @@ -13199,7 +13238,7 @@ followed by dashes - + Reset to default @@ -13212,7 +13251,7 @@ followed by dashes - + Duration line thickness: @@ -13222,7 +13261,7 @@ followed by dashes - + false @@ -13238,7 +13277,7 @@ followed by dashes - + Reset to default diff --git a/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/KeySignatureSettings.qml b/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/KeySignatureSettings.qml index 96e36be8a242..7573fb56cd82 100644 --- a/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/KeySignatureSettings.qml +++ b/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/KeySignatureSettings.qml @@ -78,4 +78,70 @@ Column { { text: qsTrc("propertiespanel", "Locrian", "key signature mode"), value: KeySignatureTypes.MODE_LOCRIAN } ] } + + DropdownPropertyView { + titleText: qsTrc("propertiespanel", "Jianpu numbering") + propertyItem: root.model ? root.model.jianpuNumbering : null + visible: root.model ? root.model.isJianpuStaff : false + + navigationName: "JianpuNumbering" + navigationPanel: root.navigationPanel + navigationRowStart: root.navigationRowStart + 3 + + model: [ + { text: qsTrc("propertiespanel", "Follow score key"), value: KeySignatureTypes.FOLLOW_SCORE_KEY }, + { text: qsTrc("propertiespanel", "Custom"), value: KeySignatureTypes.CUSTOM } + ] + } + + DropdownPropertyView { + titleText: qsTrc("propertiespanel", "Jianpu tonal center") + propertyItem: root.model ? root.model.jianpuTonicKey : null + visible: root.model ? root.model.isJianpuStaff : false + + navigationName: "JianpuTonicKey" + navigationPanel: root.navigationPanel + navigationRowStart: root.navigationRowStart + 4 + + model: [ + { text: "C♭", value: -7 }, + { text: "G♭", value: -6 }, + { text: "D♭", value: -5 }, + { text: "A♭", value: -4 }, + { text: "E♭", value: -3 }, + { text: "B♭", value: -2 }, + { text: "F", value: -1 }, + { text: "C", value: 0 }, + { text: "G", value: 1 }, + { text: "D", value: 2 }, + { text: "A", value: 3 }, + { text: "E", value: 4 }, + { text: "B", value: 5 }, + { text: "F♯", value: 6 }, + { text: "C♯", value: 7 } + ] + } + + DropdownPropertyView { + //: Musical mode (major, minor, dorian, phrygian, lydian, etc.) + titleText: qsTrc("propertiespanel", "Jianpu scale mode") + propertyItem: root.model ? root.model.jianpuTonicMode : null + visible: root.model ? root.model.isJianpuStaff : false + + navigationName: "JianpuTonicMode" + navigationPanel: root.navigationPanel + navigationRowStart: root.navigationRowStart + 5 + + model: [ + //: mode of a key signature, not an interval; Ionian is an alternative name for this mode + { text: qsTrc("propertiespanel", "Major (Ionian)", "key signature mode"), value: KeySignatureTypes.MODE_MAJOR }, + { text: qsTrc("propertiespanel", "Dorian", "key signature mode"), value: KeySignatureTypes.MODE_DORIAN }, + { text: qsTrc("propertiespanel", "Phrygian", "key signature mode"), value: KeySignatureTypes.MODE_PHRYGIAN }, + { text: qsTrc("propertiespanel", "Lydian", "key signature mode"), value: KeySignatureTypes.MODE_LYDIAN }, + { text: qsTrc("propertiespanel", "Mixolydian", "key signature mode"), value: KeySignatureTypes.MODE_MIXOLYDIAN }, + //: mode of a key signature, not an interval; Aeolian is an alternative name for this mode + { text: qsTrc("propertiespanel", "Minor (Aeolian)", "key signature mode"), value: KeySignatureTypes.MODE_MINOR }, + { text: qsTrc("propertiespanel", "Locrian", "key signature mode"), value: KeySignatureTypes.MODE_LOCRIAN } + ] + } } diff --git a/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.cpp b/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.cpp index acc85c037db5..52c6646a6f3b 100644 --- a/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.cpp +++ b/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.cpp @@ -24,6 +24,7 @@ #include "engraving/dom/layoutbreak.h" #include "engraving/dom/measure.h" #include "engraving/dom/keysig.h" +#include "engraving/dom/staff.h" #include "translation.h" @@ -43,6 +44,9 @@ void KeySignatureSettingsModel::createProperties() { m_hasToShowCourtesy = buildPropertyItem(mu::engraving::Pid::SHOW_COURTESY); m_mode = buildPropertyItem(mu::engraving::Pid::KEYSIG_MODE); + m_jianpuNumbering = buildPropertyItem(mu::engraving::Pid::JIANPU_NUMBERING); + m_jianpuTonicKey = buildPropertyItem(mu::engraving::Pid::JIANPU_TONIC_KEY); + m_jianpuTonicMode = buildPropertyItem(mu::engraving::Pid::JIANPU_TONIC_MODE); } void KeySignatureSettingsModel::requestElements() @@ -54,12 +58,15 @@ void KeySignatureSettingsModel::loadProperties() { loadPropertyItem(m_hasToShowCourtesy); loadPropertyItem(m_mode); + loadPropertyItem(m_jianpuNumbering); + loadPropertyItem(m_jianpuTonicKey); + loadPropertyItem(m_jianpuTonicMode); bool enableMode = true; bool enableCourtesy = true; for (const mu::engraving::EngravingItem* element : m_elementList) { - if (element->generated() || toKeySig(element)->isCourtesy()) { + if (toKeySig(element)->isCourtesy()) { enableMode = false; } @@ -73,6 +80,13 @@ void KeySignatureSettingsModel::loadProperties() m_hasToShowCourtesy->setIsEnabled(enableCourtesy); m_mode->setIsEnabled(enableMode); + m_jianpuNumbering->setIsEnabled(enableMode); + + const bool customJianpuMapping = m_jianpuNumbering->value().toInt() == int(mu::engraving::JianpuTonicMode::CUSTOM); + m_jianpuTonicKey->setIsEnabled(enableMode && customJianpuMapping); + m_jianpuTonicMode->setIsEnabled(enableMode && customJianpuMapping); + + updateIsJianpuStaff(); } PropertyItem* KeySignatureSettingsModel::hasToShowCourtesy() const @@ -84,3 +98,39 @@ PropertyItem* KeySignatureSettingsModel::mode() const { return m_mode; } + +PropertyItem* KeySignatureSettingsModel::jianpuNumbering() const +{ + return m_jianpuNumbering; +} + +PropertyItem* KeySignatureSettingsModel::jianpuTonicKey() const +{ + return m_jianpuTonicKey; +} + +PropertyItem* KeySignatureSettingsModel::jianpuTonicMode() const +{ + return m_jianpuTonicMode; +} + +bool KeySignatureSettingsModel::isJianpuStaff() const +{ + return m_isJianpuStaff; +} + +void KeySignatureSettingsModel::updateIsJianpuStaff() +{ + bool isJianpuStaff = false; + for (const mu::engraving::EngravingItem* element : m_elementList) { + if (element->staff() && element->staff()->isJianpuStaff(element->tick())) { + isJianpuStaff = true; + break; + } + } + + if (m_isJianpuStaff != isJianpuStaff) { + m_isJianpuStaff = isJianpuStaff; + emit isJianpuStaffChanged(m_isJianpuStaff); + } +} diff --git a/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.h b/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.h index ef5cb9c8f630..176b34ad776d 100644 --- a/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.h +++ b/src/propertiespanel/qml/MuseScore/PropertiesPanel/notation/keysignatures/keysignaturesettingsmodel.h @@ -34,6 +34,10 @@ class KeySignatureSettingsModel : public PropertiesPanelAbstractModel Q_PROPERTY(mu::propertiespanel::PropertyItem * hasToShowCourtesy READ hasToShowCourtesy CONSTANT) Q_PROPERTY(mu::propertiespanel::PropertyItem * mode READ mode CONSTANT) + Q_PROPERTY(mu::propertiespanel::PropertyItem * jianpuNumbering READ jianpuNumbering CONSTANT) + Q_PROPERTY(mu::propertiespanel::PropertyItem * jianpuTonicKey READ jianpuTonicKey CONSTANT) + Q_PROPERTY(mu::propertiespanel::PropertyItem * jianpuTonicMode READ jianpuTonicMode CONSTANT) + Q_PROPERTY(bool isJianpuStaff READ isJianpuStaff NOTIFY isJianpuStaffChanged) public: explicit KeySignatureSettingsModel(QObject* parent, const muse::modularity::ContextPtr& iocCtx, IElementRepositoryService* repository); @@ -43,9 +47,22 @@ class KeySignatureSettingsModel : public PropertiesPanelAbstractModel PropertyItem* hasToShowCourtesy() const; PropertyItem* mode() const; + PropertyItem* jianpuNumbering() const; + PropertyItem* jianpuTonicKey() const; + PropertyItem* jianpuTonicMode() const; + bool isJianpuStaff() const; + +signals: + void isJianpuStaffChanged(bool isJianpuStaff); private: + void updateIsJianpuStaff(); + PropertyItem* m_hasToShowCourtesy = nullptr; PropertyItem* m_mode = nullptr; + PropertyItem* m_jianpuNumbering = nullptr; + PropertyItem* m_jianpuTonicKey = nullptr; + PropertyItem* m_jianpuTonicMode = nullptr; + bool m_isJianpuStaff = false; }; } diff --git a/src/propertiespanel/qml/MuseScore/PropertiesPanel/types/keysignaturetypes.h b/src/propertiespanel/qml/MuseScore/PropertiesPanel/types/keysignaturetypes.h index 1571be1cd68a..576842588a4c 100644 --- a/src/propertiespanel/qml/MuseScore/PropertiesPanel/types/keysignaturetypes.h +++ b/src/propertiespanel/qml/MuseScore/PropertiesPanel/types/keysignaturetypes.h @@ -46,5 +46,12 @@ enum class Mode { }; Q_ENUM_NS(Mode) + +enum class JianpuNumbering { + FOLLOW_SCORE_KEY = int(engraving::JianpuTonicMode::FOLLOW_SCORE_KEY), + CUSTOM = int(engraving::JianpuTonicMode::CUSTOM) +}; + +Q_ENUM_NS(JianpuNumbering) } } From 90006a8c476d7898f6cdee4de395ff72ca902466 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Fri, 23 Oct 2026 23:10:00 +1100 Subject: [PATCH 18/24] jianpu tonic - suppor different number setting on linked staff --- src/engraving/dom/property.cpp | 3 ++ src/engraving/tests/keysig_tests.cpp | 49 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/engraving/dom/property.cpp b/src/engraving/dom/property.cpp index 173983d8798e..01d2592cc46c 100644 --- a/src/engraving/dom/property.cpp +++ b/src/engraving/dom/property.cpp @@ -583,6 +583,9 @@ bool propertyLinkSameScore(Pid id) { assert(id < Pid::END); switch (id) { + case Pid::JIANPU_NUMBERING: + case Pid::JIANPU_TONIC_KEY: + case Pid::JIANPU_TONIC_MODE: case Pid::STAFF_BARLINE_SPAN: case Pid::STAFF_BARLINE_SPAN_FROM: case Pid::STAFF_BARLINE_SPAN_TO: diff --git a/src/engraving/tests/keysig_tests.cpp b/src/engraving/tests/keysig_tests.cpp index f2ac12746eb3..46c1fb2bf342 100644 --- a/src/engraving/tests/keysig_tests.cpp +++ b/src/engraving/tests/keysig_tests.cpp @@ -397,6 +397,55 @@ TEST_F(Engraving_KeySigTests, jianpuKeySigProperties) delete score; } +TEST_F(Engraving_KeySigTests, jianpuLinkedKeySigProperties) +{ + std::unique_ptr score(ScoreRW::readScore(KEYSIG_DATA_DIR + u"keysigMode.mscx")); + ASSERT_TRUE(score); + Measure* measure = score->firstMeasure(); + ASSERT_TRUE(measure); + Segment* segment = measure->findSegment(SegmentType::KeySig, measure->tick()); + ASSERT_TRUE(segment); + KeySig* source = toKeySig(segment->element(0)); + KeySig* linked = toKeySig(segment->element(4)); + ASSERT_TRUE(source); + ASSERT_TRUE(linked); + linked->linkTo(source); + + const Pid properties[] = { Pid::JIANPU_NUMBERING, Pid::JIANPU_TONIC_KEY, Pid::JIANPU_TONIC_MODE }; + const PropertyValue values[] = { int(JianpuTonicMode::CUSTOM), int(Key::E_B), KeyMode::PHRYGIAN }; + PropertyValue originalValues[3]; + for (size_t index = 0; index < 3; ++index) { + originalValues[index] = source->getProperty(properties[index]); + EXPECT_EQ(linked->getProperty(properties[index]), originalValues[index]); + } + + score->startCmd(TranslatableString::untranslatable("Key signature tests")); + for (size_t index = 0; index < 3; ++index) { + source->undoChangeProperty(properties[index], values[index]); + } + score->endCmd(); + + for (size_t index = 0; index < 3; ++index) { + EXPECT_EQ(source->getProperty(properties[index]), values[index]); + EXPECT_EQ(linked->getProperty(properties[index]), originalValues[index]); + } + EXPECT_EQ(source->staff()->keySigEvent(source->tick()), source->keySigEvent()); + EXPECT_EQ(linked->staff()->keySigEvent(linked->tick()), linked->keySigEvent()); + + EditData editData; + score->transactionManager()->undoRedo(true, &editData); + for (size_t index = 0; index < 3; ++index) { + EXPECT_EQ(source->getProperty(properties[index]), originalValues[index]); + EXPECT_EQ(linked->getProperty(properties[index]), originalValues[index]); + } + + score->transactionManager()->undoRedo(false, &editData); + for (size_t index = 0; index < 3; ++index) { + EXPECT_EQ(source->getProperty(properties[index]), values[index]); + EXPECT_EQ(linked->getProperty(properties[index]), originalValues[index]); + } +} + //--------------------------------------------------------- // End-to-end check that TLayout::layoutKeySig actually populates // KeySig::LayoutData::jianpuLabel on a real Jianpu staff: the major From a117f17eaadca218b9efe3c168e2c54f1bbd9daf Mon Sep 17 00:00:00 2001 From: songzhenk Date: Sun, 25 Oct 2026 00:41:49 +1100 Subject: [PATCH 19/24] fix missing diminution duration line for single grace note --- src/engraving/rendering/score/beamlayout.cpp | 16 +++++++++ src/engraving/tests/beam_tests.cpp | 36 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/engraving/rendering/score/beamlayout.cpp b/src/engraving/rendering/score/beamlayout.cpp index e051be305a1d..79e34ce141d9 100644 --- a/src/engraving/rendering/score/beamlayout.cpp +++ b/src/engraving/rendering/score/beamlayout.cpp @@ -607,6 +607,16 @@ void BeamLayout::beamGraceNotes(LayoutContext& ctx, Chord* mainNote, bool after) } if (a1 == 0) { a1 = cr; + if (cr->isJianpuStaff()) { + beam = cr->beam(); + if (!beam || beam->elements().front() != cr) { + beam = Factory::createBeam(ctx.mutDom().score()); + beam->setGenerated(true); + beam->setTrack(mainNote->track()); + cr->replaceBeam(beam); + } + a1 = 0; + } } else { if (!beamModeMid(bm) && (bm == BeamMode::BEGIN)) { a1->removeDeleteBeam(false); @@ -1333,6 +1343,12 @@ void BeamLayout::createBeamSegment(Beam* item, ChordRest* startCr, ChordRest* en void BeamLayout::createBeamletSegment(Beam* item, const LayoutContext& ctx, ChordRest* cr, bool isBefore, int level) { + // Jianpu uses a full-width beam across the same chord/rest instead of a beamlet. + if (item->isJianpuStaff()) { + createBeamSegment(item, cr, cr, level, ctx.conf().styleB(Sid::frenchStyleBeams)); + return; + } + const double startX = BeamTremoloLayout::chordBeamAnchorX(item->ldata(), cr, isBefore ? ChordBeamAnchorType::End diff --git a/src/engraving/tests/beam_tests.cpp b/src/engraving/tests/beam_tests.cpp index cff2281783c6..44e7cdfcd66d 100644 --- a/src/engraving/tests/beam_tests.cpp +++ b/src/engraving/tests/beam_tests.cpp @@ -194,6 +194,42 @@ TEST_F(Engraving_BeamTests, jianpuBeam) beam("jianpuBeam.mscx"); } +TEST_F(Engraving_BeamTests, singleGraceNoteBeam) +{ + for (bool jianpu : { false, true }) { + for (int duration : { Constants::DIVISION / 2, Constants::DIVISION / 4 }) { + std::unique_ptr score(ScoreRW::readScore(jianpu + ? BEAM_DATA_DIR + u"jianpuBeam.mscx" + : u"note_data/grace.mscx")); + ASSERT_TRUE(score); + Chord* chord = score->firstMeasure()->findChord(Fraction(0, 1), 0); + ASSERT_TRUE(chord); + score->setGraceNote(chord, chord->upNote()->pitch(), NoteType::APPOGGIATURA, duration); + ASSERT_EQ(chord->graceNotes().size(), 1); + Chord* grace = chord->graceNotes().front(); + score->doLayout(); + if (!jianpu) { + EXPECT_EQ(grace->beam(), nullptr); + continue; + } + Beam* beam = grace->beam(); + ASSERT_TRUE(beam); + EXPECT_TRUE(beam->isGrace()); + ASSERT_EQ(beam->elements().size(), 1); + EXPECT_EQ(beam->elements().front(), grace); + ASSERT_EQ(beam->beamSegments().size(), duration == Constants::DIVISION / 2 ? 1 : 2); + for (const BeamSegment* segment : beam->beamSegments()) { + EXPECT_NEAR(segment->line.x2() - segment->line.x1(), grace->ldata()->bbox().width(), 0.001); + } + score->doLayout(); + EXPECT_EQ(grace->beam(), beam); + grace->setBeamMode(BeamMode::NONE); + score->doLayout(); + EXPECT_EQ(grace->beam(), nullptr); + } + } +} + TEST_F(Engraving_BeamTests, flatBeams) { MasterScore* score = ScoreRW::readScore(BEAM_DATA_DIR + u"flatBeams.mscx"); From 75e7eba6f9590255d8a94f67b6befb7248436150 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Sun, 25 Oct 2026 16:04:21 +1100 Subject: [PATCH 20/24] fix missing diminution duration line for single note which has custom break beam --- src/engraving/rendering/score/beamlayout.cpp | 46 ++++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/engraving/rendering/score/beamlayout.cpp b/src/engraving/rendering/score/beamlayout.cpp index 79e34ce141d9..461de7599b03 100644 --- a/src/engraving/rendering/score/beamlayout.cpp +++ b/src/engraving/rendering/score/beamlayout.cpp @@ -559,6 +559,18 @@ static bool beamNoContinue(BeamMode mode) #define beamModeMid(a) (a == BeamMode::MID || a == BeamMode::BEGIN16 || a == BeamMode::BEGIN32) +static Beam* ensureBeam(LayoutContext& ctx, ChordRest* first, track_idx_t track) +{ + Beam* beam = first->beam(); + if (!beam || beam->elements().front() != first) { + beam = Factory::createBeam(ctx.mutDom().score()); + beam->setGenerated(true); + beam->setTrack(track); + first->replaceBeam(beam); + } + return beam; +} + //--------------------------------------------------------- // beamGraceNotes //--------------------------------------------------------- @@ -606,29 +618,17 @@ void BeamLayout::beamGraceNotes(LayoutContext& ctx, Chord* mainNote, bool after) continue; } if (a1 == 0) { - a1 = cr; if (cr->isJianpuStaff()) { - beam = cr->beam(); - if (!beam || beam->elements().front() != cr) { - beam = Factory::createBeam(ctx.mutDom().score()); - beam->setGenerated(true); - beam->setTrack(mainNote->track()); - cr->replaceBeam(beam); - } - a1 = 0; + beam = ensureBeam(ctx, cr, mainNote->track()); + } else { + a1 = cr; } } else { if (!beamModeMid(bm) && (bm == BeamMode::BEGIN)) { a1->removeDeleteBeam(false); a1 = cr; } else { - beam = a1->beam(); - if (beam == 0 || beam->elements().front() != a1) { - beam = Factory::createBeam(ctx.mutDom().score()); - beam->setGenerated(true); - beam->setTrack(mainNote->track()); - a1->replaceBeam(beam); - } + beam = ensureBeam(ctx, a1, mainNote->track()); cr->replaceBeam(beam); a1 = 0; } @@ -788,7 +788,11 @@ void BeamLayout::createBeams(LayoutContext& ctx, Measure* measure) } if (a1 == 0) { - a1 = cr; + if (cr->isJianpuStaff()) { + beam = ensureBeam(ctx, cr, track); + } else { + a1 = cr; + } } else { if (!beamModeMid(bm) && @@ -800,13 +804,7 @@ void BeamLayout::createBeams(LayoutContext& ctx, Measure* measure) a1->removeDeleteBeam(false); a1 = cr; } else { - beam = a1->beam(); - if (beam == 0 || beam->elements().front() != a1) { - beam = Factory::createBeam(ctx.mutDom().score()); - beam->setGenerated(true); - beam->setTrack(track); - a1->replaceBeam(beam); - } + beam = ensureBeam(ctx, a1, track); cr->replaceBeam(beam); a1 = 0; } From d7cc196b022078c550284cf18e9138fa1521b19a Mon Sep 17 00:00:00 2001 From: songzhenk Date: Sun, 25 Oct 2026 21:48:09 +1100 Subject: [PATCH 21/24] Fixed an issue where the Jianpu font failed to load --- src/engraving/rw/read460/tread.cpp | 2 -- src/engraving/rw/read500/tread.cpp | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engraving/rw/read460/tread.cpp b/src/engraving/rw/read460/tread.cpp index 569af660e482..fc9898d89f5d 100644 --- a/src/engraving/rw/read460/tread.cpp +++ b/src/engraving/rw/read460/tread.cpp @@ -3959,8 +3959,6 @@ void TRead::read(StaffType* t, XmlReader& e, ReadContext& ctx) t->setFretUseTextStyle(e.readBool()); } else if (tag == "fretTextStyle") { t->setFretTextStyle(TextStyleType(TConv::fromXml(e.readAsciiText(), TextStyleType::TAB_FRET_NUMBER))); - } else if (tag == "jianpuTextStyle") { - t->setJianpuTextStyle(TextStyleType(TConv::fromXml(e.readAsciiText(), TextStyleType::JIANPU_NUMBER))); } else if (tag == "symbolRepeat") { t->setSymbolRepeat((TablatureSymbolRepeat)e.readInt()); } else if (tag == "linesThrough") { diff --git a/src/engraving/rw/read500/tread.cpp b/src/engraving/rw/read500/tread.cpp index 2807f5c4c6a0..b2c21e136738 100644 --- a/src/engraving/rw/read500/tread.cpp +++ b/src/engraving/rw/read500/tread.cpp @@ -4042,6 +4042,8 @@ void TRead::read(StaffType* t, XmlReader& e, ReadContext& ctx) t->setFretUseTextStyle(e.readBool()); } else if (tag == "fretTextStyle") { t->setFretTextStyle(TextStyleType(TConv::fromXml(e.readAsciiText(), TextStyleType::TAB_FRET_NUMBER))); + } else if (tag == "jianpuTextStyle") { + t->setJianpuTextStyle(TextStyleType(TConv::fromXml(e.readAsciiText(), TextStyleType::JIANPU_NUMBER))); } else if (tag == "symbolRepeat") { t->setSymbolRepeat((TablatureSymbolRepeat)e.readInt()); } else if (tag == "linesThrough") { From 87b914699bece5e23da750a8c5f37ecac363c8d0 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Wed, 28 Oct 2026 22:48:00 +1100 Subject: [PATCH 22/24] Preserve Jianpu duration-line styling and default to horizontal beams --- src/engraving/rendering/score/beamlayout.cpp | 11 ++++- src/engraving/tests/beam_tests.cpp | 51 ++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/engraving/rendering/score/beamlayout.cpp b/src/engraving/rendering/score/beamlayout.cpp index 461de7599b03..a0b740ced16f 100644 --- a/src/engraving/rendering/score/beamlayout.cpp +++ b/src/engraving/rendering/score/beamlayout.cpp @@ -198,6 +198,10 @@ void BeamLayout::layout1(Beam* item, LayoutContext& ctx) return; } + if (item->isJianpuStaff() && item->isStyled(Pid::BEAM_NO_SLOPE)) { + item->setNoSlope(true); + } + item->setMinMove(std::numeric_limits::max()); item->setMaxMove(std::numeric_limits::min()); double mag = 0.0; @@ -343,6 +347,11 @@ void BeamLayout::layout2(Beam* item, const LayoutContext& ctx, const std::vector item->setBeamWidth(item->beamWidth() * graceScale); } + if (item->isJianpuStaff()) { + item->setBeamWidth(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->mag() * graceScale); + item->setBeamDist(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->mag() * graceScale); + } + int fragmentIndex = item->directionIdx(); if (item->userModified()) { BeamTremoloLayout::setupLData(item, item->mutldata(), ctx); @@ -373,8 +382,6 @@ void BeamLayout::layout2(Beam* item, const LayoutContext& ctx, const std::vector item->setStartAnchor(item->ldata()->startAnchor); item->setEndAnchor(item->ldata()->endAnchor); item->setSlope(0.0); - item->setBeamWidth(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamThickness) * item->mag() * graceScale); - item->setBeamDist(ctx.conf().styleAbsolute(Sid::jianpuDiminutionBeamDistance) * item->mag() * graceScale); } else if (!item->isBesideTabStaff()) { BeamTremoloLayout::setupLData(item, item->mutldata(), ctx); BeamTremoloLayout::calculateAnchors(item, item->mutldata(), ctx, chordRests, item->notePositions()); diff --git a/src/engraving/tests/beam_tests.cpp b/src/engraving/tests/beam_tests.cpp index 44e7cdfcd66d..f1b8982f79f5 100644 --- a/src/engraving/tests/beam_tests.cpp +++ b/src/engraving/tests/beam_tests.cpp @@ -194,6 +194,57 @@ TEST_F(Engraving_BeamTests, jianpuBeam) beam("jianpuBeam.mscx"); } +TEST_F(Engraving_BeamTests, jianpuDraggedBeamKeepsDurationLineStyle) +{ + std::unique_ptr score(ScoreRW::readScore(BEAM_DATA_DIR + u"jianpuBeam.mscx")); + ASSERT_TRUE(score); + ChordRest* first = score->firstMeasure()->firstChordRest(0); + ASSERT_TRUE(first); + Beam* beam = first->beam(); + ASSERT_TRUE(beam); + ASSERT_TRUE(beam->isJianpuStaff()); + ASSERT_FALSE(beam->beamFragments().empty()); + const double width = beam->beamWidth(); + const double distance = beam->beamDist(); + beam->setProperty(Pid::BEAM_NO_SLOPE, false); + beam->setPropertyFlags(Pid::BEAM_NO_SLOPE, PropertyFlags::UNSTYLED); + const PairF position = beam->beamPos(); + beam->setBeamPos(PairF(position.first, position.second + 1.0)); + + score->doLayout(); + + EXPECT_FALSE(beam->getProperty(Pid::BEAM_NO_SLOPE).toBool()); + EXPECT_TRUE(beam->userModified()); + EXPECT_DOUBLE_EQ(beam->beamWidth(), width); + EXPECT_DOUBLE_EQ(beam->beamDist(), distance); + EXPECT_NE(beam->slope(), 0.0); + + beam->resetProperty(Pid::BEAM_NO_SLOPE); + score->doLayout(); + + EXPECT_TRUE(beam->getProperty(Pid::BEAM_NO_SLOPE).toBool()); + EXPECT_DOUBLE_EQ(beam->slope(), 0.0); +} + +TEST_F(Engraving_BeamTests, jianpuBeamDefaultsToForceHorizontal) +{ + std::unique_ptr score(ScoreRW::readScore(BEAM_DATA_DIR + u"jianpuBeam.mscx")); + ASSERT_TRUE(score); + ChordRest* first = score->firstMeasure()->firstChordRest(0); + ASSERT_TRUE(first); + Beam* beam = first->beam(); + ASSERT_TRUE(beam); + EXPECT_TRUE(beam->getProperty(Pid::BEAM_NO_SLOPE).toBool()); + + const PairF position = beam->beamPos(); + beam->setBeamPos(PairF(position.first + 1.0, position.second + 2.0)); + score->doLayout(); + + EXPECT_TRUE(beam->userModified()); + EXPECT_DOUBLE_EQ(beam->beamPos().first, beam->beamPos().second); + EXPECT_DOUBLE_EQ(beam->slope(), 0.0); +} + TEST_F(Engraving_BeamTests, singleGraceNoteBeam) { for (bool jianpu : { false, true }) { From af9e0b65db61d8dcf3c0afd89e47fd737b1e324d Mon Sep 17 00:00:00 2001 From: songzhenk Date: Thu, 29 Oct 2026 21:00:44 +1100 Subject: [PATCH 23/24] jianpu aligment - bypass mirroring and stem-side offsets --- src/engraving/rendering/score/chordlayout.cpp | 16 ++++--- src/engraving/tests/note_tests.cpp | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 8fb08dec94ac..2b4deea62dc2 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -3245,14 +3245,16 @@ void ChordLayout::layoutChords3(const std::vector& chords, [](Note* n1, const Note* n2) ->bool { return n1->line() < n2->line(); }); for (Note* note : chordNotes) { double noteX = 0.0; - if (note->ldata()->mirror()) { - if (_up) { - noteX = StemLayout::stemPosX(chord) - overlapMirror; - } else { - noteX = -note->headBodyWidth() + overlapMirror; + if (!isJianpu) { + if (note->ldata()->mirror()) { + if (_up) { + noteX = StemLayout::stemPosX(chord) - overlapMirror; + } else { + noteX = -note->headBodyWidth() + overlapMirror; + } + } else if (_up) { + noteX = chord->noteHeadWidth() - note->headBodyWidth(); } - } else if (_up) { - noteX = chord->noteHeadWidth() - note->headBodyWidth(); } double ny = (isJianpu ? 0 : note->line() + stepOffset) * stepDistance; diff --git a/src/engraving/tests/note_tests.cpp b/src/engraving/tests/note_tests.cpp index 44b7c37299a2..59b60e9bc238 100644 --- a/src/engraving/tests/note_tests.cpp +++ b/src/engraving/tests/note_tests.cpp @@ -35,6 +35,8 @@ #include "engraving/dom/note.h" #include "engraving/dom/pitchspelling.h" #include "engraving/dom/segment.h" +#include "engraving/dom/staff.h" +#include "engraving/dom/stafftype.h" #include "engraving/dom/tremolosinglechord.h" #include "engraving/editing/editnote.h" @@ -504,6 +506,49 @@ TEST_F(Engraving_NoteTests, tpcDegrees) //QCOMPARE(tpc2degree(Tpc::TPC_B_S, Key::C_S), 7); } +TEST_F(Engraving_NoteTests, jianpuChordNotesAlignHorizontally) +{ + MasterScore* score = ScoreRW::readScore(u"beam_data/jianpuBeam.mscx"); + ASSERT_TRUE(score); + ChordRest* chordRest = chordRestAtTick(score, Fraction(0, 1), 0); + ASSERT_TRUE(chordRest); + ASSERT_TRUE(chordRest->isChord()); + Chord* chord = toChord(chordRest); + ASSERT_TRUE(chord->isJianpuStaff()); + ASSERT_EQ(chord->notes().size(), 1); + + Note* firstNote = chord->notes().front(); + firstNote->setPitch(60); + firstNote->setTpcFromPitch(); + for (int pitch : { 62, 64 }) { + Note* note = Factory::createNote(chord); + note->setPitch(pitch); + note->setTpcFromPitch(); + chord->add(note); + } + + for (DirectionV direction : { DirectionV::UP, DirectionV::DOWN }) { + chord->setStemDirection(direction); + score->doLayout(); + for (const Note* note : chord->notes()) { + EXPECT_DOUBLE_EQ(note->x(), firstNote->x()); + if (note != firstNote) { + EXPECT_LT(note->y(), firstNote->y()); + } + } + } + + chord->staff()->setStaffType(Fraction(0, 1), *StaffType::preset(StaffTypes::STANDARD)); + ASSERT_FALSE(chord->isJianpuStaff()); + for (DirectionV direction : { DirectionV::UP, DirectionV::DOWN }) { + chord->setStemDirection(direction); + score->doLayout(); + EXPECT_NE(chord->notes()[0]->x(), chord->notes()[1]->x()); + } + + delete score; +} + TEST_F(Engraving_NoteTests, jianpuOctave) { EXPECT_EQ(pitch2JianpuOctave(36, Tpc::TPC_C, Key::C, KeyMode::MAJOR), -1); From 536ca5b1af3d1ace001008533186d3b4dfe7f630 Mon Sep 17 00:00:00 2001 From: songzhenk Date: Thu, 29 Oct 2026 22:06:57 +1100 Subject: [PATCH 24/24] =?UTF-8?q?duration=20lines=20contribute=20to=20the?= =?UTF-8?q?=20chord=E2=80=99s=20spacing=20shape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/engraving/rendering/score/chordlayout.cpp | 6 +++- src/engraving/tests/note_tests.cpp | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index 2b4deea62dc2..3373e50635bf 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -1960,12 +1960,12 @@ void ChordLayout::layoutVoiceOffsets(System* system, staff_idx_t staffIdx, Layou // chord bbox is derived from its shape; slurPos() reads it later fillShape(grace, grace->mutldata()); } - fillShape(chord, chord->mutldata()); // duration lines are positioned relative to the chord, not the note, so they // don't follow the note shift above and need to be moved explicitly for (DurationLine* dl : chord->durationLines()) { dl->mutldata()->moveY(dy); } + fillShape(chord, chord->mutldata()); } else if (e->isRest()) { Rest* rest = toRest(e); rest->mutldata()->moveY(dy); @@ -3960,6 +3960,10 @@ void ChordLayout::fillShape(const Chord* item, ChordRest::LayoutData* ldata) shape.add(l->shape().translate(l->pos())); } + for (const DurationLine* line : item->durationLines()) { + shape.add(line->shape().translate(line->pos())); + } + if (beamlet && stem) { double xPos = beamlet->line.p1().x() - stem->ldata()->pos().x(); if (beamlet->isBefore && !item->up()) { diff --git a/src/engraving/tests/note_tests.cpp b/src/engraving/tests/note_tests.cpp index 59b60e9bc238..42ff32225e04 100644 --- a/src/engraving/tests/note_tests.cpp +++ b/src/engraving/tests/note_tests.cpp @@ -27,6 +27,7 @@ #include "engraving/dom/articulation.h" #include "engraving/dom/chord.h" #include "engraving/dom/chordrest.h" +#include "engraving/dom/durationline.h" #include "engraving/dom/factory.h" #include "engraving/dom/lyrics.h" #include "engraving/dom/masterscore.h" @@ -549,6 +550,39 @@ TEST_F(Engraving_NoteTests, jianpuChordNotesAlignHorizontally) delete score; } +TEST_F(Engraving_NoteTests, jianpuDurationLinesReserveSpace) +{ + MasterScore* score = ScoreRW::readScore(u"beam_data/jianpuBeam.mscx"); + ASSERT_TRUE(score); + ChordRest* chordRest = chordRestAtTick(score, Fraction(0, 1), 0); + ASSERT_TRUE(chordRest); + ASSERT_TRUE(chordRest->isChord()); + Chord* chord = toChord(chordRest); + + for (DurationType duration : { DurationType::V_HALF, DurationType::V_WHOLE, DurationType::V_BREVE }) { + chord->setDurationType(TDuration(duration)); + score->doLayout(); + ASSERT_FALSE(chord->durationLines().empty()); + for (const DurationLine* line : chord->durationLines()) { + const double right = line->pos().x() + line->ldata()->bbox().right(); + const Shape chordShape = chord->shape(); + EXPECT_GE(chordShape.right(), right); + EXPECT_GE(chord->segment()->staffShape(chord->vStaffIdx()).right(), chord->pos().x() + right); + + bool found = false; + for (const ShapeElement& element : chordShape.elements()) { + if (element.item() == line) { + found = true; + break; + } + } + EXPECT_TRUE(found); + } + } + + delete score; +} + TEST_F(Engraving_NoteTests, jianpuOctave) { EXPECT_EQ(pitch2JianpuOctave(36, Tpc::TPC_C, Key::C, KeyMode::MAJOR), -1);