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 ContentArray& 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
276 std::optional<LayoutSymbol> symbol;
277
285
292 std::optional<LayoutStaffKeySet> sources;
293};
294
316[[nodiscard]] inline std::optional<std::vector<LayoutSpan>>
317buildLayoutSpans(const mnx::Layout& layout)
318{
319 const auto content = layout.content();
320 std::vector<LayoutSpan> spans;
321 spans.reserve(content.size()); // lower bound
322
323 size_t staffIndex = 0;
324 size_t encounter = 0; // stable tiebreaker
325
326 struct SortKey { size_t start, depth, encounter; };
327
328 struct TaggedSpan {
329 LayoutSpan span;
330 SortKey key;
331 };
332
333 std::vector<TaggedSpan> tagged;
334 tagged.reserve(content.size());
335
336 // Return value:
337 // - outer std::optional: hard failure (unsupported element) if empty
338 // - inner std::optional: no staves in subtree if empty; otherwise first/last staff indices
339 const auto walk =
340 [&](auto&& self, const ContentArray& arr, size_t depth)
341 -> std::optional<std::optional<std::pair<size_t,size_t>>>
342 {
343 std::optional<size_t> first;
344 std::optional<size_t> last;
345
346 for (auto elem : arr) {
347 if (elem.type() == layout::Staff::ContentTypeValue) {
348 layout::Staff s = elem.get<layout::Staff>();
349
350 const size_t i = staffIndex++;
351
352 LayoutSpan span;
353 span.kind = LayoutSpan::Kind::Staff;
354 span.depth = depth + 1; // staff spans must be at the deepest depth
355 span.startIndex = i;
356 span.endIndex = i;
357 span.symbol = s.symbol();
358 span.label = s.label();
359 span.labelref = s.labelref();
360 span.barlineOverride = StaffGroupBarlineOverride::None;
361 span.sources = util::analyzeLayoutStaffVoices(s);
362
363 tagged.push_back({ std::move(span), SortKey{i, depth + 1, encounter++} });
364
365 first = first.value_or(i);
366 last = i;
367 } else if (elem.type() == layout::Group::ContentTypeValue) {
368 layout::Group g = elem.get<layout::Group>();
369
370 auto childRange = self(self, g.content(), depth + 1);
371 if (!childRange) {
372 return std::nullopt; // hard failure
373 }
374 if (!*childRange) {
375 continue; // skip groups with no staves anywhere in their subtree
376 }
377 const auto [cFirst, cLast] = **childRange;
378
379 LayoutSpan span;
380 span.kind = LayoutSpan::Kind::Group;
381 span.depth = depth;
382 span.startIndex = cFirst;
383 span.endIndex = cLast;
384 span.symbol = g.symbol();
385 span.label = g.label();
386 span.barlineOverride = g.calcBarlineOverride();
387
388 tagged.push_back({ std::move(span), SortKey{cFirst, depth, encounter++} });
389
390 first = first.value_or(cFirst);
391 last = cLast;
392 } else {
393 return std::nullopt; // unsupported content element
394 }
395 }
396
397 if (!first || !last) {
398 return std::optional<std::pair<size_t,size_t>>{}; // success, but no staves
399 }
400 return std::make_pair(*first, *last);
401 };
402
403 auto rootRange = walk(walk, content, /*depth*/0);
404 if (!rootRange) {
405 return std::nullopt;
406 }
407 // Empty root content is allowed; it simply produces an empty spans vector.
408
409 std::stable_sort(tagged.begin(), tagged.end(),
410 [](const TaggedSpan& a, const TaggedSpan& b)
411 {
412 if (a.key.start != b.key.start) return a.key.start < b.key.start;
413 if (a.key.depth != b.key.depth) return a.key.depth < b.key.depth;
414 return a.key.encounter < b.key.encounter;
415 });
416
417 spans.reserve(tagged.size());
418 for (auto& t : tagged) spans.push_back(std::move(t.span));
419 return spans;
420}
421
439[[nodiscard]] inline std::vector<LayoutSpan>
440buildDefaultLayoutSpans(const Array<Part>& parts)
441{
442 std::vector<LayoutSpan> result;
443 size_t staffIdx = 0;
444
445 for (const auto& part : parts) {
446 const size_t numStaves = static_cast<size_t>(part.staves());
447 if (numStaves == 0) {
448 continue;
449 }
450 size_t staffDepth = 1;
451 bool staffNameNeeded = true;
452 if (numStaves > 1) {
453 LayoutSpan groupSpan;
454 groupSpan.depth = 0;
455 groupSpan.kind = LayoutSpan::Kind::Group;
456 groupSpan.symbol = LayoutSymbol::Brace;
457 groupSpan.startIndex = staffIdx;
458 groupSpan.endIndex = staffIdx + numStaves - 1;
459 groupSpan.label = part.name();
460 groupSpan.barlineOverride = StaffGroupBarlineOverride::Unified;
461 staffNameNeeded = false;
462 result.emplace_back(std::move(groupSpan));
463 }
464 for (size_t x = 0; x < numStaves; x++) {
465 LayoutSpan staffSpan;
466 staffSpan.depth = staffDepth;
467 staffSpan.kind = LayoutSpan::Kind::Staff;
468 staffSpan.startIndex = staffIdx;
469 staffSpan.endIndex = staffIdx;
470 staffSpan.barlineOverride = StaffGroupBarlineOverride::None;
471 if (staffNameNeeded) {
472 staffSpan.label = part.name();
473 }
474 result.emplace_back(std::move(staffSpan));
475 staffIdx++;
476 }
477 }
478
479 return result;
480}
481
482} // namespace mnx::util
Represents the element of the layout array in an MNX document.
Definition Layout.h:145
static constexpr std::string_view ContentTypeValue
type value that identifies the type within the content array
Definition Layout.h:135
Represents a single staff instance within an MNX layout.
Definition Layout.h:83
static constexpr std::string_view ContentTypeValue
type value that identifies the type within the content array
Definition Layout.h:102
StaffGroupBarlineOverride
Resolved barline override setting for a layout staff group.
Definition Enumerations.h:284
@ Unified
override with unified barline
@ Brace
piano brace
Describes a visual span in a flattened MNX layout.
Definition LayoutHelpers.h:209
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:292
std::optional< LabelRef > labelref
Optional label reference associated with this span.
Definition LayoutHelpers.h:268
std::optional< LayoutSymbol > symbol
Optional layout symbol associated with this span.
Definition LayoutHelpers.h:276
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:284
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