From 1e1b8e1aa2cb004b2d41a085f391b6c0ac6b0368 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Tue, 7 Jul 2026 06:14:52 +0000 Subject: [PATCH] fix: wire up measure-numbering read/write in mx::api MeasureData::measureNumbering and Converter::convertMeasureNumbering already existed, but neither MeasureReader nor MeasureWriter ever called the converter, so a source's (inside a measure's ) was silently dropped on round-trip even though the api audits the element as fully supported. MeasureReader::parsePrint now reads it directly into MeasureData (it is measure-scoped, presence-based state, not carried forward like time/key). MeasureWriter gains writeMeasureNumbering(), which finds-or-creates the measure's element the same way writePageInfo() already does, so it shares a single with any system/page layout already written for that measure. Closes #279 --- src/private/mx/impl/MeasureReader.cpp | 11 ++-- src/private/mx/impl/MeasureWriter.cpp | 51 +++++++++++++++++++ src/private/mx/impl/MeasureWriter.h | 1 + src/private/mxtest/api/MeasureDataTest.cpp | 41 +++++++++++++++ src/private/mxtest/api/roundtrip-baseline.txt | 5 ++ 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/private/mx/impl/MeasureReader.cpp b/src/private/mx/impl/MeasureReader.cpp index 852315c77..64ea2d3b9 100644 --- a/src/private/mx/impl/MeasureReader.cpp +++ b/src/private/mx/impl/MeasureReader.cpp @@ -815,10 +815,13 @@ void MeasureReader::parsePrint(const core::Print &inMxPrint) const // Per-measure layout is read at the score level, keyed by // measure index, in ScoreReader::scanForSystemInfo and // ScoreReader::scanForPageInfo (which capture new-system, new-page, - // page-number, system-layout, staff-layout, and page-layout). The - // per-measure music-data hook has no api home of its own, so nothing - // is captured here. - MX_UNUSED(inMxPrint); + // page-number, system-layout, staff-layout, and page-layout). + // measure-numbering is measure-scoped api state (MeasureData::measureNumbering, + // not carried forward like time/key), so it is captured directly here instead. + if (inMxPrint.measureNumbering().has_value()) + { + myOutMeasureData.measureNumbering = myConverter.convertMeasureNumbering(inMxPrint.measureNumbering()->value()); + } } void MeasureReader::parseSound(const core::Sound &inMxSound) const diff --git a/src/private/mx/impl/MeasureWriter.cpp b/src/private/mx/impl/MeasureWriter.cpp index 773474c6a..a9b548a2b 100644 --- a/src/private/mx/impl/MeasureWriter.cpp +++ b/src/private/mx/impl/MeasureWriter.cpp @@ -15,6 +15,7 @@ #include "mx/core/generated/LayoutGroup.h" #include "mx/core/generated/LeftRightMarginsGroup.h" #include "mx/core/generated/MarginType.h" +#include "mx/core/generated/MeasureNumbering.h" #include "mx/core/generated/MusicDataChoice.h" #include "mx/core/generated/PageLayout.h" #include "mx/core/generated/PageLayoutGroup.h" @@ -108,6 +109,11 @@ void MeasureWriter::writeMeasureGlobals() writePageInfo(*pageData); } + if (myMeasureData.measureNumbering != api::MeasureNumbering::unspecified) + { + writeMeasureNumbering(); + } + if (myHistory.getCursor().isFirstMeasureInPart) { myPropertiesWriter->writeDivisions(myHistory.getCursor().getGlobalTicksPerQuarter()); @@ -375,6 +381,51 @@ void MeasureWriter::writePageInfo(const api::PageData &inPageData) } } +void MeasureWriter::writeMeasureNumbering() +{ + // Same find-or-create merge pattern as writePageInfo(): measure-numbering shares + // the element with any system/page layout already written for this measure. + const auto existingData = myOutMeasure.musicData(); + int printIndex = -1; + int idx = 0; + for (const auto &mdc : existingData) + { + if (mdc.isPrint()) + { + printIndex = idx; + break; + } + ++idx; + } + + core::Print outPrint{}; + if (printIndex >= 0) + { + outPrint = existingData[printIndex].asPrint(); + } + + core::MeasureNumbering outMeasureNumbering{}; + outMeasureNumbering.setValue(myConverter.convertMeasureNumbering(myMeasureData.measureNumbering)); + outPrint.setMeasureNumbering(std::move(outMeasureNumbering)); + + if (printIndex >= 0) + { + std::vector newData; + newData.reserve(existingData.size()); + int i = 0; + for (const auto &mdc : existingData) + { + newData.push_back(i == printIndex ? core::MusicDataChoice::print(outPrint) : mdc); + ++i; + } + myOutMeasure.setMusicData(std::move(newData)); + } + else + { + myOutMeasure.addMusicData(core::MusicDataChoice::print(outPrint)); + } +} + void MeasureWriter::writeStaves() { myHistory.resetCursorFofStaffIterations(); diff --git a/src/private/mx/impl/MeasureWriter.h b/src/private/mx/impl/MeasureWriter.h index d0e9b13e1..8276c35b3 100644 --- a/src/private/mx/impl/MeasureWriter.h +++ b/src/private/mx/impl/MeasureWriter.h @@ -207,6 +207,7 @@ class MeasureWriter void writeMeasureGlobals(); void writeSystemInfo(); void writePageInfo(const api::PageData &inNewPageData); + void writeMeasureNumbering(); void writeStaves(); void writeVoices(const api::StaffData &inStaff); void writeForwardOrBackupIfNeeded(const api::NoteData ¤tNote); diff --git a/src/private/mxtest/api/MeasureDataTest.cpp b/src/private/mxtest/api/MeasureDataTest.cpp index 81ac77401..b9b63d6f6 100644 --- a/src/private/mxtest/api/MeasureDataTest.cpp +++ b/src/private/mxtest/api/MeasureDataTest.cpp @@ -262,4 +262,45 @@ TEST(staffLinesAndStaffSizeRoundTrip, MeasureData) T_END; +TEST(measureNumberingRoundTrip, MeasureData) +{ + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.measureNumbering = MeasureNumbering::system; + measure.staves.emplace_back(); + measure.staves.back().voices[0].notes.emplace_back(); + + const auto xml = mxtest::toXml(score); + CHECK(xml.find("system") != std::string::npos); + + const auto outScore = mxtest::fromXml(xml); + CHECK_EQUAL(1, outScore.parts.size()); + CHECK_EQUAL(1, outScore.parts.front().measures.size()); + CHECK(MeasureNumbering::system == outScore.parts.front().measures.front().measureNumbering); +} + +T_END; + +TEST(measureNumberingUnspecifiedOmitsElement, MeasureData) +{ + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + measure.staves.back().voices[0].notes.emplace_back(); + + const auto xml = mxtest::toXml(score); + CHECK(xml.find("") == std::string::npos); + + const auto outScore = mxtest::fromXml(xml); + CHECK(MeasureNumbering::unspecified == outScore.parts.front().measures.front().measureNumbering); +} + +T_END; + #endif diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 5c4cc3652..c4b19619e 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -293,3 +293,8 @@ lysuite/ly01a_Pitches_Pitches.xml lysuite/ly01e_Pitches_ParenthesizedAccidentals.xml lysuite/ly01f_Pitches_ParenthesizedMicrotoneAccidentals.xml musuite/testAccidentals2.xml + +# Unblocked by wiring MeasureData::measureNumbering through MeasureReader/ +# MeasureWriter: the field and its Converter existed but neither side ever +# called it, so was silently dropped on round-trip. +ksuite/k015a_System_Layout.xml