5f6efb7cc3
view_state v2 writes identities beside the v1 slot array, so a downgrade keeps what it had. An FX gone at restore time is dropped and reported, never restored onto whatever took its place.
527 lines
23 KiB
C++
527 lines
23 KiB
C++
#pragma once
|
|
// Pure core of the Design View feature — mirror of bank_model: mode registry,
|
|
// GUID-keyed membership, folder-tree-aware visibility, park/restore planner,
|
|
// and JSON round-trip. Folder structure is an INPUT (the D2 shell reads
|
|
// REAPER's I_FOLDERDEPTH); this model never fetches or stores REAPER's live
|
|
// tree. See src/core/view/CLAUDE.md for the settled invariants.
|
|
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/view/fx_offline.h"
|
|
#include "core/view/solo_cache.h"
|
|
|
|
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; ids
|
|
// are unique, adding a duplicate id is rejected.
|
|
class ModeRegistry {
|
|
public:
|
|
ModeRegistry(); // seeds Arrange (ordinal 0) + Design (ordinal 1)
|
|
|
|
bool add(const Mode& mode);
|
|
|
|
const Mode* query(const std::string& id) const;
|
|
|
|
bool contains(const std::string& id) const { return query(id) != nullptr; }
|
|
|
|
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_; }
|
|
|
|
// Empty registry (no seed modes) for deserialization, so the parsed
|
|
// Arrange/Design don't collide with the default ctor's seeded ones.
|
|
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;
|
|
}
|
|
};
|
|
|
|
// Item-level (fixed-lane) lane ownership. Mode operations touch only managed
|
|
// lanes; manual lanes are the user's own comping lanes and stay untouched —
|
|
// the fixed-lane analog of never-touch-mute/solo. Lane identity is an opaque
|
|
// key the shell supplies; this model bakes in no I_FIXEDLANE ordinal assumption.
|
|
|
|
// One lane's ownership: managed by a specific mode, or manual (user-minted).
|
|
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).
|
|
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, GUID-keyed and portable. A lane ABSENT
|
|
// from the index is treated as manual by default (never minted by the tool),
|
|
// so the managed-only guarantee holds even before the index is populated.
|
|
class LaneOwnershipIndex {
|
|
public:
|
|
bool setManaged(const std::string& trackGuid, const std::string& laneKey,
|
|
const std::string& modeId);
|
|
|
|
bool setManual(const std::string& trackGuid, const std::string& laneKey);
|
|
|
|
// Removes the lane entirely (⇒ manual-by-default again). Returns true if present.
|
|
bool remove(const std::string& trackGuid, const std::string& laneKey);
|
|
|
|
const LaneOwnership* query(const std::string& trackGuid, const std::string& laneKey) const;
|
|
|
|
// Load-bearing predicate the toggle planner gates on: absent ⇒ not managed.
|
|
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
|
|
};
|
|
|
|
// C_LANEPLAYS value for a managed lane under the given active mode: the lane
|
|
// plays exclusively iff its owning mode is active, else silent+hidden. Callers
|
|
// must only pass MANAGED lanes; 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.
|
|
class MembershipIndex {
|
|
public:
|
|
// Tags `guid` into `modeId`, replacing any prior mode set. Returns false if
|
|
// guid or modeId is empty.
|
|
bool tag(const std::string& guid, const std::string& modeId);
|
|
|
|
// Removes `guid` entirely (returns it to the Arrange default).
|
|
bool untag(const std::string& guid);
|
|
|
|
// Sets the show-both flag; creates an untagged (Arrange-default) entry if
|
|
// `guid` had none, so show-both alone is representable.
|
|
bool setShowBoth(const std::string& guid, bool showBoth);
|
|
|
|
// Installs a complete membership record verbatim, replacing any existing
|
|
// entry. Used by deserialization to rebuild a trusted entry without tag()'s
|
|
// single-mode clobbering.
|
|
bool restore(const std::string& guid, const Membership& membership);
|
|
|
|
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: an INPUT the shell rebuilds from I_FOLDERDEPTH each call, never
|
|
// stored here. A parent is visible in a mode if it belongs by its own
|
|
// membership or any descendant leaf does, and is never parked. The master
|
|
// track is implicit (always visible, untouched) 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)
|
|
};
|
|
|
|
// Arrange-view order; parentGuid links each node to its immediate parent folder.
|
|
struct FolderTree {
|
|
std::vector<FolderNode> nodes;
|
|
};
|
|
|
|
// The prior value of every tool-driven flag on one track, captured BEFORE
|
|
// parking — restore's source of truth. 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 in capture-time slot order, keyed per fxKeying:
|
|
// by the FX's own identity (live capture), or by position (a snapshot lifted
|
|
// from a project saved before identity was recorded).
|
|
std::vector<FxOfflineState> fxOffline;
|
|
FxKeying fxKeying = FxKeying::Identity;
|
|
|
|
bool operator==(const TrackSnapshot& o) const {
|
|
return showInTcp == o.showInTcp && showInMixer == o.showInMixer &&
|
|
mainSend == o.mainSend && fxEnable == o.fxEnable &&
|
|
fxOffline == o.fxOffline && fxKeying == o.fxKeying;
|
|
}
|
|
};
|
|
|
|
// 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 managed-lane play/show write the shell must apply (translated into
|
|
// C_LANEPLAYS / I_FIXEDLANE / B_FIXEDLANE_HIDDEN). Emitted for MANAGED lanes
|
|
// only — never a manual lane; 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; restore uses a snapshot's values. Park's
|
|
// fxOffline ops are slot-keyed (every live slot goes offline); restore's carry
|
|
// the snapshot's keying and are resolved against the live chain by
|
|
// resolveFxRestore before any write.
|
|
struct TrackPlan {
|
|
std::vector<TrackFlagOp> flags;
|
|
std::vector<FxOfflineOp> fxOffline;
|
|
};
|
|
|
|
// The plan for a toggle to a target mode. Parents and show-both leaves never
|
|
// appear (derived-visible, never parked — see visibleTracks). Untagged
|
|
// leaves DO appear: an untagged leaf is an Arrange member, so it parks in
|
|
// every non-Arrange mode and restores in Arrange.
|
|
struct TogglePlan {
|
|
std::vector<TrackPlan> park; // inactive leaves -> parked (fixed zeros)
|
|
std::vector<TrackPlan> restore; // active leaves returning -> snapshot values
|
|
|
|
// Per managed lane, the C_LANEPLAYS state for the target mode. Managed
|
|
// lanes only. Empty when no fixed lanes exist, so a lane-free project
|
|
// produces an identical plan to before fixed-lane support.
|
|
std::vector<LanePlayOp> lanes;
|
|
};
|
|
|
|
// Owns the mode registry, membership index, active mode, and durable
|
|
// per-track snapshots (kept while 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_; }
|
|
view::SoloCache& soloCache() { return soloCache_; }
|
|
const view::SoloCache& soloCache() const { return soloCache_; }
|
|
|
|
const std::string& activeModeId() const { return activeModeId_; }
|
|
// Returns false (no change) if the id is not registered.
|
|
bool setActiveMode(const std::string& modeId);
|
|
|
|
// The shell calls store before it parks a track, 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_; }
|
|
|
|
// Drops every snapshot whose GUID is NOT in `liveGuids`, and prunes the solo
|
|
// cache the same way (see SoloCache::reconcile). Returns the count of
|
|
// SNAPSHOTS removed — the solo cache's own count is available from it directly.
|
|
//
|
|
// Snapshots are pruned, membership is not: a parked track's snapshot is
|
|
// dead weight once the track is deleted (can never restore; a reused GUID
|
|
// would drive an incorrect restore). Membership survives because REAPER's
|
|
// undo of a track delete restores the SAME GUID — dropping the tag on
|
|
// delete would lose it on undo. A never-restored track leaves only a
|
|
// dormant membership entry, which is a fine trade against losing tags on
|
|
// undo. Folder restructure is self-healing (tree rebuilt every toggle) and
|
|
// is not what this handles.
|
|
std::size_t reconcile(const std::set<std::string>& liveGuids);
|
|
|
|
// A leaf belongs if tagged into modeId, show-both, or untagged with modeId
|
|
// == Arrange. No parent derivation here — see visibleTracks for that.
|
|
bool leafBelongsToMode(const std::string& guid, const std::string& modeId) const;
|
|
|
|
// Tree-aware visible set: active leaves, show-both leaves, and every
|
|
// parent that belongs to the mode itself or has a visible descendant.
|
|
// Untagged nodes count as Arrange. Stale tree GUIDs are tolerated; the
|
|
// master is not represented (always visible, untouched).
|
|
std::set<std::string> visibleTracks(const FolderTree& tree,
|
|
const std::string& modeId) const;
|
|
|
|
// Enumerates every leaf in `tree`; a leaf inactive in `targetMode` is
|
|
// parked (fixed zeros), one becoming active with a stored snapshot is
|
|
// restored from it. Parents and show-both leaves are never parked.
|
|
// Untagged leaves are Arrange members and park/restore accordingly. Tree
|
|
// membership is the enumeration source, so stale membership GUIDs absent
|
|
// from the tree are ignored.
|
|
//
|
|
// Park plans here carry an empty fxOffline vector — the D2 shell expands
|
|
// per-FX offline writes via TrackFX_GetCount (not available to the pure model).
|
|
TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;
|
|
|
|
// Managed lanes only, from the ownership index — the set a toggle may
|
|
// drive. Independent of the folder tree (lane ownership isn't a tree
|
|
// property); the target mode decides each lane's play VALUE, not the set.
|
|
std::set<LaneRef> lanesTouchedByToggle() const;
|
|
|
|
bool operator==(const ViewModeModel& o) const;
|
|
|
|
std::string serialize() const;
|
|
|
|
// std::nullopt on malformed input. deserialize(serialize(x)) == x on success.
|
|
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
|
|
view::SoloCache soloCache_; // modeId -> guid -> raw I_SOLO
|
|
};
|
|
|
|
// Fixed-zero park plan for one leaf, offlining `fxCount` slots.
|
|
TrackPlan makeParkPlan(const std::string& guid, int fxCount);
|
|
|
|
// Restore plan for one leaf from its snapshot — every flag to its captured
|
|
// value, never a default.
|
|
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
|
|
|
|
// Auto-tag decision: new content takes the active mode at creation; the
|
|
// Wave-2 shell diffs GUIDs on the panel timer and asks this what to tag.
|
|
// Pre-existing content never reaches here.
|
|
//
|
|
// Manual-lane exemption: an item landing on a MANUAL lane is off-limits.
|
|
//
|
|
// Adoption (strand fix): a new item on a track that already carries
|
|
// pre-existing content adopts that content's single mode rather than blindly
|
|
// taking the active mode — otherwise the track would go multi-mode, get
|
|
// lane-split, and strand the pre-existing (previously visible) items on a
|
|
// silenced lane with no user intent. Falls back to the active mode only when
|
|
// the track has no pre-existing managed-eligible content, or that content
|
|
// already spans multiple modes (an existing deliberate split).
|
|
|
|
// One new item the shell detected this poll.
|
|
struct NewItem {
|
|
std::string guid;
|
|
bool onManualLane = false; // true ⇒ EXEMPT from auto-tag
|
|
// Distinct modes the pre-existing (not-new-this-tick) content on this
|
|
// item's track resolves to. Empty ⇒ take active mode. Exactly one ⇒
|
|
// adopt it. More than one ⇒ already a deliberate split, take active mode.
|
|
std::set<std::string> trackModes;
|
|
};
|
|
|
|
// One membership write: tag `guid` into `modeId`.
|
|
struct AutoTag {
|
|
std::string guid;
|
|
std::string modeId;
|
|
|
|
bool operator==(const AutoTag& o) const { return guid == o.guid && modeId == o.modeId; }
|
|
};
|
|
|
|
// Every new track is tagged to `activeMode`. Every new item is tagged unless
|
|
// exempt (manual lane); its target is the adopted single mode of its track's
|
|
// pre-existing content, else `activeMode`. Empty `activeMode` yields no tags.
|
|
// Empty GUIDs are skipped. Mutates nothing.
|
|
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
|
const std::vector<NewItem>& newItems,
|
|
const std::string& activeMode);
|
|
|
|
// Item-level mode-move decision (bindable "Move selected items -> mode"
|
|
// actions): which selected items to retag, and to what. Manual-lane items
|
|
// (shell-reported `onManualLane`) are exempt — never retagged, never re-laned,
|
|
// upholding the managed-lanes-only invariant under an explicit user action too.
|
|
|
|
// One selected item the shell reports for the retag decision.
|
|
struct RetagItem {
|
|
std::string guid;
|
|
bool onManualLane = false; // true ⇒ EXEMPT
|
|
};
|
|
|
|
// One membership op: `untag` removes the item (Arrange default); otherwise
|
|
// tags it into `modeId`.
|
|
struct ItemRetagOp {
|
|
std::string guid;
|
|
bool untag = false; // true ⇒ untag; false ⇒ tag into modeId
|
|
std::string modeId; // the target mode when !untag (empty when untag)
|
|
|
|
bool operator==(const ItemRetagOp& o) const {
|
|
return guid == o.guid && untag == o.untag && modeId == o.modeId;
|
|
}
|
|
};
|
|
|
|
// Empty `targetMode` means untag (Move -> Arrange and Untag collapse to the
|
|
// same act, mirroring the track-level doUntag). Manual-lane and empty-GUID
|
|
// items are skipped. Mutates nothing.
|
|
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
|
const std::string& targetMode);
|
|
|
|
// Lane-minting decision (D2 Wave 3): once a track is visible in more than one
|
|
// mode while carrying its own media, whole-track parking can no longer keep
|
|
// stances separate, so it drops to fixed lanes — one managed lane per
|
|
// involved mode, each item assigned to its mode's lane.
|
|
//
|
|
// "Visible in more than one mode" has two independent triggers, either
|
|
// splits the track: (a) the track's own items span >= 2 modes, or (b) the
|
|
// track is a content-bearing folder derived-visible in >= 2 modes
|
|
// (visibleTracks) even though its own item is single-mode — the folder case
|
|
// a naive own-item-span check would miss.
|
|
//
|
|
// Show-both tracks are skipped outright (never force-split — the point of
|
|
// show-both is staying audible everywhere). Manual-lane items are exempt.
|
|
// Lanes are minted LAZILY — only for modes the track's own items actually
|
|
// occupy, never an empty reserved lane for a merely-derived-visible mode;
|
|
// confinement still holds because an absent lane never plays.
|
|
//
|
|
// Idempotent: re-reporting an already-split track yields the same mints and
|
|
// assignments, so re-running detection does not thrash the project or undo
|
|
// history.
|
|
|
|
// One item the shell reports for the minting decision.
|
|
struct LaneItem {
|
|
std::string guid;
|
|
std::string modeId; // the mode this item's content belongs to
|
|
bool onManualLane = false; // true ⇒ EXEMPT (user's hand-managed lane)
|
|
};
|
|
|
|
// One track the shell reports: its GUID plus the items on it.
|
|
struct LaneTrack {
|
|
std::string trackGuid;
|
|
std::vector<LaneItem> items;
|
|
};
|
|
|
|
// One item→lane assignment the shell must apply (I_FIXEDLANE = the lane the
|
|
// durable key `laneKey` currently occupies). Only managed-eligible items appear.
|
|
struct LaneAssign {
|
|
std::string itemGuid;
|
|
std::string trackGuid;
|
|
std::string laneKey; // durable managed-lane key (laneNameForMode(modeId))
|
|
|
|
bool operator==(const LaneAssign& o) const {
|
|
return itemGuid == o.itemGuid && trackGuid == o.trackGuid && laneKey == o.laneKey;
|
|
}
|
|
};
|
|
|
|
// One managed lane the shell must mint: its durable key (== the P_LANENAME to
|
|
// stamp) and the mode that owns it (an ownership-index write).
|
|
struct LaneMint {
|
|
std::string trackGuid;
|
|
std::string laneKey; // == laneNameForMode(modeId); the P_LANENAME to stamp
|
|
std::string modeId; // the owning mode (ownership-index managed-for-mode write)
|
|
|
|
bool operator==(const LaneMint& o) const {
|
|
return trackGuid == o.trackGuid && laneKey == o.laneKey && modeId == o.modeId;
|
|
}
|
|
};
|
|
|
|
// The complete plan; empty when no track needs splitting (D1 behavior
|
|
// unchanged). The shell wraps application in one Undo block (visible
|
|
// structural mutation).
|
|
struct LaneMintPlan {
|
|
// Tracks to switch into fixed-lane mode (I_FREEMODE=2, I_NUMFIXEDLANES >=
|
|
// laneCount). Idempotent — an already-split track still appears, but the
|
|
// shell's ensure is then a no-op.
|
|
struct TrackSplit {
|
|
std::string trackGuid;
|
|
int laneCount = 0; // number of managed lanes this track needs
|
|
};
|
|
std::vector<TrackSplit> splits;
|
|
std::vector<LaneMint> mints; // managed lanes to mint (name + ownership write)
|
|
std::vector<LaneAssign> assigns; // item→managed-lane assignments
|
|
|
|
bool empty() const {
|
|
return splits.empty() && mints.empty() && assigns.empty();
|
|
}
|
|
};
|
|
|
|
// `model` supplies membership + show-both state; `tree` supplies folder
|
|
// structure for the derived-visibility trigger. Items with an empty GUID or
|
|
// modeId are skipped (defensive). Mutates nothing.
|
|
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
|
const std::vector<LaneTrack>& tracks);
|
|
|
|
// Next mode id in ordinal order, cycling past `currentModeId` and wrapping
|
|
// after the last. Empty registry -> "". currentModeId not present -> the
|
|
// first mode's id. Free function (not a model member) so it is testable
|
|
// against a bare ModeRegistry.
|
|
std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId);
|
|
|
|
} // namespace reasampler
|