diff --git a/src/include/mx/api/SystemLayoutData.h b/src/include/mx/api/SystemLayoutData.h index 8d4801b99..3594967c0 100644 --- a/src/include/mx/api/SystemLayoutData.h +++ b/src/include/mx/api/SystemLayoutData.h @@ -9,6 +9,7 @@ #include "mx/api/LeftRight.h" #include "mx/api/PageLayoutData.h" +#include #include #include @@ -29,13 +30,21 @@ 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. Applies to every staff of a part + /// that does not have its own entry in staffDistances. OptionalDouble staffDistance; + /// Per-staff spacing for multi-staff parts, keyed by zero-based staff index: the space between + /// the bottom line of the previous staff and the top line of the keyed staff, in tenths. + /// Normally empty; only add an entry when one staff needs its own spacing, e.g. extra room + /// above an organ's pedal staff. For a given staff, an entry here wins; otherwise + /// staffDistance applies. + std::map 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 +52,7 @@ class SystemLayoutData OptionalDouble inTopSystemDistance = std::nullopt, OptionalDouble inStaffDistance = std::nullopt) : margins(inMargins), systemDistance(inSystemDistance), topSystemDistance{inTopSystemDistance}, - staffDistance(inStaffDistance) + staffDistance(inStaffDistance), staffDistances{} { } }; @@ -53,6 +62,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..9f8200bd4 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 &[staffIndex, staffDistance] : inDefaults.systemLayout.staffDistances) + { + core::StaffLayout staffLayout; + staffLayout.setNumber(core::StaffNumber{staffIndex + 1}); + staffLayout.setStaffDistance(toTenths(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 (a staffDistances map entry); + // 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[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 bbc5c885f..dd2164133 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" @@ -263,6 +264,15 @@ void MeasureWriter::writeSystemInfo() outLayoutGroup.addStaffLayout(outStaffLayout); outPrint.setLayout(outLayoutGroup); } + + for (const auto &[staffIndex, staffDistance] : inSystemLayout.staffDistances) + { + core::StaffLayout outStaffLayout{}; + outStaffLayout.setNumber(core::StaffNumber{staffIndex + 1}); + outStaffLayout.setStaffDistance(core::Tenths{core::Decimal{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..4dad7d8df 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 (a staffDistances map entry); 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[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..0ff7f4b9b 100644 --- a/src/private/mxtest/api/PrintLayoutRoundTripTest.cpp +++ b/src/private/mxtest/api/PrintLayoutRoundTripTest.cpp @@ -71,4 +71,32 @@ 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[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)); + CHECK_EQUAL(1, static_cast(sysOut.layout.staffDistances.size())); + const auto sdIt = sysOut.layout.staffDistances.find(1); + REQUIRE(sdIt != sysOut.layout.staffDistances.end()); + CHECK_EQUAL(65.0, sdIt->second); +} + #endif