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:
+19
-4
@@ -753,8 +753,11 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
// persist.cpp writes it on the next project save alongside the bank and view state, the
|
||||
// same way an action-driven tag is persisted. Wrapping this in an Undo block would flood
|
||||
// the REAPER undo history with a new entry for every timer tick that sees new content.
|
||||
void detectNewContent() {
|
||||
if (!g_panel.session) return;
|
||||
// Returns true iff this tick tagged at least one new GUID into a mode — the signal the
|
||||
// caller uses to decide whether to run the lane-minting pass (a track can only newly
|
||||
// become multi-mode when auto-tag just placed content on it). No tag ⇒ nothing to mint.
|
||||
bool detectNewContent() {
|
||||
if (!g_panel.session) return false;
|
||||
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
|
||||
@@ -778,7 +781,7 @@ void detectNewContent() {
|
||||
enumerateLiveGuids(proj, live, itemOnManualLane);
|
||||
|
||||
const std::vector<std::string> added = g_panel.contentBaseline.observe(live);
|
||||
if (added.empty()) return; // first poll after open, or nothing new this tick
|
||||
if (added.empty()) return false; // first poll after open, or nothing new this tick
|
||||
|
||||
// Split the new GUIDs into tracks vs items so the pure decision can apply the
|
||||
// manual-lane exemption to items only. A GUID present in the item-lane map is an
|
||||
@@ -799,6 +802,7 @@ void detectNewContent() {
|
||||
autoTagNewContent(newTracks, newItems, model.activeModeId());
|
||||
for (const AutoTag& tag : tags)
|
||||
model.membership().tag(tag.guid, tag.modeId);
|
||||
return !tags.empty();
|
||||
}
|
||||
|
||||
// --- Audition preview ---------------------------------------------------------
|
||||
@@ -1245,7 +1249,18 @@ void bankPanelRefresh() {
|
||||
// tracks/items are created in the arrange view, not the panel, so detection must
|
||||
// not be gated on the dock being visible. READ-ONLY on the project; only mutates
|
||||
// the in-memory membership index (persist saves it like any action-driven tag).
|
||||
detectNewContent();
|
||||
const bool tagged = detectNewContent();
|
||||
|
||||
// Lane minting (D2 Wave 3) runs ONLY when detection just tagged new content — a
|
||||
// track can only newly become multi-mode when auto-tag placed content on it. Unlike
|
||||
// the invisible membership tag above, minting is a visible structural mutation
|
||||
// (I_FREEMODE/I_FIXEDLANE/P_LANENAME), so mintManagedLanes wraps it in its own Undo
|
||||
// block and only mints for tracks that hold >1 mode's content — a single-mode track
|
||||
// is left to D1 whole-track parking. Managed lanes only; manual lanes untouched.
|
||||
if (tagged && g_panel.session) {
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
mintManagedLanes(g_panel.session->view(), proj);
|
||||
}
|
||||
|
||||
if (!g_panel.open || !g_panel.hwnd) return;
|
||||
// Repaint only when the bank actually changed (generation bump). Cheap tick
|
||||
|
||||
@@ -31,6 +31,13 @@ std::string laneNameForMode(const std::string& modeId) {
|
||||
return std::string(kManagedLanePrefix) + modeId;
|
||||
}
|
||||
|
||||
std::optional<std::string> modeIdFromLaneName(const std::string& laneName) {
|
||||
if (!hasManagedPrefix(laneName)) return std::nullopt; // manual/unnamed ⇒ no mode
|
||||
const std::size_t n = std::strlen(kManagedLanePrefix);
|
||||
if (laneName.size() == n) return std::nullopt; // prefix only, no mode suffix (illegal)
|
||||
return laneName.substr(n);
|
||||
}
|
||||
|
||||
bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName) {
|
||||
// On a normal (non-fixed-lane) track there is no concept of a manual lane; the
|
||||
// item follows the normal auto-tag rule.
|
||||
|
||||
@@ -58,6 +58,14 @@ std::optional<std::string> managedLaneKey(const std::string& laneName);
|
||||
// contract is asserted here so minting and reading cannot drift.
|
||||
std::string laneNameForMode(const std::string& modeId);
|
||||
|
||||
// The owning mode id encoded in a managed lane NAME — the suffix after the managed
|
||||
// prefix. std::nullopt for a manual/unnamed lane (no managed prefix) or a name that is
|
||||
// EXACTLY the prefix with no mode suffix (illegal — a managed lane always names a mode).
|
||||
// The exact inverse of laneNameForMode: modeIdFromLaneName(laneNameForMode(m)) == m.
|
||||
// Used by the load-time reconcile to recover managed ownership from REAPER's durable
|
||||
// lane name (the source of truth for identity across sessions — design point #2).
|
||||
std::optional<std::string> modeIdFromLaneName(const std::string& laneName);
|
||||
|
||||
// True iff an item on a fixed-lane track with the given lane name is on a MANUAL lane
|
||||
// (i.e. exempt from auto-tag). The two inputs are:
|
||||
// isFixedLaneTrack — whether the item's track has I_FREEMODE==2. On a normal
|
||||
|
||||
@@ -224,6 +224,12 @@ static void OnTimer()
|
||||
// re-arm and the model restore ride the one authoritative load event.
|
||||
if (g_session.consumeLoadSignal()) {
|
||||
reasampler::bankPanelNotifyProjectLoaded();
|
||||
// Reconcile the restored lane-ownership index against the live project's lanes
|
||||
// FIRST (via REAPER's durable P_LANENAME — the cross-session source of truth),
|
||||
// so a saved lane-split project's managed/manual classification is correct
|
||||
// before the active mode's lane visibility is reapplied. Never re-mints, never
|
||||
// mass-tags — it only records managed ownership recovered from lane names.
|
||||
reasampler::reconcileManagedLanes(g_session.view(), nullptr);
|
||||
reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr);
|
||||
}
|
||||
|
||||
|
||||
+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
|
||||
|
||||
+37
@@ -52,4 +52,41 @@ namespace reasampler {
|
||||
// registered mode. `proj` may be nullptr to mean REAPER's current project.
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
||||
|
||||
// Mints managed fixed lanes for any track in `proj` that now holds content of MORE
|
||||
// THAN ONE mode, and assigns each item to its mode's managed lane (Phase D2 Wave 3).
|
||||
// 1. Enumerates every track + its items; resolves each item's mode from the model's
|
||||
// membership (untagged ⇒ Arrange) and reads whether it currently sits on a MANUAL
|
||||
// lane (exempt).
|
||||
// 2. Runs the pure planLaneMinting decision. A track with content of only one mode
|
||||
// is left whole-track-parked (D1) — NOT lane-split.
|
||||
// 3. For each track that must split: enables fixed-lane mode (I_FREEMODE=2), ensures
|
||||
// enough fixed lanes (I_NUMFIXEDLANES), stamps each managed lane's durable name
|
||||
// (P_LANENAME:n), records the lane MANAGED-for-its-mode in the model's ownership
|
||||
// index, and assigns each managed-eligible item to its mode's lane (I_FIXEDLANE).
|
||||
// Manual lanes and the items on them are NEVER minted-over or reassigned.
|
||||
// 4. Reapplies the active mode's lane visibility so the just-minted lanes take their
|
||||
// correct play/show state immediately (the active mode's lane plays; others hide).
|
||||
// The whole structural mutation is wrapped in ONE Undo_BeginBlock2/EndBlock2 — but only
|
||||
// when the plan is non-empty (no undo point for a tick that mints nothing).
|
||||
//
|
||||
// Returns true if any lane was minted this call (⇒ the caller may want a repaint).
|
||||
// `proj` may be nullptr to mean REAPER's current project. READ of the membership index
|
||||
// only; the sole model mutation is recording new managed-lane ownership.
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
// Reconciles the model's lane-ownership index against the live project's lanes on
|
||||
// project open (Phase D2 Wave 3). REAPER's durable P_LANENAME is the source of truth for
|
||||
// lane identity across sessions (design point #2): a lane whose name carries the managed
|
||||
// prefix is tool-managed and owned by the mode encoded in that name. This walks every
|
||||
// track's lanes and records each managed-named lane MANAGED-for-its-mode in the index —
|
||||
// self-healing a saved project's classification WITHOUT re-minting (it never creates a
|
||||
// lane, changes I_FREEMODE/I_NUMFIXEDLANES, or reassigns an item) and WITHOUT mass-
|
||||
// tagging (it never touches membership). A lane without the managed prefix is left
|
||||
// untouched (manual by default). Reload's active-mode lane visibility is then reapplied
|
||||
// by the caller's applyMode, mirroring D1's reapply-on-open.
|
||||
//
|
||||
// `proj` may be nullptr to mean REAPER's current project. The only model mutation is
|
||||
// recording managed ownership recovered from durable lane names.
|
||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#include "lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention
|
||||
|
||||
// view_mode_model implementation.
|
||||
//
|
||||
// JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief:
|
||||
@@ -156,6 +158,56 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
return tags;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// lane minting decision
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LaneMintPlan planLaneMinting(const std::vector<LaneTrack>& tracks) {
|
||||
LaneMintPlan plan;
|
||||
|
||||
for (const LaneTrack& track : tracks) {
|
||||
if (track.trackGuid.empty()) continue;
|
||||
|
||||
// Collect the DISTINCT modes the track's managed-eligible items belong to, in
|
||||
// deterministic (sorted) order so the mint list and lane count are stable across
|
||||
// runs (a set orders by mode id). Items on a manual lane are EXEMPT — never
|
||||
// counted toward the multi-mode test and never reassigned (the managed-only
|
||||
// invariant, upheld at the source of the decision).
|
||||
std::set<std::string> involvedModes;
|
||||
for (const LaneItem& item : track.items) {
|
||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||
if (item.onManualLane) continue; // exempt — user's hand-managed lane
|
||||
involvedModes.insert(item.modeId);
|
||||
}
|
||||
|
||||
// Single-mode (or empty) track: whole-track parking (D1) still separates the
|
||||
// stances. NO split, NO mint, NO assignment — this is the load-bearing
|
||||
// "don't lane-split single-mode tracks" rule.
|
||||
if (involvedModes.size() < 2) continue;
|
||||
|
||||
// Multi-mode track: transition to lane-split. One managed lane per involved
|
||||
// mode (durable key = laneNameForMode(mode)), owned by that mode.
|
||||
plan.splits.push_back(LaneMintPlan::TrackSplit{
|
||||
track.trackGuid, static_cast<int>(involvedModes.size())});
|
||||
for (const std::string& mode : involvedModes) {
|
||||
plan.mints.push_back(
|
||||
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
||||
}
|
||||
|
||||
// Assign EVERY managed-eligible item onto its mode's lane — including the
|
||||
// pre-existing single-mode items, so a track that just gained a second mode
|
||||
// retroactively lanes all of its content, not only the newly-added item.
|
||||
for (const LaneItem& item : track.items) {
|
||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||
if (item.onManualLane) continue; // exempt — never reassigned
|
||||
plan.assigns.push_back(LaneAssign{
|
||||
item.guid, track.trackGuid, laneNameForMode(item.modeId)});
|
||||
}
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// planner helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -519,6 +519,113 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
const std::vector<NewItem>& newItems,
|
||||
const std::string& activeMode);
|
||||
|
||||
// -- Lane minting decision (Phase D2 / Wave 3) -------------------------------
|
||||
//
|
||||
// D1 parks a whole track when it holds content of only ONE mode. The moment a track
|
||||
// would carry content of MORE THAN ONE mode, whole-track parking can no longer keep
|
||||
// the stances separate (the track is visible in every mode its content belongs to),
|
||||
// so the projection drops to the ITEM level: the track becomes a fixed-lane track,
|
||||
// each involved mode gets its own MANAGED lane, and each item is assigned to its
|
||||
// mode's lane. A toggle then shows+plays only the active mode's lane.
|
||||
//
|
||||
// This is the pure DECISION behind that transition — REAPER-free and unit-tested.
|
||||
// The shell reads each track's items and their live mode+lane disposition, calls this,
|
||||
// and applies the resulting REAPER writes (I_FREEMODE / I_NUMFIXEDLANES / P_LANENAME /
|
||||
// I_FIXEDLANE) plus the ownership-index writes. The DECISION never lives in the shell.
|
||||
//
|
||||
// THE MANAGED-LANES-ONLY INVARIANT is upheld here at the source: an item the shell
|
||||
// reports as already on a MANUAL lane is EXEMPT — it is never counted toward the
|
||||
// multi-mode test, never reassigned, and its lane is never minted-over. The plan only
|
||||
// ever names lanes with the managed prefix (laneNameForMode) and only ever moves
|
||||
// managed-eligible items. A track the user already lane-splits for their own comping
|
||||
// is handled by minting ADDITIONAL managed lanes alongside the user's manual lanes;
|
||||
// the manual lanes and the items on them are untouched (they are reported exempt).
|
||||
|
||||
// One item the shell reports for the minting decision: its GUID, the mode its
|
||||
// membership resolves to (untagged ⇒ Arrange, resolved by the shell via
|
||||
// leafBelongsToMode / the active-mode default), and whether it currently sits on a
|
||||
// MANUAL lane (⇒ exempt: never counted, never reassigned).
|
||||
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. The shell builds this by
|
||||
// enumerating the track's media items and resolving each item's mode from membership.
|
||||
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; the shell resolves key→ordinal exactly as the
|
||||
// C_LANEPLAYS apply path does). Only managed-eligible items appear here.
|
||||
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 on a track: its durable key (== the name to
|
||||
// stamp via P_LANENAME) and the mode that owns it (recorded in the ownership index).
|
||||
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 lane-minting plan for the tracks the shell reported. Empty (all three
|
||||
// vectors) when NO track needs splitting — a single-mode-only project produces an empty
|
||||
// plan and the shell does nothing (D1 behavior unchanged). The shell wraps the whole
|
||||
// application in ONE Undo block because it is a visible structural mutation.
|
||||
struct LaneMintPlan {
|
||||
// Tracks to switch into fixed-lane mode, each with the number of managed lanes to
|
||||
// ensure (I_FREEMODE=2, I_NUMFIXEDLANES >= laneCount). Only tracks that need a
|
||||
// split appear; a track already carrying the tool's managed lanes for exactly the
|
||||
// involved modes still appears (idempotent — the shell's ensure is a no-op then).
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
// The pure lane-minting decision. For each reported track:
|
||||
// * Ignore items on manual lanes entirely (exempt — the managed-only invariant).
|
||||
// * Collect the DISTINCT modes the remaining (managed-eligible) items belong to.
|
||||
// * If that set has < 2 modes, the track stays whole-track-parked (D1) — NO split,
|
||||
// NO mint, NO assignment. This is the single-mode-track rule.
|
||||
// * If it has >= 2 modes, the track transitions to lane-split: emit one TrackSplit
|
||||
// (laneCount == number of involved modes), one LaneMint per involved mode (durable
|
||||
// key laneNameForMode(mode), owned by that mode), and one LaneAssign per managed-
|
||||
// eligible item onto its mode's lane — INCLUDING the pre-existing items, so a
|
||||
// single-mode track that just gained a second mode retroactively lanes ALL its
|
||||
// items, not only the newly-added one.
|
||||
//
|
||||
// Items with an empty GUID or empty modeId are skipped (defensive; a real item always
|
||||
// resolves to a mode). The function mutates nothing — it returns a plan the shell
|
||||
// applies. Idempotency: re-reporting an already-split track yields the same mints and
|
||||
// assignments; the shell's ensure/assign writes are no-ops when the state already
|
||||
// matches, so re-running the detection path does not thrash the project or the undo
|
||||
// history (the shell only opens an Undo block when the plan is non-empty AND some
|
||||
// write actually changes state — see the shell).
|
||||
LaneMintPlan planLaneMinting(const std::vector<LaneTrack>& tracks);
|
||||
|
||||
// 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).
|
||||
|
||||
Reference in New Issue
Block a user