feat(view): mint managed lanes to separate cross-mode content per stance (D2 W3-A)
Pure planLaneMinting decides which tracks hold >1 mode's content and which managed lane each item lands on; view.cpp applies it (I_FREEMODE/I_NUMFIXEDLANES/P_LANENAME/ I_FIXEDLANE) under one undo block, driven off the auto-tag detection tick. Manual lanes and their items are never touched. Load-time reconcile rebuilds ownership from durable lane names before reapplying active-mode visibility.
This commit is contained in:
+261
@@ -36,6 +36,13 @@
|
||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||
#define REAPERAPI_WANT_UpdateArrange
|
||||
#define REAPERAPI_WANT_UpdateTimeline
|
||||
// Lane minting (D2 Wave 3): enumerate a track's items and read/write item-side lane
|
||||
// state to assign each item to its mode's managed lane.
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaItemInfo_String
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
@@ -236,6 +243,188 @@ bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handle
|
||||
return touchedFreeMode;
|
||||
}
|
||||
|
||||
// -- Managed-lane minting (D2 Wave 3) ----------------------------------------
|
||||
//
|
||||
// Mints one managed fixed lane per mode on any track that now holds content of MORE
|
||||
// THAN ONE mode, and assigns each item to its mode's managed lane. The DECISION —
|
||||
// which tracks split, which lanes to mint, which item goes where — is the pure
|
||||
// planLaneMinting; this shell only reads live per-item mode+lane state, calls the
|
||||
// decision, and applies the resulting REAPER + ownership-index writes.
|
||||
|
||||
// An item's canonical GUID string via GetSetMediaItemInfo_String("GUID"). Empty on
|
||||
// failure. Mirrors bank_panel.cpp's itemGuid — the same read seam for item identity.
|
||||
std::string itemGuidString(MediaItem* it) {
|
||||
char buf[64] = {0};
|
||||
if (!GetSetMediaItemInfo_String(it, "GUID", buf, false)) return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// The durable P_LANENAME of the lane item `it` currently sits on, for a track known to
|
||||
// be a fixed-lane track. Empty if unnamed/unavailable. Same derivation as bank_panel's
|
||||
// itemLaneName; kept local so view.cpp stays self-contained.
|
||||
std::string itemLaneNameOf(MediaTrack* tr, MediaItem* it) {
|
||||
const int laneIdx = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
return laneName(tr, laneIdx);
|
||||
}
|
||||
|
||||
// Maps every item GUID on `tr` to its MediaItem* handle, in one pass. The assign pass
|
||||
// resolves plan item GUIDs back to handles through this map rather than re-scanning the
|
||||
// track per item (avoids the quadratic that a per-item find would incur).
|
||||
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||
std::map<std::string, MediaItem*> byGuid;
|
||||
const int itemCount = CountTrackMediaItems(tr);
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||
if (!it) continue;
|
||||
std::string ig = itemGuidString(it);
|
||||
if (!ig.empty()) byGuid.emplace(std::move(ig), it);
|
||||
}
|
||||
return byGuid;
|
||||
}
|
||||
|
||||
// Resolves the mode one item's content belongs to, from the model's membership index.
|
||||
// An item tagged into exactly one mode returns that mode; an untagged item is an
|
||||
// Arrange member by default (mirrors leafBelongsToMode's untagged rule). A show-both or
|
||||
// multi-mode item resolves to its first mode id — such items are unusual for lane
|
||||
// content, and the pure decision only needs A mode per item; the managed-lane it lands
|
||||
// on is that mode's lane. Never returns empty for a real item.
|
||||
std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) {
|
||||
const std::set<std::string> modes = model.membership().modesOf(itemGuid);
|
||||
if (modes.empty()) return kArrangeModeId; // untagged ⇒ Arrange default
|
||||
return *modes.begin();
|
||||
}
|
||||
|
||||
// Builds the per-track LaneItem picture the pure decision consumes. For each track and
|
||||
// each item: resolve the item's mode from membership, and — only on a track already in
|
||||
// fixed-lane mode — read whether it sits on a MANUAL lane (exempt). On a non-fixed-lane
|
||||
// track no item is on a manual lane (isOnManualLane returns false for the empty name),
|
||||
// so the manual read is skipped entirely there.
|
||||
std::vector<LaneTrack> readLaneTracks(
|
||||
const ViewModeModel& model,
|
||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
std::vector<LaneTrack> tracks;
|
||||
tracks.reserve(handleByGuid.size());
|
||||
for (const auto& [guid, tr] : handleByGuid) {
|
||||
LaneTrack lt;
|
||||
lt.trackGuid = guid;
|
||||
|
||||
const bool fixedLane =
|
||||
static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
||||
|
||||
const int itemCount = CountTrackMediaItems(tr);
|
||||
lt.items.reserve(static_cast<std::size_t>(itemCount));
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||
if (!it) continue;
|
||||
const std::string ig = itemGuidString(it);
|
||||
if (ig.empty()) continue;
|
||||
LaneItem li;
|
||||
li.guid = ig;
|
||||
li.modeId = itemModeFromMembership(model, ig);
|
||||
// Manual-lane exemption: only meaningful on a fixed-lane track. The shared
|
||||
// pure predicate decides; on a normal track it returns false regardless of
|
||||
// name, so we pass an empty name and skip the P_LANENAME read.
|
||||
const std::string ln = fixedLane ? itemLaneNameOf(tr, it) : std::string{};
|
||||
li.onManualLane = isOnManualLane(fixedLane, ln);
|
||||
lt.items.push_back(std::move(li));
|
||||
}
|
||||
tracks.push_back(std::move(lt));
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
// Assigns item `it` to the managed lane whose durable key resolves to a current ordinal
|
||||
// on `tr` (via managedLaneOrdinals). Idempotent: writes I_FIXEDLANE only when it differs
|
||||
// from the item's current lane, so a re-run does not thrash the item or the undo state.
|
||||
// Returns true iff a write actually changed the item's lane. Non-destructive: only the
|
||||
// reversible I_FIXEDLANE flag is written — the item is never moved in time or across
|
||||
// tracks. (I_FIXEDLANE is settable per SDK: "fine to call with setNewValue".)
|
||||
bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
||||
const int current = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
if (current == laneOrdinal) return false; // already there — no-op
|
||||
SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast<double>(laneOrdinal));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Applies the pure LaneMintPlan to the live project. For each track that must split:
|
||||
// enables fixed lanes, ensures the lane count, stamps each managed lane's durable name,
|
||||
// records ownership in the model, then assigns each item to its mode's lane by resolving
|
||||
// the durable key to the lane's current ordinal. Returns true if ANY project write
|
||||
// changed state (⇒ the caller keeps the Undo block and refreshes the timeline).
|
||||
//
|
||||
// MANAGED-LANES-ONLY: the plan only ever names lanes with the managed prefix and only
|
||||
// ever assigns managed-eligible items (manual-lane items were reported exempt and are
|
||||
// absent from the plan). We only ever GROW I_NUMFIXEDLANES to fit the managed lanes and
|
||||
// stamp names on the lanes we mint — a user's existing manual lanes keep their ordinals
|
||||
// below/around ours and are never renamed or reassigned.
|
||||
bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
bool changed = false;
|
||||
|
||||
// Group mints + assigns by track so each track is set up once.
|
||||
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
||||
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
||||
std::map<std::string, std::vector<const LaneAssign*>> assignsByTrack;
|
||||
for (const LaneAssign& a : plan.assigns) assignsByTrack[a.trackGuid].push_back(&a);
|
||||
|
||||
for (const LaneMintPlan::TrackSplit& split : plan.splits) {
|
||||
MediaTrack* tr = resolve(handleByGuid, split.trackGuid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
|
||||
// Enable fixed-lane mode if not already (SDK: UpdateTimeline() owed after).
|
||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Ensure enough lanes for the managed set WITHOUT shrinking: a track may already
|
||||
// carry the user's manual lanes, so only GROW the count, never reduce it (which
|
||||
// would delete a user lane). The managed lanes we mint occupy the tail ordinals.
|
||||
const int haveLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
|
||||
// Which managed keys are already present on this track (durable-name reconcile).
|
||||
std::map<std::string, int> present = managedLaneOrdinals(tr);
|
||||
|
||||
// Mint each managed lane that is not already present, appending at the tail so an
|
||||
// existing manual lane is never overwritten. Record ownership in the model.
|
||||
int nextOrdinal = haveLanes;
|
||||
for (const LaneMint* m : mintsByTrack[split.trackGuid]) {
|
||||
model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership
|
||||
if (present.count(m->laneKey)) continue; // already minted — idempotent
|
||||
|
||||
// Grow the lane count to include the new tail ordinal, then stamp its name.
|
||||
const int laneIdx = nextOrdinal++;
|
||||
if (laneIdx >= static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"))) {
|
||||
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES",
|
||||
static_cast<double>(laneIdx + 1));
|
||||
}
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||
std::vector<char> name(m->laneKey.begin(), m->laneKey.end());
|
||||
name.push_back('\0');
|
||||
GetSetMediaTrackInfo_String(tr, parm, name.data(), true);
|
||||
present.emplace(m->laneKey, laneIdx); // now resolvable for the assign pass
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Assign each item to its mode's managed lane, resolving the durable key to the
|
||||
// lane's current ordinal on THIS track. A key not present (shouldn't happen — we
|
||||
// just minted them all) is skipped rather than mis-assigned. Item handles are
|
||||
// resolved through a one-pass GUID map (avoids re-scanning the track per item).
|
||||
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||
const std::map<std::string, MediaItem*> itemsByGuid = itemHandlesByGuid(tr);
|
||||
for (const LaneAssign* a : assignsByTrack[split.trackGuid]) {
|
||||
auto ord = ordinals.find(a->laneKey);
|
||||
if (ord == ordinals.end()) continue; // key not live — prune, never mis-assign
|
||||
auto handle = itemsByGuid.find(a->itemGuid);
|
||||
if (handle == itemsByGuid.end()) continue; // stale item GUID — prune
|
||||
if (assignItemToLane(tr, handle->second, ord->second)) changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||
@@ -352,4 +541,76 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
||||
|
||||
// Build the live per-track item picture and run the PURE decision. A single-mode
|
||||
// track produces no split; a track that now holds >1 mode's content produces mints
|
||||
// + assignments. Manual-lane items are reported exempt inside readLaneTracks.
|
||||
const std::vector<LaneTrack> tracks = readLaneTracks(model, handleByGuid);
|
||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
||||
if (plan.empty()) return false; // nothing to mint — no Undo point for a no-op tick
|
||||
|
||||
// Wrap the structural mutation in ONE Undo block (unlike the invisible membership
|
||||
// tag). Only opened when the plan is non-empty; applyMintPlan reports whether any
|
||||
// write actually changed state so we can label the undo meaningfully.
|
||||
Undo_BeginBlock2(proj);
|
||||
const bool changed = applyMintPlan(model, plan, handleByGuid);
|
||||
|
||||
if (!changed) {
|
||||
// The plan was non-empty but every write was already satisfied (idempotent
|
||||
// re-run: lanes exist, items already assigned, ownership already recorded). Close
|
||||
// the block with no description so REAPER discards the empty undo point rather
|
||||
// than flooding history with a no-change entry every detection tick.
|
||||
Undo_EndBlock2(proj, "", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reapply the active mode's lane visibility so the freshly-minted lanes take their
|
||||
// correct play/show state immediately: the active mode's lane plays+shows, every
|
||||
// other managed lane hides+silences. Reusing planToggle's lane ops keeps the drive
|
||||
// logic in one place; applyLaneOps also (re)asserts I_FREEMODE and drives C_LANEPLAYS.
|
||||
// NOTE: applyMode is NOT reused here — it would re-park/restore whole tracks and
|
||||
// recompute parent visibility, which the minting tick must not do (it only just
|
||||
// changed item lanes). Driving lane play state directly is the minimal correct step.
|
||||
const TogglePlan togglePlan = model.planToggle(FolderTree{}, model.activeModeId());
|
||||
applyLaneOps(handleByGuid, togglePlan.lanes);
|
||||
|
||||
// I_FREEMODE was (re)set to fixed lanes on at least one track (the plan minted a
|
||||
// split), so a timeline refresh is owed (SDK). Repaint the arrange too so the new
|
||||
// lane layout appears immediately.
|
||||
UpdateTimeline();
|
||||
UpdateArrange();
|
||||
|
||||
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
||||
|
||||
// Walk every track's lanes; for each lane whose durable name carries the managed
|
||||
// prefix, record it MANAGED-for-its-mode in the ownership index. This is a pure READ
|
||||
// of REAPER state (no lane is created, no I_FREEMODE/I_NUMFIXEDLANES/I_FIXEDLANE is
|
||||
// written) plus an index write — self-healing classification from the source of
|
||||
// truth (the durable name) without re-minting or mass-tagging. A lane lacking the
|
||||
// prefix is left alone (manual by default), so a user's own lanes stay off the index.
|
||||
for (const auto& [guid, tr] : handleByGuid) {
|
||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
||||
|
||||
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
for (int lane = 0; lane < numLanes; ++lane) {
|
||||
const std::string name = laneName(tr, lane);
|
||||
std::optional<std::string> key = managedLaneKey(name);
|
||||
if (!key) continue; // manual/unnamed lane — leave off the index
|
||||
std::optional<std::string> mode = modeIdFromLaneName(name);
|
||||
if (!mode) continue; // prefix-only/illegal name — skip defensively
|
||||
model.lanes().setManaged(guid, *key, *mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user