Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/include/mx/api/SystemLayoutData.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ namespace mx
{
namespace api
{
/// A <staff-layout> 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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A source? Meaning a file?

Since the coding agent has been tasked with round trip success, it is writing API documentation from that vantage-point which would be confusing to a user. Instead this documentation should explain how someone using mx::api to build up a score should use the interface. And the interface should avoid adding confusing things that exist solely to represent what is not music-notation-semantics-meaningful in a round-trip corpus test file.

/// 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
{
Expand All @@ -29,21 +52,27 @@ 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
/// <staff-layout> 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 <staff-layout>
/// (e.g. an organ's third staff needing its own spacing). Normally empty.
std::vector<StaffDistanceData> 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<LeftRight> inMargins = std::nullopt,
OptionalDouble inSystemDistance = std::nullopt,
OptionalDouble inTopSystemDistance = std::nullopt,
OptionalDouble inStaffDistance = std::nullopt)
: margins(inMargins), systemDistance(inSystemDistance), topSystemDistance{inTopSystemDistance},
staffDistance(inStaffDistance)
staffDistance(inStaffDistance), staffDistances{}
{
}
};
Expand All @@ -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
Expand Down
34 changes: 26 additions & 8 deletions src/private/mx/impl/LayoutFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/private/mx/impl/MeasureWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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));
Expand Down
20 changes: 14 additions & 6 deletions src/private/mx/impl/ScoreReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,23 @@ void ScoreReader::scanForSystemInfo() const
}
}

// Per-measure <staff-layout> 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-layout> 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;
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/private/mxtest/api/PrintLayoutRoundTripTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,31 @@ TEST(printLayoutRoundTrip, perMeasureStaffDistance)
CHECK_EQUAL(120.0, sysOut.layout.systemDistance.value());
}

// A <staff-layout number="N"> is scoped to one staff of a multi-staff part; the writer
// only ever emitted an unscoped <staff-layout> (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<bool>(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
Loading