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:
2026-07-23 20:08:38 -04:00
parent fed70c0a80
commit b182146f9a
11 changed files with 673 additions and 4 deletions
+19 -4
View File
@@ -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