feat(view): D2 Wave 2 — apply managed-lane ops + timer-diff auto-tag new content

Drives fixed-lane C_LANEPLAYS for managed lanes only (name-keyed, ordinal-renumber
safe) in the view shell; diffs live track/item GUIDs on the panel timer to auto-tag
new content to the active mode, first-poll-guarded. Pure guid_diff + lane_keys
modules unit-tested; folds in Wave-1 polish.
This commit is contained in:
2026-07-23 17:32:22 -04:00
parent af5f7ee912
commit af34143107
11 changed files with 773 additions and 4 deletions
+29 -4
View File
@@ -1,10 +1,12 @@
#include "view_mode_model.h"
#include <algorithm>
#include <cassert>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <utility>
// view_mode_model implementation.
//
@@ -121,6 +123,13 @@ int laneModeState(const std::string& managedMode, const std::string& activeMode)
// and hidden (C_LANEPLAYS = 0). Exclusive membership: only one stance's lane at a
// time. Show-both, which keeps a lane audible across modes, is a per-lane opt-out
// the shell layers on; the default per-mode decision here is exclusive.
//
// EXCLUSIVITY ASSUMPTION (one managed lane per mode per track): the model assumes a
// given (track, mode) owns AT MOST ONE managed lane. C_LANEPLAYS=1 means "this lane
// plays EXCLUSIVELY" — two lanes on the same track both claiming mode M would both
// be told to play exclusively on M's toggle, which REAPER cannot honor coherently
// (the last write wins in the DAW). The Wave-3 lane-minting path is responsible for
// upholding one-lane-per-(track,mode); planToggle asserts it in debug builds.
return managedMode == activeMode ? kLanePlaysExclusive : kLaneSilent;
}
@@ -319,8 +328,20 @@ TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string&
// "never touch mute/solo"). Lane ownership is not a tree property, so this walks the
// ownership index directly, not the FolderTree; a project with no fixed lanes leaves
// plan.lanes empty and the plan is byte-identical to a D1 plan.
#ifndef NDEBUG
// Debug-time guard for the one-managed-lane-per-mode-per-track exclusivity
// assumption (see laneModeState). Two managed lanes on the same track claiming the
// same mode would both be told to play exclusively on that mode's toggle, which
// REAPER cannot honor. Cheap set membership over the (usually tiny) managed-lane
// set; compiled out of release builds.
std::set<std::pair<std::string, std::string>> seenTrackMode; // (trackGuid, mode)
#endif
for (const auto& [ref, ownership] : lanes_.all()) {
if (!ownership.isManaged()) continue; // manual lanes are off-limits
#ifndef NDEBUG
assert(seenTrackMode.insert({ref.trackGuid, *ownership.managedMode}).second &&
"two managed lanes on one track claim the same mode (exclusivity broken)");
#endif
const int lanePlays = laneModeState(*ownership.managedMode, targetMode);
plan.lanes.push_back(LanePlayOp{ref.trackGuid, ref.laneKey, lanePlays});
}
@@ -448,7 +469,8 @@ std::string ViewModeModel::serialize() const {
{
bool first = true;
for (const auto& [guid, mem] : membership_.all()) {
if (!first) out += ','; first = false;
if (!first) out += ',';
first = false;
ObjWriter e(out);
e.keyStr("guid", guid);
e.keyBegin("modes");
@@ -456,7 +478,8 @@ std::string ViewModeModel::serialize() const {
{
bool mf = true;
for (const auto& id : mem.modeIds) {
if (!mf) out += ','; mf = false;
if (!mf) out += ',';
mf = false;
writeEscaped(out, id);
}
}
@@ -472,7 +495,8 @@ std::string ViewModeModel::serialize() const {
{
bool first = true;
for (const auto& [guid, snap] : snapshots_) {
if (!first) out += ','; first = false;
if (!first) out += ',';
first = false;
ObjWriter e(out);
e.keyStr("guid", guid);
e.keyRaw("showInTcp", intToStr(snap.showInTcp));
@@ -494,7 +518,8 @@ std::string ViewModeModel::serialize() const {
{
bool first = true;
for (const auto& [ref, ownership] : lanes_.all()) {
if (!first) out += ','; first = false;
if (!first) out += ',';
first = false;
ObjWriter e(out);
e.keyStr("trackGuid", ref.trackGuid);
e.keyStr("laneKey", ref.laneKey);