631 lines
23 KiB
C++
631 lines
23 KiB
C++
#include "core/view/view_mode_model.h"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <set>
|
|
#include <utility>
|
|
|
|
#include "core/json/json.h"
|
|
#include "core/view/lane_keys.h" // laneNameForMode
|
|
|
|
namespace reasampler {
|
|
|
|
using view::laneNameForMode;
|
|
|
|
bool Mode::operator==(const Mode& o) const {
|
|
return id == o.id && displayName == o.displayName && ordinal == o.ordinal;
|
|
}
|
|
|
|
ModeRegistry::ModeRegistry() {
|
|
modes_.push_back(Mode{kArrangeModeId, "Arrange", 0});
|
|
modes_.push_back(Mode{kDesignModeId, "Design", 1});
|
|
}
|
|
|
|
bool ModeRegistry::add(const Mode& mode) {
|
|
if (mode.id.empty()) return false;
|
|
if (query(mode.id) != nullptr) return false; // ids are unique
|
|
modes_.push_back(mode);
|
|
std::stable_sort(modes_.begin(), modes_.end(),
|
|
[](const Mode& a, const Mode& b) { return a.ordinal < b.ordinal; });
|
|
return true;
|
|
}
|
|
|
|
const Mode* ModeRegistry::query(const std::string& id) const {
|
|
for (const auto& m : modes_)
|
|
if (m.id == id) return &m;
|
|
return nullptr;
|
|
}
|
|
|
|
bool MembershipIndex::tag(const std::string& guid, const std::string& modeId) {
|
|
if (guid.empty() || modeId.empty()) return false;
|
|
Membership& m = entries_[guid];
|
|
m.modeIds.clear(); // a leaf lives in exactly one mode (show-both aside)
|
|
m.modeIds.insert(modeId);
|
|
return true;
|
|
}
|
|
|
|
bool MembershipIndex::untag(const std::string& guid) {
|
|
return entries_.erase(guid) > 0;
|
|
}
|
|
|
|
bool MembershipIndex::setShowBoth(const std::string& guid, bool showBoth) {
|
|
if (guid.empty()) return false;
|
|
entries_[guid].showBoth = showBoth; // creates an Arrange-default entry if new
|
|
return true;
|
|
}
|
|
|
|
bool MembershipIndex::restore(const std::string& guid, const Membership& membership) {
|
|
if (guid.empty()) return false;
|
|
entries_[guid] = membership;
|
|
return true;
|
|
}
|
|
|
|
const Membership* MembershipIndex::query(const std::string& guid) const {
|
|
auto it = entries_.find(guid);
|
|
return it == entries_.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
std::set<std::string> MembershipIndex::modesOf(const std::string& guid) const {
|
|
const Membership* m = query(guid);
|
|
return m ? m->modeIds : std::set<std::string>{};
|
|
}
|
|
|
|
bool LaneOwnershipIndex::setManaged(const std::string& trackGuid, const std::string& laneKey,
|
|
const std::string& modeId) {
|
|
if (trackGuid.empty() || laneKey.empty() || modeId.empty()) return false;
|
|
entries_[LaneRef{trackGuid, laneKey}] = LaneOwnership{modeId};
|
|
return true;
|
|
}
|
|
|
|
bool LaneOwnershipIndex::setManual(const std::string& trackGuid, const std::string& laneKey) {
|
|
if (trackGuid.empty() || laneKey.empty()) return false;
|
|
entries_[LaneRef{trackGuid, laneKey}] = LaneOwnership{std::nullopt};
|
|
return true;
|
|
}
|
|
|
|
bool LaneOwnershipIndex::remove(const std::string& trackGuid, const std::string& laneKey) {
|
|
return entries_.erase(LaneRef{trackGuid, laneKey}) > 0;
|
|
}
|
|
|
|
const LaneOwnership* LaneOwnershipIndex::query(const std::string& trackGuid,
|
|
const std::string& laneKey) const {
|
|
auto it = entries_.find(LaneRef{trackGuid, laneKey});
|
|
return it == entries_.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
int laneModeState(const std::string& managedMode, const std::string& activeMode) {
|
|
// Assumes at most one managed lane per (track, mode) — planToggle asserts
|
|
// this in debug builds; two lanes claiming the same mode would both be
|
|
// told to play exclusively, which REAPER can't honor coherently.
|
|
return managedMode == activeMode ? kLanePlaysExclusive : kLaneSilent;
|
|
}
|
|
|
|
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
|
const std::vector<NewItem>& newItems,
|
|
const std::string& activeMode) {
|
|
std::vector<AutoTag> tags;
|
|
if (activeMode.empty()) return tags; // nothing to tag into
|
|
|
|
for (const auto& guid : newTrackGuids) {
|
|
if (guid.empty()) continue;
|
|
tags.push_back(AutoTag{guid, activeMode});
|
|
}
|
|
for (const auto& item : newItems) {
|
|
if (item.guid.empty()) continue;
|
|
if (item.onManualLane) continue;
|
|
|
|
// Adopt the track's single pre-existing mode (strand guard — see header).
|
|
const std::string& target =
|
|
item.trackModes.size() == 1 ? *item.trackModes.begin() : activeMode;
|
|
tags.push_back(AutoTag{item.guid, target});
|
|
}
|
|
return tags;
|
|
}
|
|
|
|
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
|
const std::string& targetMode) {
|
|
std::vector<ItemRetagOp> ops;
|
|
const bool untag = targetMode.empty(); // empty target ⇒ untag (→ Arrange default)
|
|
for (const RetagItem& item : selected) {
|
|
if (item.guid.empty()) continue;
|
|
if (item.onManualLane) continue;
|
|
ops.push_back(ItemRetagOp{item.guid, untag, untag ? std::string{} : targetMode});
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
|
const std::vector<LaneTrack>& tracks) {
|
|
LaneMintPlan plan;
|
|
|
|
// Per track GUID, the modes it's visible in (tree-aware) — captures the
|
|
// folder-derived-visibility split trigger, not just own-item mode span.
|
|
std::map<std::string, std::set<std::string>> visibleModesOf;
|
|
for (const Mode& mode : model.modes().all()) {
|
|
const std::set<std::string> vis = model.visibleTracks(tree, mode.id);
|
|
for (const std::string& guid : vis)
|
|
visibleModesOf[guid].insert(mode.id);
|
|
}
|
|
|
|
for (const LaneTrack& track : tracks) {
|
|
if (track.trackGuid.empty()) continue;
|
|
|
|
if (model.membership().isShowBoth(track.trackGuid)) continue; // never force-split
|
|
|
|
std::set<std::string> ownItemModes;
|
|
for (const LaneItem& item : track.items) {
|
|
if (item.guid.empty() || item.modeId.empty()) continue;
|
|
if (item.onManualLane) continue; // exempt
|
|
ownItemModes.insert(item.modeId);
|
|
}
|
|
|
|
if (ownItemModes.empty()) continue; // no own media, nothing to confine
|
|
|
|
const auto visIt = visibleModesOf.find(track.trackGuid);
|
|
const std::size_t visibleModeCount =
|
|
visIt == visibleModesOf.end() ? 0 : visIt->second.size();
|
|
const bool multiMode = ownItemModes.size() >= 2 || visibleModeCount >= 2;
|
|
|
|
if (!multiMode) continue; // single-mode: D1 whole-track parking still separates
|
|
|
|
const std::set<std::string>& laneModes = ownItemModes; // lazy-mint: own modes only
|
|
|
|
plan.splits.push_back(LaneMintPlan::TrackSplit{
|
|
track.trackGuid, static_cast<int>(laneModes.size())});
|
|
for (const std::string& mode : laneModes) {
|
|
plan.mints.push_back(
|
|
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
|
}
|
|
|
|
for (const LaneItem& item : track.items) {
|
|
if (item.guid.empty() || item.modeId.empty()) continue;
|
|
if (item.onManualLane) continue;
|
|
plan.assigns.push_back(LaneAssign{
|
|
item.guid, track.trackGuid, laneNameForMode(item.modeId)});
|
|
}
|
|
}
|
|
|
|
return plan;
|
|
}
|
|
|
|
TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
|
TrackPlan p;
|
|
p.flags = {
|
|
{guid, Flag::ShowInTcp, 0},
|
|
{guid, Flag::ShowInMixer, 0},
|
|
{guid, Flag::MainSend, 0},
|
|
{guid, Flag::FxEnable, 0},
|
|
};
|
|
for (int i = 0; i < fxCount; ++i)
|
|
p.fxOffline.push_back({guid, i, true});
|
|
return p;
|
|
}
|
|
|
|
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
|
|
TrackPlan p;
|
|
p.flags = {
|
|
{guid, Flag::ShowInTcp, snap.showInTcp},
|
|
{guid, Flag::ShowInMixer, snap.showInMixer},
|
|
{guid, Flag::MainSend, snap.mainSend},
|
|
{guid, Flag::FxEnable, snap.fxEnable},
|
|
};
|
|
for (std::size_t i = 0; i < snap.fxOffline.size(); ++i)
|
|
p.fxOffline.push_back({guid, static_cast<int>(i), snap.fxOffline[i] != 0});
|
|
return p;
|
|
}
|
|
|
|
std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId) {
|
|
const std::vector<Mode>& all = modes.all();
|
|
if (all.empty()) return {}; // nothing to cycle to
|
|
for (std::size_t i = 0; i < all.size(); ++i) {
|
|
if (all[i].id == currentModeId)
|
|
return all[(i + 1) % all.size()].id; // wrap past the last
|
|
}
|
|
return all.front().id; // stale/unknown current id -> jump to the first mode
|
|
}
|
|
|
|
ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {}
|
|
|
|
bool ViewModeModel::setActiveMode(const std::string& modeId) {
|
|
if (!modes_.contains(modeId)) return false;
|
|
activeModeId_ = modeId;
|
|
return true;
|
|
}
|
|
|
|
void ViewModeModel::storeSnapshot(const std::string& guid, const TrackSnapshot& snap) {
|
|
snapshots_[guid] = snap;
|
|
}
|
|
|
|
void ViewModeModel::clearSnapshot(const std::string& guid) {
|
|
snapshots_.erase(guid);
|
|
}
|
|
|
|
const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
|
|
auto it = snapshots_.find(guid);
|
|
return it == snapshots_.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
std::size_t ViewModeModel::reconcile(const std::set<std::string>& liveGuids) {
|
|
// See header: snapshots are pruned, membership is not (undo-delete rationale).
|
|
std::size_t removed = 0;
|
|
for (auto it = snapshots_.begin(); it != snapshots_.end();) {
|
|
if (liveGuids.count(it->first) == 0) {
|
|
it = snapshots_.erase(it);
|
|
++removed;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
return removed;
|
|
}
|
|
|
|
bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const {
|
|
const Membership* m = membership_.query(guid);
|
|
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
|
|
if (m->showBoth) return true; // show-both ⇒ every mode
|
|
if (m->modeIds.empty()) return modeId == kArrangeModeId;
|
|
return m->modeIds.count(modeId) > 0;
|
|
}
|
|
|
|
std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
|
|
const std::string& modeId) const {
|
|
std::set<std::string> visible;
|
|
|
|
// Pass 1: nodes visible by their own membership (leaf rule, or an
|
|
// untagged/Arrange-default folder).
|
|
for (const auto& node : tree.nodes) {
|
|
if (leafBelongsToMode(node.guid, modeId))
|
|
visible.insert(node.guid);
|
|
}
|
|
|
|
// Pass 2: propagate up parent chains so a parent with any visible
|
|
// descendant is visible too (OR'd with pass 1). Cycle-guarded.
|
|
std::map<std::string, std::string> parentOf;
|
|
for (const auto& node : tree.nodes) parentOf[node.guid] = node.parentGuid;
|
|
|
|
// Seed from the full pass-1 set (not just leaves) so a parent already visible by
|
|
// its own membership still propagates visibility up its remaining ancestors; a
|
|
// snapshot copy so mid-loop insertions into `visible` are never re-walked.
|
|
const std::vector<std::string> seeds(visible.begin(), visible.end());
|
|
for (const auto& node : seeds) {
|
|
auto it = parentOf.find(node);
|
|
std::size_t guard = 0;
|
|
while (it != parentOf.end() && !it->second.empty() && guard++ < parentOf.size()) {
|
|
const std::string& parent = it->second;
|
|
if (!visible.insert(parent).second) break; // already marked ⇒ chain done
|
|
it = parentOf.find(parent);
|
|
}
|
|
}
|
|
|
|
return visible;
|
|
}
|
|
|
|
TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
|
|
TogglePlan plan;
|
|
|
|
// Enumerate the tree (not membership_.all()) so untagged leaves — absent
|
|
// from the membership index but still Arrange members — park/restore too.
|
|
for (const auto& node : tree.nodes) {
|
|
if (node.isParent) continue;
|
|
const std::string& guid = node.guid;
|
|
if (membership_.isShowBoth(guid)) continue;
|
|
|
|
const bool active = leafBelongsToMode(guid, targetMode);
|
|
if (active) {
|
|
if (const TrackSnapshot* snap = snapshot(guid))
|
|
plan.restore.push_back(makeRestorePlan(guid, *snap));
|
|
} else {
|
|
// fxOffline is empty here; the D2 shell expands it via TrackFX_GetCount.
|
|
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
|
|
}
|
|
}
|
|
|
|
// One C_LANEPLAYS op per MANAGED lane; manual lanes are skipped entirely.
|
|
#ifndef NDEBUG
|
|
std::set<std::pair<std::string, std::string>> seenTrackMode; // (trackGuid, mode)
|
|
#endif
|
|
for (const auto& [ref, ownership] : lanes_.all()) {
|
|
if (!ownership.isManaged()) continue;
|
|
#ifndef NDEBUG
|
|
assert(seenTrackMode.insert({ref.trackGuid, *ownership.managedMode}).second &&
|
|
"two managed lanes on one track claim the same mode (exclusivity broken)");
|
|
#endif
|
|
const int lanePlays = laneModeState(*ownership.managedMode, targetMode);
|
|
plan.lanes.push_back(LanePlayOp{ref.trackGuid, ref.laneKey, lanePlays});
|
|
}
|
|
|
|
return plan;
|
|
}
|
|
|
|
std::set<LaneRef> ViewModeModel::lanesTouchedByToggle() const {
|
|
std::set<LaneRef> touched;
|
|
for (const auto& [ref, ownership] : lanes_.all()) {
|
|
if (ownership.isManaged()) touched.insert(ref);
|
|
}
|
|
return touched;
|
|
}
|
|
|
|
bool ViewModeModel::operator==(const ViewModeModel& o) const {
|
|
return modes_ == o.modes_ && membership_ == o.membership_ && lanes_ == o.lanes_ &&
|
|
activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Shared core/json emit helpers (Q-W1) — byte-identical escape/int rendering.
|
|
using json::writeEscaped;
|
|
using json::writeIntArray;
|
|
std::string intToStr(int v) { return json::numToStr(v); }
|
|
using ObjWriter = json::Writer;
|
|
|
|
} // namespace
|
|
|
|
std::string ViewModeModel::serialize() const {
|
|
std::string out;
|
|
{
|
|
ObjWriter root(out);
|
|
root.keyRaw("version", intToStr(1));
|
|
root.keyStr("activeMode", activeModeId_);
|
|
|
|
root.keyBegin("modes");
|
|
out += '[';
|
|
{
|
|
const auto& all = modes_.all();
|
|
for (std::size_t i = 0; i < all.size(); ++i) {
|
|
if (i) out += ',';
|
|
ObjWriter m(out);
|
|
m.keyStr("id", all[i].id);
|
|
m.keyStr("displayName", all[i].displayName);
|
|
m.keyRaw("ordinal", intToStr(all[i].ordinal));
|
|
}
|
|
}
|
|
out += ']';
|
|
|
|
// membership: array of { guid, modes[], showBoth }
|
|
root.keyBegin("membership");
|
|
out += '[';
|
|
{
|
|
bool first = true;
|
|
for (const auto& [guid, mem] : membership_.all()) {
|
|
if (!first) out += ',';
|
|
first = false;
|
|
ObjWriter e(out);
|
|
e.keyStr("guid", guid);
|
|
e.keyBegin("modes");
|
|
out += '[';
|
|
{
|
|
bool mf = true;
|
|
for (const auto& id : mem.modeIds) {
|
|
if (!mf) out += ',';
|
|
mf = false;
|
|
writeEscaped(out, id);
|
|
}
|
|
}
|
|
out += ']';
|
|
e.keyRaw("showBoth", mem.showBoth ? "true" : "false");
|
|
}
|
|
}
|
|
out += ']';
|
|
|
|
// snapshots: array of { guid, showInTcp, showInMixer, mainSend, fxEnable, fxOffline[] }
|
|
root.keyBegin("snapshots");
|
|
out += '[';
|
|
{
|
|
bool first = true;
|
|
for (const auto& [guid, snap] : snapshots_) {
|
|
if (!first) out += ',';
|
|
first = false;
|
|
ObjWriter e(out);
|
|
e.keyStr("guid", guid);
|
|
e.keyRaw("showInTcp", intToStr(snap.showInTcp));
|
|
e.keyRaw("showInMixer", intToStr(snap.showInMixer));
|
|
e.keyRaw("mainSend", intToStr(snap.mainSend));
|
|
e.keyRaw("fxEnable", intToStr(snap.fxEnable));
|
|
e.keyBegin("fxOffline");
|
|
writeIntArray(out, snap.fxOffline);
|
|
}
|
|
}
|
|
out += ']';
|
|
|
|
// lanes: array of { trackGuid, laneKey, managed(bool), mode(str, managed only) }
|
|
root.keyBegin("lanes");
|
|
out += '[';
|
|
{
|
|
bool first = true;
|
|
for (const auto& [ref, ownership] : lanes_.all()) {
|
|
if (!first) out += ',';
|
|
first = false;
|
|
ObjWriter e(out);
|
|
e.keyStr("trackGuid", ref.trackGuid);
|
|
e.keyStr("laneKey", ref.laneKey);
|
|
e.keyRaw("managed", ownership.isManaged() ? "true" : "false");
|
|
if (ownership.isManaged()) e.keyStr("mode", *ownership.managedMode);
|
|
}
|
|
}
|
|
out += ']';
|
|
} // root closes here (NRVO + deferred close, mirrors bank_model)
|
|
return out;
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool parseModes(json::Reader& r, ModeRegistry& reg) {
|
|
if (!r.consume('[')) return false;
|
|
r.skipWs();
|
|
if (r.consume(']')) return true; // empty array (unusual, but valid)
|
|
do {
|
|
if (!r.consume('{')) return false;
|
|
Mode m;
|
|
bool haveId = false;
|
|
do {
|
|
std::string k;
|
|
if (!r.parseKey(k)) return false;
|
|
if (k == "id") { if (!r.parseString(m.id)) return false; haveId = true; }
|
|
else if (k == "displayName") { if (!r.parseString(m.displayName)) return false; }
|
|
else if (k == "ordinal") { if (!r.parseInt(m.ordinal)) return false; }
|
|
else if (!r.skipValue()) return false;
|
|
} while (r.consume(','));
|
|
if (!r.consume('}')) return false;
|
|
if (!haveId || !reg.add(m)) return false; // malformed / duplicate id
|
|
} while (r.consume(','));
|
|
return r.consume(']');
|
|
}
|
|
|
|
bool parseMembership(json::Reader& r, MembershipIndex& idx) {
|
|
if (!r.consume('[')) return false;
|
|
r.skipWs();
|
|
if (r.consume(']')) return true;
|
|
do {
|
|
if (!r.consume('{')) return false;
|
|
std::string guid;
|
|
Membership mem;
|
|
bool haveGuid = false;
|
|
do {
|
|
std::string k;
|
|
if (!r.parseKey(k)) return false;
|
|
if (k == "guid") { if (!r.parseString(guid)) return false; haveGuid = true; }
|
|
else if (k == "modes") {
|
|
if (!r.consume('[')) return false;
|
|
r.skipWs();
|
|
if (!r.consume(']')) {
|
|
do {
|
|
std::string id;
|
|
if (!r.parseString(id)) return false;
|
|
mem.modeIds.insert(id);
|
|
} while (r.consume(','));
|
|
if (!r.consume(']')) return false;
|
|
}
|
|
}
|
|
else if (k == "showBoth") { if (!r.parseBool(mem.showBoth)) return false; }
|
|
else if (!r.skipValue()) return false;
|
|
} while (r.consume(','));
|
|
if (!r.consume('}')) return false;
|
|
if (!haveGuid || guid.empty()) return false;
|
|
// Install verbatim (tag() would clobber a multi-mode set / show-both).
|
|
// Stale mode ids / stale GUIDs are tolerated by design — only
|
|
// activeMode is validated (below).
|
|
if (!idx.restore(guid, mem)) return false;
|
|
} while (r.consume(','));
|
|
return r.consume(']');
|
|
}
|
|
|
|
bool parseSnapshots(json::Reader& r, std::map<std::string, TrackSnapshot>& snaps) {
|
|
if (!r.consume('[')) return false;
|
|
r.skipWs();
|
|
if (r.consume(']')) return true;
|
|
do {
|
|
if (!r.consume('{')) return false;
|
|
std::string guid;
|
|
TrackSnapshot snap;
|
|
bool haveGuid = false;
|
|
do {
|
|
std::string k;
|
|
if (!r.parseKey(k)) return false;
|
|
if (k == "guid") { if (!r.parseString(guid)) return false; haveGuid = true; }
|
|
else if (k == "showInTcp") { if (!r.parseInt(snap.showInTcp)) return false; }
|
|
else if (k == "showInMixer") { if (!r.parseInt(snap.showInMixer)) return false; }
|
|
else if (k == "mainSend") { if (!r.parseInt(snap.mainSend)) return false; }
|
|
else if (k == "fxEnable") { if (!r.parseInt(snap.fxEnable)) return false; }
|
|
else if (k == "fxOffline") { if (!r.parseIntArray(snap.fxOffline)) return false; }
|
|
else if (!r.skipValue()) return false;
|
|
} while (r.consume(','));
|
|
if (!r.consume('}')) return false;
|
|
if (!haveGuid || guid.empty()) return false;
|
|
snaps[guid] = snap;
|
|
} while (r.consume(','));
|
|
return r.consume(']');
|
|
}
|
|
|
|
bool parseLanes(json::Reader& r, LaneOwnershipIndex& idx) {
|
|
if (!r.consume('[')) return false;
|
|
r.skipWs();
|
|
if (r.consume(']')) return true;
|
|
do {
|
|
if (!r.consume('{')) return false;
|
|
std::string trackGuid, laneKey, mode;
|
|
bool haveTrack = false, haveLane = false, managed = false, haveManaged = false;
|
|
do {
|
|
std::string k;
|
|
if (!r.parseKey(k)) return false;
|
|
if (k == "trackGuid") { if (!r.parseString(trackGuid)) return false; haveTrack = true; }
|
|
else if (k == "laneKey") { if (!r.parseString(laneKey)) return false; haveLane = true; }
|
|
else if (k == "managed") { if (!r.parseBool(managed)) return false; haveManaged = true; }
|
|
else if (k == "mode") { if (!r.parseString(mode)) return false; }
|
|
else if (!r.skipValue()) return false;
|
|
} while (r.consume(','));
|
|
if (!r.consume('}')) return false;
|
|
// Both keys mandatory/non-empty; managed must carry a mode, manual must not
|
|
// — keeps a round-tripped index byte-for-byte identical to the source.
|
|
if (!haveTrack || !haveLane || !haveManaged) return false;
|
|
if (trackGuid.empty() || laneKey.empty()) return false;
|
|
if (managed) {
|
|
if (mode.empty()) return false;
|
|
if (!idx.setManaged(trackGuid, laneKey, mode)) return false;
|
|
} else {
|
|
if (!mode.empty()) return false; // manual lane must not carry a mode
|
|
if (!idx.setManual(trackGuid, laneKey)) return false;
|
|
}
|
|
} while (r.consume(','));
|
|
return r.consume(']');
|
|
}
|
|
|
|
bool parseModel(json::Reader& r, ViewModeModel& out) {
|
|
if (!r.consume('{')) return false;
|
|
r.skipWs();
|
|
if (r.consume('}')) return true; // lenient empty root ⇒ default-seeded model
|
|
|
|
ModeRegistry reg; // seeded default; REPLACED if a modes array is present
|
|
bool haveModes = false;
|
|
std::string activeMode;
|
|
bool haveActive = false;
|
|
MembershipIndex membership;
|
|
LaneOwnershipIndex lanes;
|
|
std::map<std::string, TrackSnapshot> snaps;
|
|
|
|
do {
|
|
std::string key;
|
|
if (!r.parseKey(key)) return false;
|
|
if (key == "activeMode") {
|
|
if (!r.parseString(activeMode)) return false;
|
|
haveActive = true;
|
|
} else if (key == "modes") {
|
|
ModeRegistry fresh = ModeRegistry::makeEmpty(); // parse into empty, then own
|
|
if (!parseModes(r, fresh)) return false;
|
|
reg = fresh;
|
|
haveModes = true;
|
|
} else if (key == "membership") {
|
|
if (!parseMembership(r, membership)) return false;
|
|
} else if (key == "snapshots") {
|
|
if (!parseSnapshots(r, snaps)) return false;
|
|
} else if (key == "lanes") {
|
|
if (!parseLanes(r, lanes)) return false;
|
|
} else {
|
|
if (!r.skipValue()) return false; // unknown keys / "version" placeholder
|
|
}
|
|
} while (r.consume(','));
|
|
|
|
if (!r.consume('}')) return false;
|
|
r.skipWs();
|
|
if (!r.eof()) return false; // trailing garbage
|
|
|
|
if (haveModes) out.modes() = reg;
|
|
out.membership() = membership;
|
|
out.lanes() = lanes;
|
|
for (const auto& [guid, snap] : snaps) out.storeSnapshot(guid, snap);
|
|
if (haveActive) {
|
|
if (!out.setActiveMode(activeMode)) return false; // active mode must exist
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string& blob) {
|
|
ViewModeModel vm;
|
|
json::Reader r(blob);
|
|
if (!parseModel(r, vm)) return std::nullopt;
|
|
return vm;
|
|
}
|
|
|
|
} // namespace reasampler
|