MNX Document Model
Loading...
Searching...
No Matches
LayoutHelpers.h
1/*
2 * Copyright (C) 2025, Robert Patterson
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22#pragma once
23
24#include <optional>
25#include <string>
26#include <string_view>
27#include <unordered_map>
28#include <unordered_set>
29#include <vector>
30
31#include "../BaseTypes.h"
32#include "../Global.h"
33#include "../Layout.h"
34
35namespace mnx::util {
36
44{
46 std::string partId;
47
49 int staffNo = 1;
50
55 bool operator==(const StaffKey& o) const noexcept
56 {
57 return staffNo == o.staffNo && partId == o.partId;
58 }
59};
60
65{
70 size_t operator()(const StaffKey& k) const noexcept
71 {
72 size_t h1 = std::hash<std::string>{}(k.partId);
73 size_t h2 = std::hash<int>{}(k.staffNo);
74 return h1 ^ (h2 + 0x9e3779b9u + (h1 << 6) + (h1 >> 2));
75 }
76};
77
79using LayoutStaffKeySet = std::unordered_set<StaffKey, StaffKeyHash>;
80
93[[nodiscard]] inline std::optional<LayoutStaffKeySet>
94analyzeLayoutStaffVoices(const layout::Staff& staff)
95{
96 const auto sources = staff.sources();
97 if (sources.empty()) {
98 return std::nullopt;
99 }
100
101 struct KeyState
102 {
103 size_t count = 0;
104 bool anyMissingVoice = false;
105 std::unordered_set<std::string> voices;
106 };
107
108 std::unordered_map<StaffKey, KeyState, StaffKeyHash> stateByKey;
109 stateByKey.reserve(sources.size());
110
111 for (const auto src : sources) {
112 const std::string partId = src.part();
113 if (partId.empty()) {
114 return std::nullopt;
115 }
116
117 const StaffKey key{partId, src.staff()};
118 auto& st = stateByKey[key];
119 ++st.count;
120
121 const auto v = src.voice();
122 if (!v) {
123 st.anyMissingVoice = true;
124 continue;
125 }
126
127 if (v->empty()) {
128 return std::nullopt;
129 }
130
131 if (!st.voices.emplace(*v).second) {
132 return std::nullopt; // duplicate voice for same StaffKey
133 }
134 }
135
136 // Enforce per-StaffKey semantic rule.
137 for (const auto& kv : stateByKey) {
138 const KeyState& st = kv.second;
139 if (st.count > 1) {
140 if (st.anyMissingVoice || st.voices.size() != st.count) {
141 return std::nullopt;
142 }
143 }
144 }
145
146 LayoutStaffKeySet result;
147 result.reserve(stateByKey.size());
148 for (const auto& kv : stateByKey) {
149 result.insert(kv.first);
150 }
151
152 return result;
153}
154
164[[nodiscard]] inline std::optional<std::vector<layout::Staff>>
165flattenLayoutStaves(const Layout& layout)
166{
167 auto content = layout.content();
168 std::vector<layout::Staff> result;
169 result.reserve(content.size()); // lower bound; groups may expand further
170
171 const auto walk = [&](auto&& self, const layout::LayoutContent& content) -> std::optional<bool> {
172 for (auto elem : content) {
173 if (elem.type() == layout::Group::ContentTypeValue) {
174 layout::Group g = elem.get<layout::Group>();
175 auto ok = self(self, g.content());
176 if (!ok) {
177 return std::nullopt;
178 }
179 } else if (elem.type() == layout::Staff::ContentTypeValue) {
180 result.push_back(elem.get<layout::Staff>());
181 } else {
182 return std::nullopt;
183 }
184 }
185 return true;
186 };
187
188 if (!walk(walk, content)) {
189 return std::nullopt;
190 }
191 return result;
192}
193
209{
214 enum class Kind
215 {
216 Staff,
217 Group
218 };
219
221
228 size_t startIndex{};
229
237 size_t endIndex{};
238
252 size_t depth{};
253
260 std::optional<std::string> label;
261
268 std::optional<LabelRef> labelref;
269
278
286
293 std::optional<LayoutStaffKeySet> sources;
294};
295
317[[nodiscard]] inline std::optional<std::vector<LayoutSpan>>
318buildLayoutSpans(const mnx::Layout& layout)
319{
320 const auto content = layout.content();
321 std::vector<LayoutSpan> spans;
322 spans.reserve(content.size()); // lower bound
323
324 size_t staffIndex = 0;
325 size_t encounter = 0; // stable tiebreaker
326
327 struct SortKey { size_t start, depth, encounter; };
328
329 struct TaggedSpan {
330 LayoutSpan span;
331 SortKey key;
332 };
333
334 std::vector<TaggedSpan> tagged;
335 tagged.reserve(content.size());
336
337 // Return value:
338 // - outer std::optional: hard failure (unsupported element) if empty
339 // - inner std::optional: no staves in subtree if empty; otherwise first/last staff indices
340 const auto walk =
341 [&](auto&& self, const layout::LayoutContent& arr, size_t depth)
342 -> std::optional<std::optional<std::pair<size_t,size_t>>>
343 {
344 std::optional<size_t> first;
345 std::optional<size_t> last;
346
347 for (auto elem : arr) {
348 if (elem.type() == layout::Staff::ContentTypeValue) {
349 layout::Staff s = elem.get<layout::Staff>();
350
351 const size_t i = staffIndex++;
352
353 LayoutSpan span;
354 span.kind = LayoutSpan::Kind::Staff;
355 span.depth = depth + 1; // staff spans must be at the deepest depth
356 span.startIndex = i;
357 span.endIndex = i;
358 span.symbol = s.symbol();
359 span.label = s.label();
360 span.labelref = s.labelref();
361 span.barlineOverride = StaffGroupBarlineOverride::None;
362 span.sources = util::analyzeLayoutStaffVoices(s);
363
364 tagged.push_back({ std::move(span), SortKey{i, depth + 1, encounter++} });
365
366 first = first.value_or(i);
367 last = i;
368 } else if (elem.type() == layout::Group::ContentTypeValue) {
369 layout::Group g = elem.get<layout::Group>();
370
371 auto childRange = self(self, g.content(), depth + 1);
372 if (!childRange) {
373 return std::nullopt; // hard failure
374 }
375 if (!*childRange) {
376 continue; // skip groups with no staves anywhere in their subtree
377 }
378 const auto [cFirst, cLast] = **childRange;
379
380 LayoutSpan span;
381 span.kind = LayoutSpan::Kind::Group;
382 span.depth = depth;
383 span.startIndex = cFirst;
384 span.endIndex = cLast;
385 span.symbol = g.symbol();
386 span.label = g.label();
387 span.barlineOverride = g.calcBarlineOverride();
388
389 tagged.push_back({ std::move(span), SortKey{cFirst, depth, encounter++} });
390
391 first = first.value_or(cFirst);
392 last = cLast;
393 } else {
394 return std::nullopt; // unsupported content element
395 }
396 }
397
398 if (!first || !last) {
399 return std::optional<std::pair<size_t,size_t>>{}; // success, but no staves
400 }
401 return std::make_pair(*first, *last);
402 };
403
404 auto rootRange = walk(walk, content, /*depth*/0);
405 if (!rootRange) {
406 return std::nullopt;
407 }
408 // Empty root content is allowed; it simply produces an empty spans vector.
409
410 std::stable_sort(tagged.begin(), tagged.end(),
411 [](const TaggedSpan& a, const TaggedSpan& b)
412 {
413 if (a.key.start != b.key.start) return a.key.start < b.key.start;
414 if (a.key.depth != b.key.depth) return a.key.depth < b.key.depth;
415 return a.key.encounter < b.key.encounter;
416 });
417
418 spans.reserve(tagged.size());
419 for (auto& t : tagged) spans.push_back(std::move(t.span));
420 return spans;
421}
422
440[[nodiscard]] inline std::vector<LayoutSpan>
441buildDefaultLayoutSpans(const Array<Part>& parts)
442{
443 std::vector<LayoutSpan> result;
444 size_t staffIdx = 0;
445
446 for (const auto& part : parts) {
447 const size_t numStaves = static_cast<size_t>(part.staves());
448 if (numStaves == 0) {
449 continue;
450 }
451 size_t staffDepth = 1;
452 bool staffNameNeeded = true;
453 if (numStaves > 1) {
454 LayoutSpan groupSpan;
455 groupSpan.depth = 0;
456 groupSpan.kind = LayoutSpan::Kind::Group;
457 groupSpan.symbol = LayoutSymbol::Brace;
458 groupSpan.startIndex = staffIdx;
459 groupSpan.endIndex = staffIdx + numStaves - 1;
460 groupSpan.label = part.name();
461 groupSpan.barlineOverride = StaffGroupBarlineOverride::Unified;
462 staffNameNeeded = false;
463 result.emplace_back(std::move(groupSpan));
464 }
465 for (size_t x = 0; x < numStaves; x++) {
466 LayoutSpan staffSpan;
467 staffSpan.depth = staffDepth;
468 staffSpan.kind = LayoutSpan::Kind::Staff;
469 staffSpan.startIndex = staffIdx;
470 staffSpan.endIndex = staffIdx;
471 staffSpan.symbol = LayoutSymbol::NoSymbol;
472 staffSpan.barlineOverride = StaffGroupBarlineOverride::None;
473 if (staffNameNeeded) {
474 staffSpan.label = part.name();
475 }
476 result.emplace_back(std::move(staffSpan));
477 staffIdx++;
478 }
479 }
480
481 return result;
482}
483
484} // namespace mnx::util
Represents the element of the layout array in an MNX document.
Definition Layout.h:170
static constexpr std::string_view ContentTypeValue
type value that identifies the type within the content array
Definition Layout.h:154
Represents a single staff instance within an MNX layout.
Definition Layout.h:102
static constexpr std::string_view ContentTypeValue
type value that identifies the type within the content array
Definition Layout.h:121
StaffGroupBarlineOverride
Resolved barline override setting for a layout staff group.
Definition Enumerations.h:427
@ Unified
override with unified barline
@ s
"s-" as in sforzando (sf)
LayoutSymbol
The symbols available to bracket a staff group.
Definition Enumerations.h:245
@ NoSymbol
the default (none)
@ Brace
piano brace
Describes a visual span in a flattened MNX layout.
Definition LayoutHelpers.h:209
LayoutSymbol symbol
Layout symbol associated with this span.
Definition LayoutHelpers.h:277
size_t startIndex
Index of the first staff covered by this span.
Definition LayoutHelpers.h:228
std::optional< LayoutStaffKeySet > sources
Optional staff sources associated with this span.
Definition LayoutHelpers.h:293
std::optional< LabelRef > labelref
Optional label reference associated with this span.
Definition LayoutHelpers.h:268
std::optional< std::string > label
Optional label text associated with this span.
Definition LayoutHelpers.h:260
size_t endIndex
Index of the last staff covered by this span.
Definition LayoutHelpers.h:237
size_t depth
Nesting depth of this span within the layout hierarchy.
Definition LayoutHelpers.h:252
StaffGroupBarlineOverride barlineOverride
Resolved barline override associated with this span.
Definition LayoutHelpers.h:285
Kind
Identifies whether this span represents a staff or a group.
Definition LayoutHelpers.h:215
@ Group
Span represents a group of staves.
@ Staff
Span represents a single staff.
Kind kind
The kind of layout element represented by this span.
Definition LayoutHelpers.h:220
Hash functor for StaffKey.
Definition LayoutHelpers.h:65
size_t operator()(const StaffKey &k) const noexcept
Computes a hash value for a StaffKey.
Definition LayoutHelpers.h:70
Identifies a specific staff within a specific part.
Definition LayoutHelpers.h:44
std::string partId
The ID of the part.
Definition LayoutHelpers.h:46
bool operator==(const StaffKey &o) const noexcept
Equality comparison.
Definition LayoutHelpers.h:55
int staffNo
The 1-based staff number within the part.
Definition LayoutHelpers.h:49