From 5fe0911757652c2dd992ac82f6bacc699fb77053 Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Wed, 29 Jul 2026 06:22:13 -0500 Subject: [PATCH] Add fields for `` and ` ` to `MeasureData`. --- src/include/mx/api/MeasureData.h | 16 ++++ src/private/mx/impl/MeasureReader.cpp | 11 +++ src/private/mx/impl/MeasureWriter.cpp | 13 +++ src/private/mxtest/api/MeasureDataTest.cpp | 106 +++++++++++++++++++++ 4 files changed, 146 insertions(+) diff --git a/src/include/mx/api/MeasureData.h b/src/include/mx/api/MeasureData.h index c7b029f2b..8a6cde682 100644 --- a/src/include/mx/api/MeasureData.h +++ b/src/include/mx/api/MeasureData.h @@ -13,6 +13,7 @@ #include "mx/api/TransposeData.h" #include +#include #include namespace mx @@ -51,6 +52,15 @@ class MeasureData // about measure numbering can be defined using the measure-numbering element. std::string number; + // The measure number as it should appear on the page, when that differs from the 'number' + // above. 'number' identifies the measure (and aligns it with the same measure in other parts), + // while this is purely what the engraver prints. Set it when the printed numbering does not + // follow the identifying numbering -- a second ending that restarts at 8, an editor's numbering + // like "12a", or a rehearsal-driven relabeling. Leave it absent and the printed number is the + // 'number' value. An empty string is not written; use implicit = Bool::yes to suppress the + // number entirely, which is also what makes engravers ignore this field. + std::optional displayedNumber; + // The measure-numbering-value type describes how measure numbers are displayed on this part: // no numbers, numbers every measure, or numbers every system. MeasureNumbering measureNumbering; @@ -65,6 +75,10 @@ class MeasureData // The attribute; see SystemRelation. SystemRelation measureNumberingSystemRelation; + // Which staff of the part the measure number is vertically positioned against, zero-based from + // the top staff. Meaningful only when measureNumbering != unspecified. Absent means the top staff. + std::optional measureNumberingStaffIndex; + // a number greater than zero indicates that this measure is the beginning of a mult-measure // rest that will last for the indicated number of measures. following measures will be affected // by this. @@ -112,10 +126,12 @@ MXAPI_EQUALS_MEMBER(staves) MXAPI_EQUALS_MEMBER(timeSignature) MXAPI_EQUALS_MEMBER(staffTimeSignatures) MXAPI_EQUALS_MEMBER(number) +MXAPI_EQUALS_MEMBER(displayedNumber) MXAPI_EQUALS_MEMBER(measureNumbering) MXAPI_EQUALS_MEMBER(measureNumberingMultipleRestAlways) MXAPI_EQUALS_MEMBER(measureNumberingMultipleRestRange) MXAPI_EQUALS_MEMBER(measureNumberingSystemRelation) +MXAPI_EQUALS_MEMBER(measureNumberingStaffIndex) MXAPI_EQUALS_MEMBER(multiMeasureRest) MXAPI_EQUALS_MEMBER(multiMeasureRestUseSymbols) MXAPI_EQUALS_MEMBER(implicit) diff --git a/src/private/mx/impl/MeasureReader.cpp b/src/private/mx/impl/MeasureReader.cpp index f6fa4f001..2041cd105 100644 --- a/src/private/mx/impl/MeasureReader.cpp +++ b/src/private/mx/impl/MeasureReader.cpp @@ -151,6 +151,11 @@ std::pair> MeasureReader::ge myOutMeasureData.number = ""; } + if (myPartwiseMeasure.text().has_value()) + { + myOutMeasureData.displayedNumber = myPartwiseMeasure.text()->value(); + } + if (myPartwiseMeasure.width().has_value()) { myOutMeasureData.width = static_cast(myPartwiseMeasure.width()->value().value()); @@ -883,6 +888,12 @@ void MeasureReader::parsePrint(const core::Print &inMxPrint) const myOutMeasureData.measureNumberingSystemRelation = myConverter.convertSystemRelation(*measureNumbering.system()); } + + if (measureNumbering.staff().has_value()) + { + // core staff numbers are one-based; the api index is zero-based. + myOutMeasureData.measureNumberingStaffIndex = measureNumbering.staff()->value() - 1; + } } } diff --git a/src/private/mx/impl/MeasureWriter.cpp b/src/private/mx/impl/MeasureWriter.cpp index d463896f0..0f651348f 100644 --- a/src/private/mx/impl/MeasureWriter.cpp +++ b/src/private/mx/impl/MeasureWriter.cpp @@ -16,6 +16,7 @@ #include "mx/core/generated/LeftRightMarginsGroup.h" #include "mx/core/generated/MarginType.h" #include "mx/core/generated/MeasureNumbering.h" +#include "mx/core/generated/MeasureText.h" #include "mx/core/generated/MusicDataChoice.h" #include "mx/core/generated/PageLayout.h" #include "mx/core/generated/PageLayoutGroup.h" @@ -88,6 +89,12 @@ void MeasureWriter::writeMeasureGlobals() myOutMeasure.setNumber(std::to_string(myHistory.getCursor().measureIndex + 1)); } + // is not legal MusicXML; an empty displayedNumber means "no override". + if (myMeasureData.displayedNumber.has_value() && !myMeasureData.displayedNumber->empty()) + { + myOutMeasure.setText(core::MeasureText{*myMeasureData.displayedNumber}); + } + if (myMeasureData.width >= 0.0) { myOutMeasure.setWidth(core::Tenths{core::Decimal{static_cast(myMeasureData.width)}}); @@ -453,6 +460,12 @@ void MeasureWriter::writeMeasureNumbering() outMeasureNumbering.setSystem(myConverter.convertSystemRelation(myMeasureData.measureNumberingSystemRelation)); } + if (myMeasureData.measureNumberingStaffIndex.has_value()) + { + // the api index is zero-based; core staff numbers are one-based. + outMeasureNumbering.setStaff(core::StaffNumber{*myMeasureData.measureNumberingStaffIndex + 1}); + } + outPrint.setMeasureNumbering(std::move(outMeasureNumbering)); if (printIndex >= 0) diff --git a/src/private/mxtest/api/MeasureDataTest.cpp b/src/private/mxtest/api/MeasureDataTest.cpp index 630d54d00..c1444cd9f 100644 --- a/src/private/mxtest/api/MeasureDataTest.cpp +++ b/src/private/mxtest/api/MeasureDataTest.cpp @@ -332,6 +332,112 @@ TEST(measureNumberingUnspecifiedOmitsElement, MeasureData) T_END; +TEST(measureNumberingStaffRoundTrip, 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.measureNumberingStaffIndex = 1; + measure.staves.emplace_back(); + measure.staves.back().voices[0].notes.emplace_back(); + measure.staves.emplace_back(); + measure.staves.back().voices[0].notes.emplace_back(); + + const auto xml = mxtest::toXml(score); + CHECK(xml.find("staff=\"2\"") != std::string::npos); + + const auto outScore = mxtest::fromXml(xml); + const auto &outMeasure = outScore.parts.front().measures.front(); + REQUIRE(outMeasure.measureNumberingStaffIndex.has_value()); + CHECK_EQUAL(1, *outMeasure.measureNumberingStaffIndex); +} + +T_END; + +TEST(measureNumberingStaffAbsentOmitsAttribute, 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("