diff --git a/src/include/mx/api/SystemLayoutData.h b/src/include/mx/api/SystemLayoutData.h index 8d4801b99..167bf9603 100644 --- a/src/include/mx/api/SystemLayoutData.h +++ b/src/include/mx/api/SystemLayoutData.h @@ -16,6 +16,29 @@ namespace mx { namespace api { +/// A scoped to one staff of a multi-staff part (MusicXML's number attribute): +/// the space between the bottom line of the previous staff and the top line of this one. +/// staffIndex is zero-based; a source with an explicit number attribute always populates +/// SystemLayoutData::staffDistances with one of these rather than the unscoped +/// SystemLayoutData::staffDistance, so the number round-trips faithfully. +class StaffDistanceData +{ + public: + int staffIndex; + double staffDistance; + + explicit inline StaffDistanceData(int inStaffIndex = INDEX_UNSPECIFIED, double inStaffDistance = DOUBLE_UNSPECIFIED) + : staffIndex(inStaffIndex), staffDistance(inStaffDistance) + { + } +}; + +MXAPI_EQUALS_BEGIN(StaffDistanceData) +MXAPI_EQUALS_MEMBER(staffIndex) +MXAPI_DOUBLES_EQUALS_MEMBER(staffDistance) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(StaffDistanceData); + /// Margins and spacing for staff systems. class SystemLayoutData { @@ -29,13 +52,19 @@ class SystemLayoutData /// Distance from the top margin of the page to the top line of the first staff on the page, in tenths. OptionalDouble topSystemDistance; - /// the space between staves within the same system, in tenths. + /// the space between staves within the same system, in tenths. Only populated when the source's + /// had no number attribute (the common single-staff-distance case); a source with + /// an explicit number populates staffDistances instead. See StaffDistanceData. OptionalDouble staffDistance; + /// Per-staff overrides of staffDistance for parts with more than one staff-scoped + /// (e.g. an organ's third staff needing its own spacing). Normally empty. + std::vector staffDistances; + /// Returns true if any of the members have values. inline bool isUsed() const { - return margins || systemDistance || topSystemDistance || staffDistance; + return margins || systemDistance || topSystemDistance || staffDistance || !staffDistances.empty(); } explicit inline SystemLayoutData(std::optional inMargins = std::nullopt, @@ -43,7 +72,7 @@ class SystemLayoutData OptionalDouble inTopSystemDistance = std::nullopt, OptionalDouble inStaffDistance = std::nullopt) : margins(inMargins), systemDistance(inSystemDistance), topSystemDistance{inTopSystemDistance}, - staffDistance(inStaffDistance) + staffDistance(inStaffDistance), staffDistances{} { } }; @@ -53,6 +82,7 @@ MXAPI_EQUALS_MEMBER(margins) MXAPI_EQUALS_MEMBER(systemDistance) MXAPI_EQUALS_MEMBER(topSystemDistance) MXAPI_EQUALS_MEMBER(staffDistance) +MXAPI_EQUALS_MEMBER(staffDistances) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(SystemLayoutData); } // namespace api diff --git a/src/private/mx/impl/LayoutFunctions.cpp b/src/private/mx/impl/LayoutFunctions.cpp index a497e1ae3..0072d0169 100644 --- a/src/private/mx/impl/LayoutFunctions.cpp +++ b/src/private/mx/impl/LayoutFunctions.cpp @@ -236,6 +236,15 @@ void addSystemMargins(const api::DefaultsData &inDefaults, core::ScoreHeaderGrou needsDefaults = true; } + for (const auto &staffDistance : inDefaults.systemLayout.staffDistances) + { + core::StaffLayout staffLayout; + staffLayout.setNumber(core::StaffNumber{staffDistance.staffIndex + 1}); + staffLayout.setStaffDistance(toTenths(staffDistance.staffDistance)); + layout.addStaffLayout(staffLayout); + needsDefaults = true; + } + if (needsDefaults) { layout.setSystemLayout(systemLayout); @@ -427,15 +436,24 @@ void addStaffLayout(const core::ScoreHeaderGroup &inScoreHeaderGroup, api::Defau return; } - const auto &staffLayouts = inScoreHeaderGroup.defaults()->layout().staffLayout(); - if (staffLayouts.empty()) - { - return; - } - - if (staffLayouts.front().staffDistance().has_value()) + // A source with an explicit number attribute is staff-scoped (api::StaffDistanceData); one + // with no number applies to staff 1 by schema default and is captured as the plain + // unscoped value. Mirrors ScoreReader::scanForSystemInfo's per-measure handling. + for (const auto &staffLayout : inScoreHeaderGroup.defaults()->layout().staffLayout()) { - outDefaults.systemLayout.staffDistance = staffLayouts.front().staffDistance()->value().value(); + if (!staffLayout.staffDistance().has_value()) + { + continue; + } + const double distance = staffLayout.staffDistance()->value().value(); + if (staffLayout.number().has_value()) + { + outDefaults.systemLayout.staffDistances.emplace_back(staffLayout.number()->value() - 1, distance); + } + else + { + outDefaults.systemLayout.staffDistance = distance; + } } } diff --git a/src/private/mx/impl/MeasureWriter.cpp b/src/private/mx/impl/MeasureWriter.cpp index a9b548a2b..177dfc8d8 100644 --- a/src/private/mx/impl/MeasureWriter.cpp +++ b/src/private/mx/impl/MeasureWriter.cpp @@ -26,6 +26,7 @@ #include "mx/core/generated/Repeat.h" #include "mx/core/generated/RightLeftMiddle.h" #include "mx/core/generated/StaffLayout.h" +#include "mx/core/generated/StaffNumber.h" #include "mx/core/generated/SystemLayout.h" #include "mx/core/generated/SystemMargins.h" #include "mx/core/generated/Tenths.h" @@ -255,6 +256,15 @@ void MeasureWriter::writeSystemInfo() outLayoutGroup.addStaffLayout(outStaffLayout); outPrint.setLayout(outLayoutGroup); } + + for (const auto &staffDistance : inSystemLayout.staffDistances) + { + core::StaffLayout outStaffLayout{}; + outStaffLayout.setNumber(core::StaffNumber{staffDistance.staffIndex + 1}); + outStaffLayout.setStaffDistance(core::Tenths{core::Decimal{staffDistance.staffDistance}}); + outLayoutGroup.addStaffLayout(outStaffLayout); + outPrint.setLayout(outLayoutGroup); + } } myOutMeasure.addMusicData(core::MusicDataChoice::print(outPrint)); diff --git a/src/private/mx/impl/ScoreReader.cpp b/src/private/mx/impl/ScoreReader.cpp index 6873d05c7..b772f1b84 100644 --- a/src/private/mx/impl/ScoreReader.cpp +++ b/src/private/mx/impl/ScoreReader.cpp @@ -466,15 +466,23 @@ void ScoreReader::scanForSystemInfo() const } } - // Per-measure staff-distance. (We model a single - // staff-distance, matching how the defaults staff-layout is - // mapped; the first staff-layout's distance is used.) + // Per-measure staff-distance. A source with an explicit number + // attribute is staff-scoped (api::StaffDistanceData); one with no number applies + // to staff 1 by schema default and is captured as the plain unscoped value. for (const auto &staffLayout : layoutGroup.staffLayout()) { - if (staffLayout.staffDistance().has_value()) + if (!staffLayout.staffDistance().has_value()) { - systemData.layout.staffDistance = staffLayout.staffDistance()->value().value(); - break; + continue; + } + const double distance = staffLayout.staffDistance()->value().value(); + if (staffLayout.number().has_value()) + { + systemData.layout.staffDistances.emplace_back(staffLayout.number()->value() - 1, distance); + } + else + { + systemData.layout.staffDistance = distance; } } diff --git a/src/private/mxtest/api/PrintLayoutRoundTripTest.cpp b/src/private/mxtest/api/PrintLayoutRoundTripTest.cpp index dee526883..691b5f8ec 100644 --- a/src/private/mxtest/api/PrintLayoutRoundTripTest.cpp +++ b/src/private/mxtest/api/PrintLayoutRoundTripTest.cpp @@ -71,4 +71,31 @@ TEST(printLayoutRoundTrip, perMeasureStaffDistance) CHECK_EQUAL(120.0, sysOut.layout.systemDistance.value()); } +// A is scoped to one staff of a multi-staff part; the writer +// only ever emitted an unscoped (matching SystemLayoutData::staffDistance), +// and the reader dropped the number attribute on the one it read back, silently +// misattributing the distance to staff 1. Verify a staff-scoped distance survives via +// SystemLayoutData::staffDistances. +TEST(printLayoutRoundTrip, staffScopedStaffDistance) +{ + auto in = makeScoreWithMeasures(1); + auto &staff0 = in.parts.front().measures.front().staves.front(); + StaffData staff1 = staff0; + in.parts.front().measures.front().staves.push_back(staff1); + + SystemData sys{}; + sys.layout.staffDistances.emplace_back(1, 65.0); + in.layout.emplace(0, LayoutData{sys, PageData{}}); + + const auto out = mxtest::roundTrip(in); + + const auto it = out.layout.find(0); + REQUIRE(it != out.layout.end()); + const auto &sysOut = it->second.system; + CHECK(!static_cast(sysOut.layout.staffDistance)); + REQUIRE(1 == sysOut.layout.staffDistances.size()); + CHECK_EQUAL(1, sysOut.layout.staffDistances.front().staffIndex); + CHECK_EQUAL(65.0, sysOut.layout.staffDistances.front().staffDistance); +} + #endif