diff --git a/src/include/mx/api/AccordionRegistrationData.h b/src/include/mx/api/AccordionRegistrationData.h new file mode 100644 index 000000000..39a1be178 --- /dev/null +++ b/src/include/mx/api/AccordionRegistrationData.h @@ -0,0 +1,52 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// An accordion registration symbol, MusicXML's element: the circular +// three-section diagram telling the player which reed ranks to engage. high engages the high +// (4') section, drawn as a dot in the top section; middle engages the middle (8') section with +// 1 to 3 dots; low engages the low (16') section, drawn as a dot in the bottom section. An +// absent middle means no middle-section dots. A registration with nothing engaged is legal and +// draws the empty diagram. +class AccordionRegistrationData +{ + public: + bool high; + std::optional middle; + bool low; + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + AccordionRegistrationData() : high{false}, middle{}, low{false}, positionData{}, fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(AccordionRegistrationData) +MXAPI_EQUALS_MEMBER(high) +MXAPI_EQUALS_MEMBER(middle) +MXAPI_EQUALS_MEMBER(low) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(AccordionRegistrationData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/DirectionData.h b/src/include/mx/api/DirectionData.h index d37510676..eca6d6dde 100644 --- a/src/include/mx/api/DirectionData.h +++ b/src/include/mx/api/DirectionData.h @@ -4,14 +4,18 @@ #pragma once +#include "mx/api/AccordionRegistrationData.h" #include "mx/api/ApiCommon.h" #include "mx/api/ChordData.h" #include "mx/api/CodaData.h" #include "mx/api/DampData.h" #include "mx/api/EyeglassesData.h" #include "mx/api/FiguredBassData.h" +#include "mx/api/ImageData.h" #include "mx/api/MarkData.h" +#include "mx/api/OtherDirectionData.h" #include "mx/api/OttavaData.h" +#include "mx/api/PrincipalVoiceData.h" #include "mx/api/RehearsalData.h" #include "mx/api/SegnoData.h" #include "mx/api/SoundData.h" @@ -48,7 +52,11 @@ enum class DirectionComponentKind dampAll, eyeglasses, stringMute, - staffDivide + staffDivide, + principalVoice, + otherDirection, + image, + accordionRegistration }; struct DirectionComponent @@ -134,6 +142,10 @@ struct DirectionData std::vector eyeglasses; std::vector stringMutes; std::vector staffDivides; + std::vector principalVoices; + std::vector otherDirections; + std::vector images; + std::vector accordionRegistrations; // Preserves the original order of direction-type children from parsed XML // for round-trip fidelity. Do NOT populate this when constructing // DirectionData programmatically. If empty, the writer uses a default @@ -163,6 +175,8 @@ inline bool isDirectionDataEmpty(const DirectionData &directionData) directionData.rehearsals.size() == 0 && directionData.damps.size() == 0 && directionData.dampAlls.size() == 0 && directionData.eyeglasses.size() == 0 && directionData.stringMutes.size() == 0 && directionData.staffDivides.size() == 0 && + directionData.principalVoices.size() == 0 && directionData.otherDirections.size() == 0 && + directionData.images.size() == 0 && directionData.accordionRegistrations.size() == 0 && directionData.figuredBasses.size() == 0 && !directionData.isSoundDataSpecified && directionData.orderedComponents.size() == 0; } @@ -197,6 +211,10 @@ MXAPI_EQUALS_MEMBER(dampAlls) MXAPI_EQUALS_MEMBER(eyeglasses) MXAPI_EQUALS_MEMBER(stringMutes) MXAPI_EQUALS_MEMBER(staffDivides) +MXAPI_EQUALS_MEMBER(principalVoices) +MXAPI_EQUALS_MEMBER(otherDirections) +MXAPI_EQUALS_MEMBER(images) +MXAPI_EQUALS_MEMBER(accordionRegistrations) MXAPI_EQUALS_MEMBER(orderedComponents) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(DirectionData); diff --git a/src/include/mx/api/ImageData.h b/src/include/mx/api/ImageData.h new file mode 100644 index 000000000..0a9a1cb07 --- /dev/null +++ b/src/include/mx/api/ImageData.h @@ -0,0 +1,49 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// A graphic placed in the music, MusicXML's direction: source locates the graphic file +// (a URL or file reference) and type gives its MIME type, e.g. "image/png". height and width, +// in tenths, scale the image; give one alone to scale proportionally. positionData captures +// default/relative x-y plus the horizontal and vertical alignment of the image relative to its +// position point; an image's vertical alignment has no baseline variant, so a baseline value +// here is not written. positionData's placement member is unused because has no +// placement attribute (placement lives on the parent ). +class ImageData +{ + public: + std::string source; + std::string type; + std::optional height; + std::optional width; + PositionData positionData; + std::optional id; + + ImageData() : source{}, type{}, height{}, width{}, positionData{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(ImageData) +MXAPI_EQUALS_MEMBER(source) +MXAPI_EQUALS_MEMBER(type) +MXAPI_EQUALS_MEMBER(height) +MXAPI_EQUALS_MEMBER(width) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(ImageData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/OtherDirectionData.h b/src/include/mx/api/OtherDirectionData.h new file mode 100644 index 000000000..e9296ffd7 --- /dev/null +++ b/src/include/mx/api/OtherDirectionData.h @@ -0,0 +1,51 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// A direction that has no dedicated MusicXML element, carried by the +// catch-all. The text is the direction's content; when the direction is best shown as a +// standard SMuFL glyph, smufl names that glyph (a canonical SMuFL glyph name) and the text +// serves as a fallback description. Set printObject to no for a direction that affects +// playback or analysis but should not be drawn. +class OtherDirectionData +{ + public: + std::string text; + Bool printObject; + std::optional smufl; + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + OtherDirectionData() : text{}, printObject{Bool::unspecified}, smufl{}, positionData{}, fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(OtherDirectionData) +MXAPI_EQUALS_MEMBER(text) +MXAPI_EQUALS_MEMBER(printObject) +MXAPI_EQUALS_MEMBER(smufl) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(OtherDirectionData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/PrincipalVoiceData.h b/src/include/mx/api/PrincipalVoiceData.h new file mode 100644 index 000000000..a009124bf --- /dev/null +++ b/src/include/mx/api/PrincipalVoiceData.h @@ -0,0 +1,72 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// Whether a PrincipalVoiceData marks where the designation begins or where it ends. +enum class PrincipalVoiceType +{ + start, + stop +}; + +// How a principal-voice marking is displayed: the Hauptstimme or Nebenstimme bracket symbols +// used in Schoenberg's and Berg's scores, a plain bracket, or no visible symbol at all. +enum class PrincipalVoiceSymbol +{ + hauptstimme, + nebenstimme, + plain, + none +}; + +// A principal-voice designation, MusicXML's element: marks a passage as the +// principal (Hauptstimme) or secondary (Nebenstimme) voice of the texture, a practice from the +// Second Viennese School. The marking spans from a start to the matching stop; the symbol +// chooses what is drawn at the start (the stop is drawn as the end of the bracket). +class PrincipalVoiceData +{ + public: + PrincipalVoiceType type; + PrincipalVoiceSymbol symbol; + + // Optional text content carried by the element, used for analysis rather than display. + std::string text; + + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + PrincipalVoiceData() + : type{PrincipalVoiceType::start}, symbol{PrincipalVoiceSymbol::hauptstimme}, text{}, positionData{}, + fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(PrincipalVoiceData) +MXAPI_EQUALS_MEMBER(type) +MXAPI_EQUALS_MEMBER(symbol) +MXAPI_EQUALS_MEMBER(text) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(PrincipalVoiceData); +} // namespace api +} // namespace mx diff --git a/src/private/mx/impl/DirectionReader.cpp b/src/private/mx/impl/DirectionReader.cpp index c27e5030f..0165424ea 100644 --- a/src/private/mx/impl/DirectionReader.cpp +++ b/src/private/mx/impl/DirectionReader.cpp @@ -48,6 +48,7 @@ #include "mx/core/generated/PedalType.h" #include "mx/core/generated/Percussion.h" #include "mx/core/generated/PrincipalVoice.h" +#include "mx/core/generated/PrincipalVoiceSymbol.h" #include "mx/core/generated/Root.h" #include "mx/core/generated/RootStep.h" #include "mx/core/generated/Scordatura.h" @@ -62,6 +63,7 @@ #include "mx/core/generated/StringMute.h" #include "mx/core/generated/StyleText.h" #include "mx/core/generated/UpDownStopContinue.h" +#include "mx/core/generated/ValignImage.h" #include "mx/core/generated/Wedge.h" #include "mx/core/generated/WedgeType.h" #include "mx/core/generated/YesNo.h" @@ -823,15 +825,14 @@ void DirectionReader::parseOctaveShift(const core::DirectionType &directionType) static_cast(myOutDirectionData.ottavaStarts.size()) - 1); } -// The stubs below (harp-pedals, scordatura, image, principal-voice, accordion-registration, -// percussion, other-direction) are genuinely unmodeled direction-type choices, tracked at -// #324, not a bug in the surrounding dispatch. When a contains only one of these, -// the resulting DirectionData carries no content and is correctly left unwritten -- MusicXML -// requires at least one direction-type child, so there is no schema-valid way to keep the -// (and its /) without modeling what it actually says. That is why -// round-trip discovery reports those files as dropping -// /// together: it is one gap (the unmodeled -// content), not four. +// The stubs below (harp-pedals, scordatura, percussion) are genuinely unmodeled +// direction-type choices, tracked at #324, not a bug in the surrounding dispatch. When a +// contains only one of these, the resulting DirectionData carries no content and +// is correctly left unwritten -- MusicXML requires at least one direction-type child, so +// there is no schema-valid way to keep the (and its /) without +// modeling what it actually says. That is why round-trip discovery reports those files as +// dropping /// together: it is one gap (the +// unmodeled content), not four. void DirectionReader::parseHarpPedals(const core::DirectionType &directionType) { MX_UNUSED(directionType); @@ -953,17 +954,111 @@ void DirectionReader::parseScordatura(const core::DirectionType &directionType) void DirectionReader::parseImage(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &image = directionType.choice().asImage(); + api::ImageData outImage; + outImage.source = image.source(); + outImage.type = image.type(); + if (image.height().has_value()) + { + outImage.height = static_cast(image.height()->value().value()); + } + if (image.width().has_value()) + { + outImage.width = static_cast(image.width()->value().value()); + } + outImage.positionData = getPositionData(image); + // 's valign is the valign-image type (no baseline), which the generic position + // helper cannot read; take it from the element directly. + if (image.valign().has_value()) + { + switch (image.valign()->tag()) + { + case core::ValignImage::Tag::top: + outImage.positionData.verticalAlignment = api::VerticalAlignment::top; + break; + case core::ValignImage::Tag::middle: + outImage.positionData.verticalAlignment = api::VerticalAlignment::middle; + break; + case core::ValignImage::Tag::bottom: + outImage.positionData.verticalAlignment = api::VerticalAlignment::bottom; + break; + default: + break; + } + } + else + { + outImage.positionData.verticalAlignment = api::VerticalAlignment::unspecified; + } + if (image.id().has_value()) + { + outImage.id = image.id()->value(); + } + myOutDirectionData.images.emplace_back(std::move(outImage)); + appendOrderedComponent(api::DirectionComponentKind::image, static_cast(myOutDirectionData.images.size()) - 1); } void DirectionReader::parsePrincipalVoice(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &principalVoice = directionType.choice().asPrincipalVoice(); + api::PrincipalVoiceData outPrincipalVoice; + outPrincipalVoice.type = principalVoice.type().tag() == core::StartStop::Tag::stop ? api::PrincipalVoiceType::stop + : api::PrincipalVoiceType::start; + switch (principalVoice.symbol().tag()) + { + case core::PrincipalVoiceSymbol::Tag::nebenstimme: + outPrincipalVoice.symbol = api::PrincipalVoiceSymbol::nebenstimme; + break; + case core::PrincipalVoiceSymbol::Tag::plain: + outPrincipalVoice.symbol = api::PrincipalVoiceSymbol::plain; + break; + case core::PrincipalVoiceSymbol::Tag::none: + outPrincipalVoice.symbol = api::PrincipalVoiceSymbol::none; + break; + case core::PrincipalVoiceSymbol::Tag::hauptstimme: + default: + outPrincipalVoice.symbol = api::PrincipalVoiceSymbol::hauptstimme; + break; + } + outPrincipalVoice.text = principalVoice.value(); + outPrincipalVoice.positionData = getPositionData(principalVoice); + outPrincipalVoice.fontData = getFontData(principalVoice); + if (principalVoice.color().has_value()) + { + outPrincipalVoice.color = getColor(principalVoice); + } + if (principalVoice.id().has_value()) + { + outPrincipalVoice.id = principalVoice.id()->value(); + } + myOutDirectionData.principalVoices.emplace_back(std::move(outPrincipalVoice)); + appendOrderedComponent(api::DirectionComponentKind::principalVoice, + static_cast(myOutDirectionData.principalVoices.size()) - 1); } void DirectionReader::parseAccordionRegistration(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &accordion = directionType.choice().asAccordionRegistration(); + api::AccordionRegistrationData outAccordion; + outAccordion.high = accordion.accordionHigh(); + if (accordion.accordionMiddle().has_value()) + { + outAccordion.middle = accordion.accordionMiddle()->value(); + } + outAccordion.low = accordion.accordionLow(); + outAccordion.positionData = getPositionData(accordion); + outAccordion.fontData = getFontData(accordion); + if (accordion.color().has_value()) + { + outAccordion.color = getColor(accordion); + } + if (accordion.id().has_value()) + { + outAccordion.id = accordion.id()->value(); + } + myOutDirectionData.accordionRegistrations.emplace_back(std::move(outAccordion)); + appendOrderedComponent(api::DirectionComponentKind::accordionRegistration, + static_cast(myOutDirectionData.accordionRegistrations.size()) - 1); } void DirectionReader::parsePercussion(const core::DirectionType &directionType) @@ -973,7 +1068,27 @@ void DirectionReader::parsePercussion(const core::DirectionType &directionType) void DirectionReader::parseOtherDirection(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &otherDirection = directionType.choice().asOtherDirection(); + api::OtherDirectionData outOtherDirection; + outOtherDirection.text = otherDirection.value(); + outOtherDirection.printObject = getPrintObject(otherDirection); + if (otherDirection.smufl().has_value()) + { + outOtherDirection.smufl = otherDirection.smufl()->toString(); + } + outOtherDirection.positionData = getPositionData(otherDirection); + outOtherDirection.fontData = getFontData(otherDirection); + if (otherDirection.color().has_value()) + { + outOtherDirection.color = getColor(otherDirection); + } + if (otherDirection.id().has_value()) + { + outOtherDirection.id = otherDirection.id()->value(); + } + myOutDirectionData.otherDirections.emplace_back(std::move(outOtherDirection)); + appendOrderedComponent(api::DirectionComponentKind::otherDirection, + static_cast(myOutDirectionData.otherDirections.size()) - 1); } void DirectionReader::parseHarmony(const core::Harmony &inHarmony, const core::HarmonyChordGroup &inGrp) diff --git a/src/private/mx/impl/DirectionWriter.cpp b/src/private/mx/impl/DirectionWriter.cpp index 402823659..16a4a5fb9 100644 --- a/src/private/mx/impl/DirectionWriter.cpp +++ b/src/private/mx/impl/DirectionWriter.cpp @@ -4,6 +4,8 @@ #include "mx/impl/DirectionWriter.h" #include "mx/api/BarlineData.h" +#include "mx/core/generated/AccordionMiddle.h" +#include "mx/core/generated/AccordionRegistration.h" #include "mx/core/generated/Bass.h" #include "mx/core/generated/BassStep.h" #include "mx/core/generated/BeatUnitGroup.h" @@ -35,6 +37,7 @@ #include "mx/core/generated/HarmonyAlter.h" #include "mx/core/generated/HarmonyChordGroup.h" #include "mx/core/generated/HarmonyChordGroupChoice.h" +#include "mx/core/generated/Image.h" #include "mx/core/generated/Inversion.h" #include "mx/core/generated/Kind.h" #include "mx/core/generated/Metronome.h" @@ -51,14 +54,18 @@ #include "mx/core/generated/OctaveShift.h" #include "mx/core/generated/Offset.h" #include "mx/core/generated/OnOff.h" +#include "mx/core/generated/OtherDirection.h" #include "mx/core/generated/Pedal.h" #include "mx/core/generated/PedalType.h" #include "mx/core/generated/PerMinute.h" #include "mx/core/generated/PositiveDivisions.h" +#include "mx/core/generated/PrincipalVoice.h" +#include "mx/core/generated/PrincipalVoiceSymbol.h" #include "mx/core/generated/Root.h" #include "mx/core/generated/RootStep.h" #include "mx/core/generated/Segno.h" #include "mx/core/generated/Semitones.h" +#include "mx/core/generated/SmuflGlyphName.h" #include "mx/core/generated/Sound.h" #include "mx/core/generated/StaffDivide.h" #include "mx/core/generated/StaffDivideSymbol.h" @@ -68,6 +75,7 @@ #include "mx/core/generated/StringMute.h" #include "mx/core/generated/StringNumber.h" #include "mx/core/generated/StyleText.h" +#include "mx/core/generated/ValignImage.h" #include "mx/core/generated/Wedge.h" #include "mx/core/generated/WedgeType.h" #include "mx/core/generated/YesNo.h" @@ -708,6 +716,135 @@ void DirectionWriter::emitStaffDivide(const api::StaffDivideData &item, core::Di addDirectionType(std::move(dt), direction); } +void DirectionWriter::emitPrincipalVoice(const api::PrincipalVoiceData &item, core::Direction &direction) +{ + core::PrincipalVoice principalVoice{}; + principalVoice.setType(item.type == api::PrincipalVoiceType::stop ? core::StartStop::stop() + : core::StartStop::start()); + switch (item.symbol) + { + case api::PrincipalVoiceSymbol::nebenstimme: + principalVoice.setSymbol(core::PrincipalVoiceSymbol::nebenstimme()); + break; + case api::PrincipalVoiceSymbol::plain: + principalVoice.setSymbol(core::PrincipalVoiceSymbol::plain()); + break; + case api::PrincipalVoiceSymbol::none: + principalVoice.setSymbol(core::PrincipalVoiceSymbol::none()); + break; + case api::PrincipalVoiceSymbol::hauptstimme: + principalVoice.setSymbol(core::PrincipalVoiceSymbol::hauptstimme()); + break; + } + principalVoice.setValue(item.text); + setAttributesFromPositionData(item.positionData, principalVoice); + setAttributesFromFontData(item.fontData, principalVoice); + if (item.color.has_value()) + { + setAttributesFromColorData(*item.color, principalVoice); + } + if (item.id.has_value()) + { + principalVoice.setID(core::Token{*item.id}); + } + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::principalVoice(std::move(principalVoice))); + addDirectionType(std::move(dt), direction); +} + +void DirectionWriter::emitOtherDirection(const api::OtherDirectionData &item, core::Direction &direction) +{ + core::OtherDirection otherDirection{}; + otherDirection.setValue(item.text); + if (item.printObject != api::Bool::unspecified) + { + otherDirection.setPrintObject(myConverter.convert(item.printObject)); + } + if (item.smufl.has_value()) + { + otherDirection.setSmufl(core::SmuflGlyphName{*item.smufl}); + } + setAttributesFromPositionData(item.positionData, otherDirection); + setAttributesFromFontData(item.fontData, otherDirection); + if (item.color.has_value()) + { + setAttributesFromColorData(*item.color, otherDirection); + } + if (item.id.has_value()) + { + otherDirection.setID(core::Token{*item.id}); + } + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::otherDirection(std::move(otherDirection))); + addDirectionType(std::move(dt), direction); +} + +void DirectionWriter::emitImage(const api::ImageData &item, core::Direction &direction) +{ + core::Image image{}; + image.setSource(item.source); + image.setType(item.type); + if (item.height.has_value()) + { + image.setHeight(core::Tenths{core::Decimal{*item.height}}); + } + if (item.width.has_value()) + { + image.setWidth(core::Tenths{core::Decimal{*item.width}}); + } + setAttributesFromPositionData(item.positionData, image); + // 's valign is the valign-image type (no baseline), which the generic position + // helper cannot write; set it on the element directly. A baseline value cannot be + // expressed on an image and is not written. + switch (item.positionData.verticalAlignment) + { + case api::VerticalAlignment::top: + image.setValign(core::ValignImage::top()); + break; + case api::VerticalAlignment::middle: + image.setValign(core::ValignImage::middle()); + break; + case api::VerticalAlignment::bottom: + image.setValign(core::ValignImage::bottom()); + break; + case api::VerticalAlignment::baseline: + case api::VerticalAlignment::unspecified: + default: + break; + } + if (item.id.has_value()) + { + image.setID(core::Token{*item.id}); + } + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::image(std::move(image))); + addDirectionType(std::move(dt), direction); +} + +void DirectionWriter::emitAccordionRegistration(const api::AccordionRegistrationData &item, core::Direction &direction) +{ + core::AccordionRegistration accordion{}; + accordion.setAccordionHigh(item.high); + if (item.middle.has_value()) + { + accordion.setAccordionMiddle(core::AccordionMiddle{*item.middle}); + } + accordion.setAccordionLow(item.low); + setAttributesFromPositionData(item.positionData, accordion); + setAttributesFromFontData(item.fontData, accordion); + if (item.color.has_value()) + { + setAttributesFromColorData(*item.color, accordion); + } + if (item.id.has_value()) + { + accordion.setID(core::Token{*item.id}); + } + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::accordionRegistration(std::move(accordion))); + addDirectionType(std::move(dt), direction); +} + void DirectionWriter::emitFixedOrder(core::Direction &direction) { for (const auto &mark : myDirectionData.marks) @@ -811,6 +948,26 @@ void DirectionWriter::emitFixedOrder(core::Direction &direction) { emitStaffDivide(item, direction); } + + for (const auto &item : myDirectionData.principalVoices) + { + emitPrincipalVoice(item, direction); + } + + for (const auto &item : myDirectionData.otherDirections) + { + emitOtherDirection(item, direction); + } + + for (const auto &item : myDirectionData.images) + { + emitImage(item, direction); + } + + for (const auto &item : myDirectionData.accordionRegistrations) + { + emitAccordionRegistration(item, direction); + } } void DirectionWriter::emitOrderedComponents(core::Direction &direction) @@ -974,6 +1131,34 @@ void DirectionWriter::emitOrderedComponents(core::Direction &direction) } break; + case api::DirectionComponentKind::principalVoice: + if (i >= 0 && static_cast(i) < myDirectionData.principalVoices.size()) + { + emitPrincipalVoice(myDirectionData.principalVoices.at(i), direction); + } + break; + + case api::DirectionComponentKind::otherDirection: + if (i >= 0 && static_cast(i) < myDirectionData.otherDirections.size()) + { + emitOtherDirection(myDirectionData.otherDirections.at(i), direction); + } + break; + + case api::DirectionComponentKind::image: + if (i >= 0 && static_cast(i) < myDirectionData.images.size()) + { + emitImage(myDirectionData.images.at(i), direction); + } + break; + + case api::DirectionComponentKind::accordionRegistration: + if (i >= 0 && static_cast(i) < myDirectionData.accordionRegistrations.size()) + { + emitAccordionRegistration(myDirectionData.accordionRegistrations.at(i), direction); + } + break; + case api::DirectionComponentKind::chord: // Chords are emitted via createHarmonyElements, not as direction-types. break; diff --git a/src/private/mx/impl/DirectionWriter.h b/src/private/mx/impl/DirectionWriter.h index b19b7aa09..3b6860b03 100644 --- a/src/private/mx/impl/DirectionWriter.h +++ b/src/private/mx/impl/DirectionWriter.h @@ -61,6 +61,10 @@ class DirectionWriter void emitEyeglasses(const api::EyeglassesData &item, core::Direction &direction); void emitStringMute(const api::StringMuteData &item, core::Direction &direction); void emitStaffDivide(const api::StaffDivideData &item, core::Direction &direction); + void emitPrincipalVoice(const api::PrincipalVoiceData &item, core::Direction &direction); + void emitOtherDirection(const api::OtherDirectionData &item, core::Direction &direction); + void emitImage(const api::ImageData &item, core::Direction &direction); + void emitAccordionRegistration(const api::AccordionRegistrationData &item, core::Direction &direction); void emitFixedOrder(core::Direction &direction); void emitOrderedComponents(core::Direction &direction); diff --git a/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp b/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp index 619e99ad5..b5fd2a860 100644 --- a/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp +++ b/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp @@ -115,6 +115,106 @@ TEST(StaffDivide, DirectionMarksRoundTrip) T_END; +TEST(PrincipalVoice, DirectionMarksRoundTrip) +{ + DirectionData direction; + PrincipalVoiceData principalVoice; + principalVoice.type = PrincipalVoiceType::start; + principalVoice.symbol = PrincipalVoiceSymbol::nebenstimme; + principalVoice.text = "N"; + direction.principalVoices.push_back(principalVoice); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().principalVoices.size() == 1); + const auto &out = directions.front().principalVoices.front(); + CHECK(out.type == PrincipalVoiceType::start); + CHECK(out.symbol == PrincipalVoiceSymbol::nebenstimme); + CHECK_EQUAL("N", out.text); +} + +T_END; + +TEST(OtherDirection, DirectionMarksRoundTrip) +{ + DirectionData direction; + OtherDirectionData otherDirection; + otherDirection.text = "con sordino misterioso"; + otherDirection.printObject = Bool::no; + otherDirection.smufl = "luteFingeringRHThumb"; + direction.otherDirections.push_back(otherDirection); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().otherDirections.size() == 1); + const auto &out = directions.front().otherDirections.front(); + CHECK_EQUAL("con sordino misterioso", out.text); + CHECK(out.printObject == Bool::no); + REQUIRE(out.smufl.has_value()); + CHECK_EQUAL("luteFingeringRHThumb", *out.smufl); +} + +T_END; + +TEST(Image, DirectionMarksRoundTrip) +{ + DirectionData direction; + ImageData image; + image.source = "logo.png"; + image.type = "image/png"; + image.height = 40.0; + image.width = 80.0; + image.positionData.verticalAlignment = VerticalAlignment::middle; + direction.images.push_back(image); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().images.size() == 1); + const auto &out = directions.front().images.front(); + CHECK_EQUAL("logo.png", out.source); + CHECK_EQUAL("image/png", out.type); + REQUIRE(out.height.has_value()); + CHECK_DOUBLES_EQUAL(40.0, *out.height, 0.0001); + REQUIRE(out.width.has_value()); + CHECK_DOUBLES_EQUAL(80.0, *out.width, 0.0001); + CHECK(out.positionData.verticalAlignment == VerticalAlignment::middle); +} + +T_END; + +TEST(AccordionRegistration, DirectionMarksRoundTrip) +{ + DirectionData direction; + AccordionRegistrationData accordion; + accordion.high = true; + accordion.middle = 2; + accordion.low = true; + direction.accordionRegistrations.push_back(accordion); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().accordionRegistrations.size() == 1); + const auto &out = directions.front().accordionRegistrations.front(); + CHECK(out.high); + REQUIRE(out.middle.has_value()); + CHECK_EQUAL(2, *out.middle); + CHECK(out.low); +} + +T_END; + +TEST(AccordionRegistrationEmpty, DirectionMarksRoundTrip) +{ + // A registration with nothing engaged is legal and draws the empty diagram. + DirectionData direction; + direction.accordionRegistrations.emplace_back(); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().accordionRegistrations.size() == 1); + const auto &out = directions.front().accordionRegistrations.front(); + CHECK(!out.high); + CHECK(!out.middle.has_value()); + CHECK(!out.low); +} + +T_END; + TEST(DampFormatting, DirectionMarksRoundTrip) { DirectionData direction; diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 9ea5ac10a..590de4e80 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -444,3 +444,15 @@ synthetic/eyeglasses.3.1.xml synthetic/staff-divide.3.1.xml synthetic/string-mute.3.0.xml synthetic/string-mute.3.1.xml + +# Unblocked by modeling principal-voice, other-direction, image, and +# accordion-registration (#324). +lysuite/ly75a_AccordionRegistrations.xml +synthetic/accordion-registration.3.0.xml +synthetic/accordion-registration.3.1.xml +synthetic/image.3.0.xml +synthetic/image.3.1.xml +synthetic/other-direction.3.0.xml +synthetic/other-direction.3.1.xml +synthetic/principal-voice.3.0.xml +synthetic/principal-voice.3.1.xml