6e901c198c
Extends the REAPER-free view_mode_model for per-item mode membership via fixed lanes: lane->mode C_LANEPLAYS mapping, managed-vs-manual lane-ownership index, managed-only item-lane ops in the toggle planner, "which lanes may this toggle touch" query, auto-tag decision (manual-lane items exempt), and JSON round-trip of the lane index. D1 behavior and tests unchanged.
534 lines
27 KiB
C++
534 lines
27 KiB
C++
#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;
|
|
}
|
|
};
|
|
|
|
// -- Lane ownership (Phase D2 / two-canvas item-level projection) -------------
|
|
//
|
|
// D2 extends the track-level projection to the ITEM level via REAPER fixed lanes
|
|
// (I_FREEMODE=2). On a track shared by two stances, each mode owns a fixed lane; a
|
|
// toggle shows/plays only the active mode's lane. This is the item-visibility analog
|
|
// of D1's track parking, and it carries the same load-bearing guarantee:
|
|
//
|
|
// THE TOOL DRIVES ONLY WHAT IT MINTED. A fixed-lane track is also REAPER's native
|
|
// comping surface — a user may keep their OWN manual lanes (comp takes, alternate
|
|
// reads). Mode operations touch ONLY managed lanes; manual lanes are never shown,
|
|
// hidden, silenced, or re-laned, and their C_LANEPLAYS stays exactly as set. This
|
|
// is the fixed-lane analog of "never touch B_MUTE/I_SOLO" and "never touch master".
|
|
//
|
|
// LANE IDENTITY IS AN OPAQUE, STABLE KEY SUPPLIED BY THE SHELL (boundary). The pure
|
|
// index keys a lane by (track GUID + a lane key string). The lane key is an OPAQUE
|
|
// identifier the shell provides; this model does NOT assume lane ordinals are stable
|
|
// and bakes in NO I_FIXEDLANE renumber/reorder assumptions. Whether the shell derives
|
|
// the key from a raw I_FIXEDLANE ordinal or a more durable identity — and how it keeps
|
|
// the index from going stale across lane reorder/renumber/deletion — is a Wave-2 SHELL
|
|
// design point (CONTEXT.md §Lane-identity fragility). The pure model's only contract:
|
|
// the same lane key denotes the same lane across calls.
|
|
|
|
// One lane's ownership: managed by a specific mode, or manual (user-minted, outside
|
|
// the mode system). `managedMode` present ⇒ managed by that mode id; absent ⇒ manual.
|
|
struct LaneOwnership {
|
|
std::optional<std::string> managedMode; // set ⇒ managed by this mode; unset ⇒ manual
|
|
|
|
bool isManaged() const { return managedMode.has_value(); }
|
|
bool isManual() const { return !managedMode.has_value(); }
|
|
|
|
bool operator==(const LaneOwnership& o) const { return managedMode == o.managedMode; }
|
|
};
|
|
|
|
// A lane's composite key: (track GUID, opaque lane key). Ordered so it can key a map.
|
|
struct LaneRef {
|
|
std::string trackGuid;
|
|
std::string laneKey; // opaque, shell-supplied; NOT assumed to be a stable ordinal
|
|
|
|
bool operator<(const LaneRef& o) const {
|
|
if (trackGuid != o.trackGuid) return trackGuid < o.trackGuid;
|
|
return laneKey < o.laneKey;
|
|
}
|
|
bool operator==(const LaneRef& o) const {
|
|
return trackGuid == o.trackGuid && laneKey == o.laneKey;
|
|
}
|
|
};
|
|
|
|
// (track GUID, lane key) -> ownership. Managed lanes name their owning mode; manual
|
|
// lanes are user-minted and off-limits to every mode operation. GUID-keyed and
|
|
// portable, it rides in the "reasampler" view_state alongside the membership index.
|
|
// A lane ABSENT from the index has no recorded ownership — the model treats an absent
|
|
// lane as manual by default (the tool never minted it), so the managed-only guarantee
|
|
// holds even before the index is populated.
|
|
class LaneOwnershipIndex {
|
|
public:
|
|
// Records lane (trackGuid, laneKey) as MANAGED by `modeId`, replacing any prior
|
|
// ownership. Returns false if any argument is empty.
|
|
bool setManaged(const std::string& trackGuid, const std::string& laneKey,
|
|
const std::string& modeId);
|
|
|
|
// Records lane (trackGuid, laneKey) as MANUAL (user-minted), replacing any prior
|
|
// ownership. Returns false if trackGuid or laneKey is empty.
|
|
bool setManual(const std::string& trackGuid, const std::string& laneKey);
|
|
|
|
// Removes the lane from the index entirely (⇒ treated as manual-by-default again).
|
|
// Returns true if it was present.
|
|
bool remove(const std::string& trackGuid, const std::string& laneKey);
|
|
|
|
// The ownership for a lane, or nullptr if the lane has no recorded entry (⇒ manual
|
|
// by default). Invalidated by any mutating call.
|
|
const LaneOwnership* query(const std::string& trackGuid, const std::string& laneKey) const;
|
|
|
|
// True if the lane is recorded MANAGED (by any mode). A lane absent from the index
|
|
// is NOT managed (manual by default) — this is the load-bearing predicate the
|
|
// toggle planner and the "which lanes may this toggle touch" query gate on.
|
|
bool isManaged(const std::string& trackGuid, const std::string& laneKey) const {
|
|
const LaneOwnership* o = query(trackGuid, laneKey);
|
|
return o && o->isManaged();
|
|
}
|
|
|
|
const std::map<LaneRef, LaneOwnership>& all() const { return entries_; }
|
|
|
|
std::size_t size() const { return entries_.size(); }
|
|
bool empty() const { return entries_.empty(); }
|
|
|
|
bool operator==(const LaneOwnershipIndex& o) const { return entries_ == o.entries_; }
|
|
|
|
private:
|
|
std::map<LaneRef, LaneOwnership> entries_; // (guid, laneKey) -> ownership
|
|
};
|
|
|
|
// The play/show state a managed lane takes for a given active mode, matching REAPER's
|
|
// item/track-side C_LANEPLAYS values (SDK: 0=lane silent+hidden, 1=lane plays
|
|
// exclusively). A managed lane owned by the ACTIVE mode plays (1); every other managed
|
|
// lane is silenced+hidden (0) — consistent with exclusive membership and D1's "a mode
|
|
// flip is a real change, not cosmetic." Exposed as a free function for direct testing.
|
|
// managedMode == activeMode ⇒ 1 (plays exclusively)
|
|
// otherwise ⇒ 0 (does not play; hidden + silent)
|
|
// The caller must only pass MANAGED lanes here; manual lanes never reach this decision.
|
|
inline constexpr int kLanePlaysExclusive = 1; // C_LANEPLAYS: plays exclusively
|
|
inline constexpr int kLaneSilent = 0; // C_LANEPLAYS: does not play (hidden+silent)
|
|
int laneModeState(const std::string& managedMode, const std::string& activeMode);
|
|
|
|
// 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 a mode if it belongs
|
|
// to that mode by its own membership OR any of its descendant leaves does, 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;
|
|
}
|
|
};
|
|
|
|
// One managed-lane play/show write the shell must apply. The shell translates this
|
|
// into the REAPER lane setters (track-side C_LANEPLAYS:N and, per item, I_FIXEDLANE /
|
|
// C_LANEPLAYS; B_FIXEDLANE_HIDDEN follows from the play state). `lanePlays` is a
|
|
// C_LANEPLAYS value: kLanePlaysExclusive when the active mode owns the lane,
|
|
// kLaneSilent otherwise. The pure model emits these for MANAGED lanes ONLY — never a
|
|
// manual lane (the fixed-lane analog of "never touch mute/solo"), enforced in
|
|
// planToggle and mirrored by lanesTouchedByToggle.
|
|
struct LanePlayOp {
|
|
std::string trackGuid;
|
|
std::string laneKey; // opaque, shell-supplied
|
|
int lanePlays = kLaneSilent;
|
|
|
|
bool operator==(const LanePlayOp& o) const {
|
|
return trackGuid == o.trackGuid && laneKey == o.laneKey && lanePlays == o.lanePlays;
|
|
}
|
|
};
|
|
|
|
// 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 LEAVES DO appear: an untagged leaf is an Arrange member,
|
|
// so it parks in every non-Arrange mode and restores in Arrange — the mode system
|
|
// manages all leaves, not only tagged ones.
|
|
struct TogglePlan {
|
|
std::vector<TrackPlan> park; // inactive leaves -> parked (fixed zeros)
|
|
std::vector<TrackPlan> restore; // active leaves returning -> snapshot values
|
|
|
|
// D2 item-level projection: per managed lane, the C_LANEPLAYS state for the target
|
|
// mode (active mode's lane plays; every other managed lane silenced+hidden). MANAGED
|
|
// lanes ONLY — a manual lane never appears here. Empty when no managed lanes exist,
|
|
// so a D1-only project (no fixed lanes) produces an identical plan to before.
|
|
std::vector<LanePlayOp> lanes;
|
|
};
|
|
|
|
// -- 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_; }
|
|
LaneOwnershipIndex& lanes() { return lanes_; }
|
|
const LaneOwnershipIndex& lanes() const { return lanes_; }
|
|
|
|
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_; }
|
|
|
|
// Prunes orphaned per-track state: drops every snapshot whose GUID is NOT in
|
|
// `liveGuids` (the set of GUIDs the shell currently enumerates from the project).
|
|
// Returns the number of snapshots removed. The shell calls this before planning a
|
|
// toggle; because reapply-on-load also routes through the shell's applyMode, this
|
|
// reconciles on project open too.
|
|
//
|
|
// Why snapshots and NOT membership: a parked track's snapshot is dead weight once
|
|
// the track is deleted — it can never be restored, and if REAPER reuses that GUID
|
|
// for a different track a stale snapshot would drive an INCORRECT restore. So it
|
|
// must be pruned. Membership is deliberately KEPT: REAPER's undo of a track delete
|
|
// restores the SAME GUID, so dropping the Design tag on delete would silently lose
|
|
// it on undo-delete. Keeping membership means an undone delete brings the track
|
|
// back correctly tagged and it re-snapshots + re-parks cleanly on the next toggle.
|
|
// A genuinely-deleted-and-never-restored track leaves only a tiny dormant
|
|
// membership entry — acceptable, and far better than losing tags on undo. Folder
|
|
// RESTRUCTURE (moving tracks without deleting) is already self-healing: the tree is
|
|
// rebuilt from I_FOLDERDEPTH every toggle, so a restructure leaves every GUID live
|
|
// and reconcile is a no-op over it. This handles DELETION specifically.
|
|
std::size_t reconcile(const std::set<std::string>& liveGuids);
|
|
|
|
// 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 that EITHER belongs to the mode by its own
|
|
// membership OR has at least one descendant visible in the mode. Untagged nodes
|
|
// (leaf or folder) count as Arrange, so an untagged folder carrying its own
|
|
// FX/media shows in Arrange even when none of its children do, and additionally
|
|
// shows in a child's mode by derivation. 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` by enumerating EVERY leaf in the supplied tree.
|
|
// A leaf inactive in the target mode — tagged into another mode, or untagged and
|
|
// the target isn't Arrange — is parked with fixed zeros; a leaf that becomes
|
|
// active AND has a stored snapshot is restored from it. Parents (visibility-only)
|
|
// and show-both leaves (always visible) are never parked; the master is not in
|
|
// the tree. Untagged leaves ARE managed: they are Arrange members, so they park
|
|
// in non-Arrange modes and restore in Arrange. Tree membership is the enumeration
|
|
// source, so stale membership GUIDs absent from the tree are naturally ignored.
|
|
//
|
|
// 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;
|
|
|
|
// The managed-only "which lanes may this toggle touch" query: the set of lane refs
|
|
// a toggle is permitted to drive — MANAGED lanes ONLY, from the ownership index.
|
|
// Manual lanes are NEVER in the result, regardless of target mode. This is the pure,
|
|
// testable decision behind the load-bearing invariant; the shell reads live lane
|
|
// state and applies C_LANEPLAYS only to lanes this query returns. Independent of the
|
|
// folder tree (lane ownership is not a tree property) — the target mode does not
|
|
// filter the SET (every managed lane is touchable), only the play VALUE each takes
|
|
// (see planToggle / laneModeState).
|
|
std::set<LaneRef> lanesTouchedByToggle() 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_;
|
|
LaneOwnershipIndex lanes_; // (guid, laneKey) -> ownership
|
|
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);
|
|
|
|
// -- Auto-tag decision (Phase D2) --------------------------------------------
|
|
//
|
|
// New content — both new tracks and new items — is tagged to whatever mode is active
|
|
// when it is created; pre-existing content defaults to Arrange. The DECISION is pure:
|
|
// the Wave-2 shell detects new GUIDs by diffing project state on the panel timer and
|
|
// asks this function what to tag. Pre-existing content (a GUID the shell does not
|
|
// report as new) never reaches here and stays at its index state (Arrange by default).
|
|
//
|
|
// Manual-lane exemption: an item that landed in a MANUAL lane is off-limits to auto-tag
|
|
// — auto-tag governs normal timeline content, not hand-managed lanes. The shell marks
|
|
// such an item `onManualLane = true` (it knows the item's lane and consults the
|
|
// ownership index); the decision then emits NO tag for it. New tracks and new items on
|
|
// managed/no lane follow the active-mode rule.
|
|
|
|
// One new item the shell detected this poll. Its lane disposition decides exemption.
|
|
struct NewItem {
|
|
std::string guid;
|
|
bool onManualLane = false; // true ⇒ EXEMPT from auto-tag (hand-managed lane)
|
|
};
|
|
|
|
// One membership write the auto-tag decision produced: tag `guid` into `modeId`. The
|
|
// shell applies it to the MembershipIndex (a new track/item joins the active mode).
|
|
struct AutoTag {
|
|
std::string guid;
|
|
std::string modeId;
|
|
|
|
bool operator==(const AutoTag& o) const { return guid == o.guid && modeId == o.modeId; }
|
|
};
|
|
|
|
// The pure auto-tag decision: given the new track GUIDs and new items detected this
|
|
// poll plus the active mode, produce the membership writes. Every new track is tagged
|
|
// to `activeMode`; every new item is tagged to `activeMode` UNLESS it landed on a
|
|
// manual lane (exempt). An empty `activeMode` yields no tags (nothing to tag into).
|
|
// Empty GUIDs are skipped. The result is a plan the shell applies; this function
|
|
// mutates nothing.
|
|
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
|
const std::vector<NewItem>& newItems,
|
|
const std::string& activeMode);
|
|
|
|
// 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
|