rename view_model -> view_mode_model; add shell-expands-FX test and parse-site comments
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
#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 <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<Mode>& 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<Mode> modes_; // kept sorted by ordinal, then insertion
|
||||
};
|
||||
|
||||
// The membership record for one tagged leaf track, keyed externally by GUID.
|
||||
struct Membership {
|
||||
std::set<std::string> 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<std::string> modesOf(const std::string& guid) const;
|
||||
|
||||
const std::map<std::string, Membership>& 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<std::string, Membership> 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<FolderNode> 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<int> 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<TrackFlagOp> flags;
|
||||
std::vector<FxOfflineOp> 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<TrackPlan> park; // inactive leaves -> parked (fixed zeros)
|
||||
std::vector<TrackPlan> 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<std::string, TrackSnapshot>& 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<std::string> 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<ViewModeModel> deserialize(const std::string& json);
|
||||
|
||||
private:
|
||||
ModeRegistry modes_;
|
||||
MembershipIndex membership_;
|
||||
std::string activeModeId_; // always a registered id
|
||||
std::map<std::string, TrackSnapshot> 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);
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user