#pragma once // view_mode_model — the pure core of the Design View feature, deliberately free of any // REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the mirror of // bank_model: it owns the mode registry, the GUID-keyed membership index, the // folder-tree-aware visibility derivation, the parking/restore planner, and the // JSON round-trip of all of it. // // PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // vendor/ includes. Standard library only. The folder structure is an INPUT // supplied by the D2 shell (which reads REAPER's I_FOLDERDEPTH); this model never // fetches or stores REAPER's live tree — folder structure is REAPER's truth and // changes underneath us, so it is passed in per query, not held. // // -- Representation decisions (design latitude exercised; invariants below) ----- // // * A mode is (stable string id, display name, ordinal). Arrange (id "arrange", // ordinal 0) and Design (id "design", ordinal 1) are seeded. Arrange is the // fallback home for every untagged leaf; structurally it is just another mode. // // * Membership is GUID -> { mode ids } (a set, not a bool) plus a per-track // show-both flag. Normally a leaf is in exactly one mode; multiple only via the // parent-derivation rule (computed, not stored) or the show-both escape hatch. // An untagged GUID is NOT in the index and belongs to Arrange by default. // // * The planner drives exactly four scalar flags (showInTcp, showInMixer, // mainSend, fxEnable) plus a per-FX offline list. Park values are fixed zeros // (defined by the parking contract), so PARK ops need no snapshot. RESTORE ops // come entirely FROM a TrackSnapshot captured before parking — never a hardcoded // default. This is where the restore-contract invariant lives and is tested. // // * The snapshot stores the full prior per-FX offline vector so a save-while-parked // project round-trips and restores each FX to its exact prior offline state. The // pure model does NOT need REAPER FX counts to plan a park (park offlines all N, // which the shell expands from TrackFX_GetCount); it only needs them to restore, // and it gets them from the snapshot it captured. #include #include #include #include #include #include namespace reasampler { // Stable seed-mode ids. Arrange is the default home for untagged leaves. inline constexpr const char* kArrangeModeId = "arrange"; inline constexpr const char* kDesignModeId = "design"; // A display "stance" the user adopts. Modes are ordered by `ordinal` for tab order. struct Mode { std::string id; // stable, persisted; never reused for a different mode std::string displayName; int ordinal = 0; // tab order bool operator==(const Mode& o) const; }; // Ordered registry of modes. Arrange + Design are seeded on construction. Add more // to prove the model is N-mode, not boolean. Ids are unique; adding a duplicate id // is rejected. class ModeRegistry { public: ModeRegistry(); // seeds Arrange (ordinal 0) + Design (ordinal 1) // Adds a mode. Rejects (returns false, no mutation) an empty or duplicate id. bool add(const Mode& mode); // Returns the mode with `id`, or nullptr. Invalidated by any mutating call. const Mode* query(const std::string& id) const; bool contains(const std::string& id) const { return query(id) != nullptr; } // All modes in ordinal order (ties broken by insertion order). const std::vector& all() const { return modes_; } std::size_t size() const { return modes_.size(); } bool operator==(const ModeRegistry& o) const { return modes_ == o.modes_; } // An empty registry (no seed modes). Deserialization parses the persisted mode // set into this and then owns it; the default ctor's seed would otherwise make // the serialized Arrange/Design collide on add() and fail to round-trip. static ModeRegistry makeEmpty() { return ModeRegistry(EmptyTag{}); } private: struct EmptyTag {}; explicit ModeRegistry(EmptyTag) {} // no seed std::vector modes_; // kept sorted by ordinal, then insertion }; // The membership record for one tagged leaf track, keyed externally by GUID. struct Membership { std::set modeIds; // the mode(s) this leaf opted into bool showBoth = false; // pinned visible + running in every mode bool operator==(const Membership& o) const { return modeIds == o.modeIds && showBoth == o.showBoth; } }; // GUID-keyed membership index. Untagged GUIDs are absent and belong to Arrange. // Keyed by track GUID string, never index (reorder-safe). class MembershipIndex { public: // Tags `guid` into `modeId`, replacing any prior mode set (a leaf lives in one // mode; use showBoth for the cross-mode case). No-op-safe on repeated calls. // Returns false if guid or modeId is empty. bool tag(const std::string& guid, const std::string& modeId); // Removes `guid` from the index entirely (returns it to the Arrange default). // Returns true if it was present. bool untag(const std::string& guid); // Sets the show-both flag for `guid`. Tags the guid into no new mode; if the // guid is untagged it is created with an empty mode set (Arrange default) so // show-both alone is representable. Returns false if guid is empty. bool setShowBoth(const std::string& guid, bool showBoth); // Installs a complete membership record verbatim (multi-mode set + show-both), // replacing any existing entry for `guid`. Used by deserialization to rebuild a // trusted, already-valid entry without tag()'s single-mode clobbering. Returns // false if guid is empty. bool restore(const std::string& guid, const Membership& membership); // Returns the membership for `guid`, or nullptr if untagged. Invalidated by any // mutating call. const Membership* query(const std::string& guid) const; bool isShowBoth(const std::string& guid) const { const Membership* m = query(guid); return m && m->showBoth; } // The mode ids `guid` belongs to. Empty for an untagged guid (⇒ Arrange). std::set modesOf(const std::string& guid) const; const std::map& all() const { return entries_; } std::size_t size() const { return entries_.size(); } bool empty() const { return entries_.empty(); } bool operator==(const MembershipIndex& o) const { return entries_ == o.entries_; } private: std::map entries_; // guid -> membership }; // -- Folder tree (INPUT, not stored) ---------------------------------------- // // The shell builds this from I_FOLDERDEPTH each time and passes it to a visibility // query. A node is a leaf or a parent; a parent is visible in every mode any of // its descendant leaves belongs to, and is never parked. The master track is // modeled implicitly (always visible, never touched) and is NOT a node here. struct FolderNode { std::string guid; std::string parentGuid; // empty ⇒ top-level (child of master / project root) bool isParent = false; // true if this node has descendant tracks (a folder) }; // A flat parent↔child description of the current track tree. Order is arrange-view // order; parentGuid links each node to its immediate parent folder. struct FolderTree { std::vector nodes; }; // -- Snapshot + planner ------------------------------------------------------ // The prior value of every tool-driven flag on one track, captured BEFORE parking. // Restore uses these values verbatim — the restore contract's source of truth. // Flags mirror REAPER's numeric representation (0/1 for the bools) so the shell // applies them without translation; ints, not bools, so a snapshot faithfully // round-trips whatever REAPER reported (defensive against non-0/1 values). struct TrackSnapshot { int showInTcp = 0; // B_SHOWINTCP prior value int showInMixer = 0; // B_SHOWINMIXER prior value int mainSend = 0; // B_MAINSEND prior value int fxEnable = 0; // I_FXEN prior value // Prior per-FX offline state, index = fx slot. Lets restore return each FX to // exactly its captured offline value rather than a blanket "online". std::vector fxOffline; bool operator==(const TrackSnapshot& o) const { return showInTcp == o.showInTcp && showInMixer == o.showInMixer && mainSend == o.mainSend && fxEnable == o.fxEnable && fxOffline == o.fxOffline; } }; // Which scalar flag a TrackFlagOp drives. FX-offline is carried separately (it is // per-slot, variable length), see TrackParkPlan::fxOffline. enum class Flag { ShowInTcp, // B_SHOWINTCP ShowInMixer, // B_SHOWINMIXER MainSend, // B_MAINSEND FxEnable, // I_FXEN }; // One scalar-flag write the shell must apply: SetMediaTrackInfo_Value(guid, flag, value). struct TrackFlagOp { std::string guid; Flag flag = Flag::ShowInTcp; int value = 0; bool operator==(const TrackFlagOp& o) const { return guid == o.guid && flag == o.flag && value == o.value; } }; // One per-FX offline write: TrackFX_SetOffline(guid, fxIndex, offline). struct FxOfflineOp { std::string guid; int fxIndex = 0; bool offline = false; bool operator==(const FxOfflineOp& o) const { return guid == o.guid && fxIndex == o.fxIndex && offline == o.offline; } }; // The complete set of operations to park one inactive leaf, or restore one leaf. // Park uses fixed zeros (parking contract); restore uses a snapshot's values. // fxOffline is emitted per known FX slot: on park, from the snapshot's slot count // (all -> offline); on restore, each slot back to its captured value. struct TrackPlan { std::vector flags; std::vector fxOffline; }; // The plan for a whole toggle to a target mode: which tracks to park, and which to // restore from their snapshots. Parents and show-both leaves never appear here — // they are derived-visible and never parked (visibility is answered separately by // visibleTracks). Untagged tracks never appear either (the tool owns only what it // tagged). struct TogglePlan { std::vector park; // inactive leaves -> parked (fixed zeros) std::vector restore; // active leaves returning -> snapshot values }; // -- The view mode model ----------------------------------------------------- // // Owns the mode registry, the membership index, the active mode, and the durable // per-track snapshots (kept for tracks currently parked so a save-while-parked // project restores correctly). Visibility and the toggle plan are computed against // a supplied FolderTree — the tree is never stored. class ViewModeModel { public: ViewModeModel(); // Arrange + Design seeded; active mode = Arrange ModeRegistry& modes() { return modes_; } const ModeRegistry& modes() const { return modes_; } MembershipIndex& membership() { return membership_; } const MembershipIndex& membership() const { return membership_; } const std::string& activeModeId() const { return activeModeId_; } // Sets the active mode. Returns false (no change) if the id is not registered. bool setActiveMode(const std::string& modeId); // Records / clears the pre-park snapshot for a track. The shell calls store // before it parks a track; the model persists it so restore survives a save. void storeSnapshot(const std::string& guid, const TrackSnapshot& snap); void clearSnapshot(const std::string& guid); const TrackSnapshot* snapshot(const std::string& guid) const; const std::map& snapshots() const { return snapshots_; } // Does `guid` belong to `modeId`? A leaf belongs if it is tagged into modeId, // is show-both (belongs everywhere), or is untagged and modeId is Arrange (the // default). Parent derivation is NOT applied here — this is the LEAF rule; use // visibleTracks for the tree-aware answer. bool leafBelongsToMode(const std::string& guid, const std::string& modeId) const; // The set of track GUIDs visible in `modeId`, tree-aware: active leaves, // show-both leaves, and every parent with at least one descendant leaf in the // mode. Untagged leaves count as Arrange. Stale GUIDs in the tree are tolerated. // The master is not represented (always visible; the shell never touches it). std::set visibleTracks(const FolderTree& tree, const std::string& modeId) const; // Plans a toggle to `targetMode` against the current tree. Inactive tagged // leaves (not show-both, not derived-visible-only) are parked with fixed zeros; // leaves that become active AND have a stored snapshot are restored from it. // Parents, show-both leaves, the master, and untagged tracks are never parked. // Unknown/stale membership GUIDs absent from the tree are ignored (prune-safe). // // Note: park plans emitted here have an empty fxOffline vector. The D2 shell // expands per-FX offline writes using TrackFX_GetCount — the pure model has no // access to REAPER FX counts at plan time. TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const; bool operator==(const ViewModeModel& o) const; std::string serialize() const; // Parses a JSON string produced by serialize(). std::nullopt on malformed // input. On success deserialize(serialize(x)) == x. static std::optional deserialize(const std::string& json); private: ModeRegistry modes_; MembershipIndex membership_; std::string activeModeId_; // always a registered id std::map snapshots_; // guid -> pre-park snapshot }; // Builds the fixed-zero park plan for one leaf. Offlines `fxCount` slots. Exposed // for the shell and for direct testing of the parking contract. TrackPlan makeParkPlan(const std::string& guid, int fxCount); // Builds the restore plan for one leaf from its snapshot — every flag set to its // captured value, never a default. Exposed for the shell and for testing the // restore-contract invariant directly. TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap); // The next mode id in the registry's ordinal order, cycling past `currentModeId` // and wrapping to the first mode after the last (Arrange -> Design -> Arrange with // the two seed modes; the same cycle scales to N modes with no call-site change). // This is the pure decision behind the "toggle active mode" action: the shell reads // the model's active mode, asks for the next one, and applies it. // * empty registry -> "" (nothing to cycle to) // * currentModeId not present -> the first mode's id (a sane home to jump to) // Exposed as a free function (not a model member) so it is unit-testable against a // bare ModeRegistry without a full ViewModeModel. std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId); } // namespace reasampler