#include "core/view/view_mode_model.h" #include #include #include #include #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 MembershipIndex::modesOf(const std::string& guid) const { const Membership* m = query(guid); return m ? m->modeIds : std::set{}; } 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 autoTagNewContent(const std::vector& newTrackGuids, const std::vector& newItems, const std::string& activeMode) { std::vector 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 planItemRetag(const std::vector& selected, const std::string& targetMode) { std::vector 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& 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> visibleModesOf; for (const Mode& mode : model.modes().all()) { const std::set 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 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& laneModes = ownItemModes; // lazy-mint: own modes only plan.splits.push_back(LaneMintPlan::TrackSplit{ track.trackGuid, static_cast(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(i), snap.fxOffline[i] != 0}); return p; } std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId) { const std::vector& 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& 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 ViewModeModel::visibleTracks(const FolderTree& tree, const std::string& modeId) const { std::set 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 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 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> 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 ViewModeModel::lanesTouchedByToggle() const { std::set 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& 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 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::deserialize(const std::string& blob) { ViewModeModel vm; json::Reader r(blob); if (!parseModel(r, vm)) return std::nullopt; return vm; } } // namespace reasampler