Cut core/view and shell/view comment bloat ~65% (comments only, zero code change)
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
// guid_diff implementation — pure set arithmetic for new-content detection. See
|
// See guid_diff.h.
|
||||||
// guid_diff.h. No REAPER, no SWELL — std only.
|
|
||||||
|
|
||||||
#include "core/view/guid_diff.h"
|
#include "core/view/guid_diff.h"
|
||||||
|
|
||||||
@@ -10,8 +9,6 @@ namespace reasampler::view {
|
|||||||
std::vector<std::string> newGuids(const std::set<std::string>& previous,
|
std::vector<std::string> newGuids(const std::set<std::string>& previous,
|
||||||
const std::set<std::string>& current) {
|
const std::set<std::string>& current) {
|
||||||
std::vector<std::string> added;
|
std::vector<std::string> added;
|
||||||
// current \ previous. std::set iterates ascending, so set_difference yields a
|
|
||||||
// deterministic order without a separate sort.
|
|
||||||
for (const std::string& g : current) {
|
for (const std::string& g : current) {
|
||||||
if (g.empty()) continue; // never tag a GUID-read failure
|
if (g.empty()) continue; // never tag a GUID-read failure
|
||||||
if (previous.count(g) == 0) added.push_back(g);
|
if (previous.count(g) == 0) added.push_back(g);
|
||||||
@@ -21,24 +18,21 @@ std::vector<std::string> newGuids(const std::set<std::string>& previous,
|
|||||||
|
|
||||||
std::vector<std::string> GuidBaseline::observe(const std::set<std::string>& current) {
|
std::vector<std::string> GuidBaseline::observe(const std::set<std::string>& current) {
|
||||||
if (!primed_) {
|
if (!primed_) {
|
||||||
// First poll after open/reset: establish the baseline, report nothing new so
|
|
||||||
// pre-existing content is NOT auto-tagged (it defaults to Arrange).
|
|
||||||
baseline_ = current;
|
baseline_ = current;
|
||||||
primed_ = true;
|
primed_ = true;
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::vector<std::string> added = newGuids(baseline_, current);
|
std::vector<std::string> added = newGuids(baseline_, current);
|
||||||
// Advance the baseline to the full current set. Using `current` (not baseline_ ∪
|
// Assign `current`, not baseline_ ∪ added: a deleted GUID drops out of the
|
||||||
// added) means a DELETED GUID drops out of the baseline too, so if REAPER later
|
// baseline, so a later reused GUID is detected again rather than looking
|
||||||
// reuses that GUID for genuinely new content it is detected again — the baseline
|
// pre-existing.
|
||||||
// tracks the live set exactly, not a monotonic union.
|
|
||||||
baseline_ = current;
|
baseline_ = current;
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GuidBaseline::reset() {
|
void GuidBaseline::reset() {
|
||||||
baseline_.clear();
|
baseline_.clear();
|
||||||
primed_ = false; // next observe() re-baselines (first-poll guard re-armed)
|
primed_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace reasampler::view
|
} // namespace reasampler::view
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// guid_diff — the pure, REAPER-free core of the D2 Wave-2 new-content detection.
|
// Pure, REAPER-free new-content detection: which GUIDs appeared since the last
|
||||||
//
|
// poll. See src/core/view/CLAUDE.md for the module contract.
|
||||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
|
||||||
// vendor/ includes. Standard library only. Unit-tested outside the DAW.
|
|
||||||
//
|
|
||||||
// The shell (bank_panel timer) reads REAPER's live track/item GUID set each tick;
|
|
||||||
// this module owns the DECISION of "which GUIDs are new since the last tick" and the
|
|
||||||
// first-poll-after-open guard so pre-existing content is never mass-tagged. Keeping
|
|
||||||
// this here — rather than in the shell — means the fiddly baseline/diff logic is
|
|
||||||
// unit-tested, mirroring how view_tree splits the folder-depth walk out of view.cpp.
|
|
||||||
//
|
|
||||||
// The shell then hands the "new since last tick" GUIDs to the pure autoTagNewContent
|
|
||||||
// (view_mode_model) to produce the membership writes.
|
|
||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -19,44 +8,25 @@
|
|||||||
|
|
||||||
namespace reasampler::view {
|
namespace reasampler::view {
|
||||||
|
|
||||||
// The GUIDs present in `current` but absent from `previous` — i.e. new since the
|
// GUIDs in `current` but not `previous`, ascending order; empty GUIDs ignored.
|
||||||
// previous poll. Order is the set's ascending order (deterministic; the caller does
|
|
||||||
// not depend on discovery order). Empty GUIDs are ignored (a GUID read failure at the
|
|
||||||
// shell boundary must never be tagged).
|
|
||||||
std::vector<std::string> newGuids(const std::set<std::string>& previous,
|
std::vector<std::string> newGuids(const std::set<std::string>& previous,
|
||||||
const std::set<std::string>& current);
|
const std::set<std::string>& current);
|
||||||
|
|
||||||
// Tracks the live GUID set across polls for ONE project, implementing the
|
// Tracks the live GUID set across polls for one project.
|
||||||
// first-poll-after-open guard: the first observation after a (re)start establishes a
|
|
||||||
// BASELINE and reports NOTHING new, so pre-existing content stays at its default
|
|
||||||
// (Arrange) rather than being mass-tagged. Every subsequent observe() returns only the
|
|
||||||
// GUIDs created since the prior observe().
|
|
||||||
//
|
|
||||||
// Project switches are handled by reset(): the shell detects a project change (the
|
|
||||||
// active ReaProject* / project GUID changed) and calls reset() so the next observe()
|
|
||||||
// re-baselines against the newly-opened project instead of diffing across two
|
|
||||||
// unrelated projects (which would spuriously "detect" the entire new project as new
|
|
||||||
// content, or miss content because a same-GUID collision looked pre-existing).
|
|
||||||
class GuidBaseline {
|
class GuidBaseline {
|
||||||
public:
|
public:
|
||||||
// Observes the current live GUID set. On the FIRST call after construction or
|
// First call after construction/reset() establishes the baseline and
|
||||||
// reset() this records the baseline and returns {} (nothing is "new" at open).
|
// returns {}; later calls return GUIDs added since the prior call.
|
||||||
// On every later call it returns the GUIDs added since the previous call and
|
|
||||||
// advances the baseline to `current`. Empty GUIDs are ignored.
|
|
||||||
std::vector<std::string> observe(const std::set<std::string>& current);
|
std::vector<std::string> observe(const std::set<std::string>& current);
|
||||||
|
|
||||||
// Re-arms the first-poll guard: the next observe() re-baselines and reports
|
// Re-arms the first-poll guard on a detected project switch.
|
||||||
// nothing new. Called on a project switch so detection never diffs across
|
|
||||||
// projects.
|
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
// True until the first observe() after construction/reset — exposed for the shell
|
|
||||||
// to reason about (and for tests) about whether a baseline is established yet.
|
|
||||||
bool primed() const { return primed_; }
|
bool primed() const { return primed_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::set<std::string> baseline_;
|
std::set<std::string> baseline_;
|
||||||
bool primed_ = false; // false ⇒ next observe() sets the baseline
|
bool primed_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace reasampler::view
|
} // namespace reasampler::view
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// lane_keys implementation — pure string convention, no REAPER. See lane_keys.h.
|
// See lane_keys.h.
|
||||||
|
|
||||||
#include "core/view/lane_keys.h"
|
#include "core/view/lane_keys.h"
|
||||||
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
namespace reasampler::view {
|
namespace reasampler::view {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Does `s` start with the managed-lane prefix?
|
|
||||||
bool hasManagedPrefix(const std::string& s) {
|
bool hasManagedPrefix(const std::string& s) {
|
||||||
const std::size_t n = std::strlen(kManagedLanePrefix);
|
const std::size_t n = std::strlen(kManagedLanePrefix);
|
||||||
return s.size() >= n && s.compare(0, n, kManagedLanePrefix) == 0;
|
return s.size() >= n && s.compare(0, n, kManagedLanePrefix) == 0;
|
||||||
@@ -19,11 +18,10 @@ bool isManagedLaneName(const std::string& laneName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> managedLaneKey(const std::string& laneName) {
|
std::optional<std::string> managedLaneKey(const std::string& laneName) {
|
||||||
if (!hasManagedPrefix(laneName)) return std::nullopt; // manual/unnamed ⇒ no key
|
if (!hasManagedPrefix(laneName)) return std::nullopt;
|
||||||
// The durable name IS the key (stable across ordinal renumber). Keeping the full
|
// Keep the full prefixed name as the key (not just the mode id) so it stays
|
||||||
// prefixed name — rather than stripping to the mode id — means the key is globally
|
// globally unambiguous; the ownership index's mode field is the sole
|
||||||
// unambiguous and the ownership index's mode field remains the single source of
|
// source of truth for which mode owns the lane.
|
||||||
// truth for which mode owns the lane.
|
|
||||||
return laneName;
|
return laneName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,19 +30,14 @@ std::string laneNameForMode(const std::string& modeId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> modeIdFromLaneName(const std::string& laneName) {
|
std::optional<std::string> modeIdFromLaneName(const std::string& laneName) {
|
||||||
if (!hasManagedPrefix(laneName)) return std::nullopt; // manual/unnamed ⇒ no mode
|
if (!hasManagedPrefix(laneName)) return std::nullopt;
|
||||||
const std::size_t n = std::strlen(kManagedLanePrefix);
|
const std::size_t n = std::strlen(kManagedLanePrefix);
|
||||||
if (laneName.size() == n) return std::nullopt; // prefix only, no mode suffix (illegal)
|
if (laneName.size() == n) return std::nullopt; // prefix only, no mode suffix
|
||||||
return laneName.substr(n);
|
return laneName.substr(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName) {
|
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.
|
|
||||||
if (!isFixedLaneTrack) return false;
|
if (!isFixedLaneTrack) return false;
|
||||||
// On a fixed-lane track: a managed lane (prefixed) is NOT manual; everything else
|
|
||||||
// — including the empty/unnamed lane that REAPER creates by default — IS manual
|
|
||||||
// (user-minted, off-limits to auto-tag and to the lane-drive path).
|
|
||||||
return !hasManagedPrefix(laneName);
|
return !hasManagedPrefix(laneName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-66
@@ -1,85 +1,32 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// lane_keys — the pure, REAPER-free convention that maps a REAPER fixed lane's
|
// Managed/manual fixed-lane convention: maps a lane's durable P_LANENAME to the
|
||||||
// durable NAME (P_LANENAME:n) to the opaque lane-key the pure view_mode_model uses,
|
// opaque lane-key view_mode_model keys by. See src/core/view/CLAUDE.md (Gotchas):
|
||||||
// and the managed/manual heuristic that rides on it.
|
// lane identity must ride the durable name, never the raw I_FIXEDLANE ordinal,
|
||||||
//
|
// or a reorder silently corrupts managed/manual ownership.
|
||||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL. std only.
|
|
||||||
// Unit-tested outside the DAW. The shell (view.cpp) reads each lane's P_LANENAME:n
|
|
||||||
// string from REAPER and asks this module whether the lane is tool-managed and what
|
|
||||||
// its stable lane-key is; the shell never re-derives the prefix rule itself.
|
|
||||||
//
|
|
||||||
// -- Design point #2 (lane-identity robustness) resolution --------------------
|
|
||||||
//
|
|
||||||
// REAPER exposes no durable per-lane GUID. The only lane identity is the ordinal
|
|
||||||
// I_FIXEDLANE, which REAPER RENUMBERS when lanes are reordered or deleted — so keying
|
|
||||||
// the ownership index by raw ordinal would silently corrupt managed/manual ownership
|
|
||||||
// on any reorder. REAPER DOES expose a writable, durable lane NAME (P_LANENAME:n) that
|
|
||||||
// travels with the lane across renumber. So the tool names each lane it mints with a
|
|
||||||
// stable, prefixed identity ("reasampler:<mode>") and keys the ownership index by that
|
|
||||||
// NAME, not the ordinal. On each apply the shell walks the track's lanes by current
|
|
||||||
// ordinal, reads each name, and reconciles ordinal<->laneKey — so a C_LANEPLAYS:N
|
|
||||||
// write always targets the lane's CURRENT ordinal for a given durable key even after a
|
|
||||||
// reorder. A lane WITHOUT the prefix was not minted by the tool: it is manual and
|
|
||||||
// off-limits (the fixed-lane analog of "never touch mute/solo").
|
|
||||||
//
|
|
||||||
// -- Design point #1 (manual-lane exemption) resolution -----------------------
|
|
||||||
//
|
|
||||||
// The SAME prefix rule is the manual/managed heuristic for auto-tag: an item on a lane
|
|
||||||
// whose name lacks the "reasampler:" prefix is on a manual lane and is EXEMPT from
|
|
||||||
// auto-tag. isManagedLaneName is the single predicate both the toggle-apply path and
|
|
||||||
// the new-content detection path consult, so the boundary is defined in one place and
|
|
||||||
// unit-tested.
|
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace reasampler::view {
|
namespace reasampler::view {
|
||||||
|
|
||||||
// The prefix the tool stamps on every lane NAME it mints. A lane name carrying this
|
// Prefix stamped on every lane name the tool mints. Stable-forever like an
|
||||||
// prefix is a managed lane the tool created; any other name (or an empty/unnamed lane)
|
// action-id string — changing it strands ownership of every already-minted lane.
|
||||||
// is a user-minted manual lane. Stable-forever: changing it would strand the ownership
|
|
||||||
// of every lane in every already-saved project, so treat it like an action id string.
|
|
||||||
inline constexpr const char* kManagedLanePrefix = "reasampler:";
|
inline constexpr const char* kManagedLanePrefix = "reasampler:";
|
||||||
|
|
||||||
// True iff `laneName` is a tool-minted managed-lane name (carries kManagedLanePrefix).
|
|
||||||
// This is the load-bearing managed/manual predicate for BOTH design points #1 and #2.
|
|
||||||
bool isManagedLaneName(const std::string& laneName);
|
bool isManagedLaneName(const std::string& laneName);
|
||||||
|
|
||||||
// The opaque lane-key the pure model keys by, for a lane with REAPER name `laneName`.
|
// A managed lane's key is its full durable name; nullopt for manual/unnamed.
|
||||||
// For a managed lane the key IS the durable name (stable across ordinal renumber). For
|
|
||||||
// a manual/unnamed lane there is no managed key: returns std::nullopt so the caller
|
|
||||||
// treats the lane as manual (never driven, items on it exempt from auto-tag).
|
|
||||||
std::optional<std::string> managedLaneKey(const std::string& laneName);
|
std::optional<std::string> managedLaneKey(const std::string& laneName);
|
||||||
|
|
||||||
// The lane NAME the tool mints for the lane owned by `modeId` (kManagedLanePrefix +
|
// Inverse pair: managedLaneKey(laneNameForMode(m)) == kManagedLanePrefix + m;
|
||||||
// modeId). The inverse of managedLaneKey for a managed lane: managedLaneKey(
|
// modeIdFromLaneName(laneNameForMode(m)) == m.
|
||||||
// laneNameForMode(m)) == kManagedLanePrefix + m. Exposed for the Wave-3 lane-minting
|
|
||||||
// path and for tests; the apply path in this wave only READS names, but the round-trip
|
|
||||||
// contract is asserted here so minting and reading cannot drift.
|
|
||||||
std::string laneNameForMode(const std::string& modeId);
|
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);
|
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
|
// Single predicate governing both which lanes the apply path may drive and
|
||||||
// (i.e. exempt from auto-tag). The two inputs are:
|
// which items are exempt from auto-tag. No manual-lane concept on a non-fixed-
|
||||||
// isFixedLaneTrack — whether the item's track has I_FREEMODE==2. On a normal
|
// lane track (returns false); on a fixed-lane track, any unprefixed name —
|
||||||
// (non-fixed-lane) track the concept of a "manual lane" does not
|
// including REAPER's default empty lane — is manual.
|
||||||
// apply; the item follows the normal auto-tag rule (return false).
|
|
||||||
// laneName — the durable P_LANENAME of the lane the item sits on. A lane
|
|
||||||
// that carries kManagedLanePrefix is a tool-minted managed lane
|
|
||||||
// (not manual); any other name — including empty (unnamed) — is
|
|
||||||
// a user-minted manual lane (exempt from auto-tag).
|
|
||||||
//
|
|
||||||
// This is the SINGLE predicate that governs BOTH the apply path (which lanes may be
|
|
||||||
// driven) and the auto-tag exemption path (which items are exempt). It is unit-tested
|
|
||||||
// here so both paths share exactly one definition; the shell supplies the two REAPER
|
|
||||||
// inputs (I_FREEMODE result, P_LANENAME string) and never re-derives this logic.
|
|
||||||
bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName);
|
bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName);
|
||||||
|
|
||||||
} // namespace reasampler::view
|
} // namespace reasampler::view
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// mode_switch — pure implementation. See mode_switch.h. NO REAPER / SWELL / vendor.
|
// See mode_switch.h.
|
||||||
|
|
||||||
#include "core/view/mode_switch.h"
|
#include "core/view/mode_switch.h"
|
||||||
|
|
||||||
@@ -8,11 +8,8 @@ namespace reasampler::view {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The left edge of segment i in a header of the given x-origin and width divided
|
// Left edge of segment i; segment i spans [edge(i), edge(i+1)). count assumed
|
||||||
// into `count` segments. Boundary i is x + (i * width) / count, so segment i spans
|
// >= 1 by callers.
|
||||||
// [edge(i), edge(i+1)). Because every boundary is derived from the same formula,
|
|
||||||
// consecutive segments share an exact edge (no gap, no overlap) and edge(count)
|
|
||||||
// == x + width precisely. count assumed >= 1 by callers.
|
|
||||||
int segmentEdge(int x, int width, int i, int count) {
|
int segmentEdge(int x, int width, int i, int count) {
|
||||||
return x + (i * width) / count;
|
return x + (i * width) / count;
|
||||||
}
|
}
|
||||||
@@ -31,7 +28,7 @@ std::vector<SegmentRect> computeSegmentRects(const HeaderRect& header,
|
|||||||
SegmentRect r;
|
SegmentRect r;
|
||||||
r.x = left;
|
r.x = left;
|
||||||
r.y = header.y;
|
r.y = header.y;
|
||||||
r.width = right - left; // absorbs rounding; adjacent segments abut exactly
|
r.width = right - left;
|
||||||
r.height = header.height;
|
r.height = header.height;
|
||||||
rects.push_back(r);
|
rects.push_back(r);
|
||||||
}
|
}
|
||||||
@@ -41,23 +38,15 @@ std::vector<SegmentRect> computeSegmentRects(const HeaderRect& header,
|
|||||||
int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount) {
|
int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount) {
|
||||||
if (segmentCount <= 0 || header.width <= 0 || header.height <= 0) return -1;
|
if (segmentCount <= 0 || header.width <= 0 || header.height <= 0) return -1;
|
||||||
|
|
||||||
// Reject anything outside the header band first (half-open bounds match the
|
|
||||||
// segment rects). Below the header is where the grid lives — the panel falls
|
|
||||||
// through to grid handling on a -1.
|
|
||||||
if (px < header.x || px >= header.x + header.width ||
|
if (px < header.x || px >= header.x + header.width ||
|
||||||
py < header.y || py >= header.y + header.height)
|
py < header.y || py >= header.y + header.height)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
// Inside the band: find the segment whose [edge(i), edge(i+1)) contains px.
|
|
||||||
// Linear over N (N is tiny — one per mode); mirrors the boundary formula so the
|
|
||||||
// hit matches the drawn segment exactly.
|
|
||||||
for (int i = 0; i < segmentCount; ++i) {
|
for (int i = 0; i < segmentCount; ++i) {
|
||||||
const int left = segmentEdge(header.x, header.width, i, segmentCount);
|
const int left = segmentEdge(header.x, header.width, i, segmentCount);
|
||||||
const int right = segmentEdge(header.x, header.width, i + 1, segmentCount);
|
const int right = segmentEdge(header.x, header.width, i + 1, segmentCount);
|
||||||
if (px >= left && px < right) return i;
|
if (px >= left && px < right) return i;
|
||||||
}
|
}
|
||||||
// Guard: px == header.x + header.width would fail the < above but was already
|
|
||||||
// excluded by the band check. Any residual falls to -1 (defensive, unreachable).
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,23 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "core/ui/rect.h"
|
#include "core/ui/rect.h"
|
||||||
// mode_switch — the REAPER-free layout math behind the bank_panel's Design-View
|
// Pure segment layout + hit-test for the bank_panel's Design-View mode switch.
|
||||||
// mode switch (Phase D, Wave 4 — D5). A segmented control `[ Arrange | Design ]`
|
// Mirror of bank_grid. See src/core/view/CLAUDE.md.
|
||||||
// (N-mode general, one segment per registered mode) drawn in a fixed-height header
|
|
||||||
// strip at the top of the docked panel. The panel shell (shell/panel/) owns the
|
|
||||||
// SWELL window, LICE drawing, and the live ViewModeModel read + mode activation —
|
|
||||||
// all REAPER-bound, DAW-verified. What is NOT DAW-bound — how N segments tile a
|
|
||||||
// header rectangle, and which segment a click lands in — lives here so it is
|
|
||||||
// unit-tested outside the DAW (CLAUDE.md §load-bearing split). Mirror of bank_grid.
|
|
||||||
//
|
|
||||||
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library
|
|
||||||
// only. Builds and unit-tests without REAPER.
|
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace reasampler::view {
|
namespace reasampler::view {
|
||||||
|
|
||||||
// The header strip the switch is drawn into, top-left origin (SWELL/LICE
|
using HeaderRect = ui::Rect;
|
||||||
// convention). (x, y) is the top-left corner; width/height are the strip extents.
|
using SegmentRect = ui::Rect;
|
||||||
// The panel reserves this at the top of its client area and offsets the grid below.
|
|
||||||
using HeaderRect = ui::Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
|
|
||||||
|
|
||||||
// One segment's pixel rectangle within the header, top-left origin. These are the
|
// Divides `header` into `segmentCount` equal segments left-to-right. Boundaries
|
||||||
// draw bounds for one mode's button; the panel draws the mode's display name inside
|
// use header.x + (i * width) / segmentCount so segments abut exactly despite
|
||||||
// it and lights it when it is the active mode.
|
// integer rounding. segmentCount <= 0 or non-positive width returns empty.
|
||||||
using SegmentRect = ui::Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
|
|
||||||
|
|
||||||
// Divides `header` into `segmentCount` equal segments left-to-right, in the caller's
|
|
||||||
// order (the panel passes modes in ordinal order). Returns exactly segmentCount
|
|
||||||
// rects. The division tiles the header EXACTLY: each segment's left edge is
|
|
||||||
// header.x + (i * width) / segmentCount, so integer rounding is absorbed at the
|
|
||||||
// boundaries — segments abut with no gap and no overlap, and the last segment
|
|
||||||
// reaches header.x + header.width precisely (individual widths may differ by one
|
|
||||||
// pixel when width does not divide evenly). Each segment inherits the header's full
|
|
||||||
// y/height. segmentCount <= 0 or a non-positive header width returns empty.
|
|
||||||
std::vector<SegmentRect> computeSegmentRects(const HeaderRect& header,
|
std::vector<SegmentRect> computeSegmentRects(const HeaderRect& header,
|
||||||
int segmentCount);
|
int segmentCount);
|
||||||
|
|
||||||
// Hit-tests a point (SWELL/LICE top-left client coords) against the segmented
|
// Segment index containing (px, py), or -1 for a miss (outside header bounds,
|
||||||
// control laid out in `header` with `segmentCount` segments. Returns the index of
|
// or segmentCount <= 0). Half-open bounds match computeSegmentRects.
|
||||||
// the segment containing the point, or -1 for a miss: a point outside the header
|
|
||||||
// bounds entirely (including below it, where the grid lives), or when segmentCount
|
|
||||||
// <= 0. Half-open bounds [x, x+width) x [y, y+height) match computeSegmentRects, so
|
|
||||||
// adjacent segments never both claim a pixel and the point maps to the same segment
|
|
||||||
// the panel drew there.
|
|
||||||
int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount);
|
int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount);
|
||||||
|
|
||||||
} // namespace reasampler::view
|
} // namespace reasampler::view
|
||||||
|
|||||||
@@ -6,35 +6,16 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/json/json.h"
|
#include "core/json/json.h"
|
||||||
#include "core/view/lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention
|
#include "core/view/lane_keys.h" // laneNameForMode
|
||||||
|
|
||||||
// view_mode_model implementation.
|
|
||||||
//
|
|
||||||
// JSON rides on the shared core/json lexical layer (Q-W1), mirroring bank_model.
|
|
||||||
// A compact writer
|
|
||||||
// plus a recursive-descent parser covers the field set: the mode registry, the
|
|
||||||
// GUID-keyed membership map, per-track snapshots (with a variable-length per-FX
|
|
||||||
// offline vector), and the active mode. Ints are emitted plainly; strings are
|
|
||||||
// escaped identically to bank_model so control chars and unicode survive.
|
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// Q-W1 interim: laneNameForMode lives in reasampler::view now; this god module
|
|
||||||
// re-namespaces in its own split wave.
|
|
||||||
using view::laneNameForMode;
|
using view::laneNameForMode;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// equality
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
bool Mode::operator==(const Mode& o) const {
|
bool Mode::operator==(const Mode& o) const {
|
||||||
return id == o.id && displayName == o.displayName && ordinal == o.ordinal;
|
return id == o.id && displayName == o.displayName && ordinal == o.ordinal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ModeRegistry
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
ModeRegistry::ModeRegistry() {
|
ModeRegistry::ModeRegistry() {
|
||||||
modes_.push_back(Mode{kArrangeModeId, "Arrange", 0});
|
modes_.push_back(Mode{kArrangeModeId, "Arrange", 0});
|
||||||
modes_.push_back(Mode{kDesignModeId, "Design", 1});
|
modes_.push_back(Mode{kDesignModeId, "Design", 1});
|
||||||
@@ -44,8 +25,6 @@ bool ModeRegistry::add(const Mode& mode) {
|
|||||||
if (mode.id.empty()) return false;
|
if (mode.id.empty()) return false;
|
||||||
if (query(mode.id) != nullptr) return false; // ids are unique
|
if (query(mode.id) != nullptr) return false; // ids are unique
|
||||||
modes_.push_back(mode);
|
modes_.push_back(mode);
|
||||||
// Keep ordinal order stable; std::stable_sort so equal ordinals keep insertion
|
|
||||||
// order (the tie-break documented in the header).
|
|
||||||
std::stable_sort(modes_.begin(), modes_.end(),
|
std::stable_sort(modes_.begin(), modes_.end(),
|
||||||
[](const Mode& a, const Mode& b) { return a.ordinal < b.ordinal; });
|
[](const Mode& a, const Mode& b) { return a.ordinal < b.ordinal; });
|
||||||
return true;
|
return true;
|
||||||
@@ -57,10 +36,6 @@ const Mode* ModeRegistry::query(const std::string& id) const {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// MembershipIndex
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
bool MembershipIndex::tag(const std::string& guid, const std::string& modeId) {
|
bool MembershipIndex::tag(const std::string& guid, const std::string& modeId) {
|
||||||
if (guid.empty() || modeId.empty()) return false;
|
if (guid.empty() || modeId.empty()) return false;
|
||||||
Membership& m = entries_[guid];
|
Membership& m = entries_[guid];
|
||||||
@@ -95,10 +70,6 @@ std::set<std::string> MembershipIndex::modesOf(const std::string& guid) const {
|
|||||||
return m ? m->modeIds : std::set<std::string>{};
|
return m ? m->modeIds : std::set<std::string>{};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// LaneOwnershipIndex
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
bool LaneOwnershipIndex::setManaged(const std::string& trackGuid, const std::string& laneKey,
|
bool LaneOwnershipIndex::setManaged(const std::string& trackGuid, const std::string& laneKey,
|
||||||
const std::string& modeId) {
|
const std::string& modeId) {
|
||||||
if (trackGuid.empty() || laneKey.empty() || modeId.empty()) return false;
|
if (trackGuid.empty() || laneKey.empty() || modeId.empty()) return false;
|
||||||
@@ -123,24 +94,12 @@ const LaneOwnership* LaneOwnershipIndex::query(const std::string& trackGuid,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int laneModeState(const std::string& managedMode, const std::string& activeMode) {
|
int laneModeState(const std::string& managedMode, const std::string& activeMode) {
|
||||||
// The active mode's lane plays exclusively; every other managed lane is silenced
|
// Assumes at most one managed lane per (track, mode) — planToggle asserts
|
||||||
// and hidden (C_LANEPLAYS = 0). Exclusive membership: only one stance's lane at a
|
// this in debug builds; two lanes claiming the same mode would both be
|
||||||
// time. Show-both, which keeps a lane audible across modes, is a per-lane opt-out
|
// told to play exclusively, which REAPER can't honor coherently.
|
||||||
// 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;
|
return managedMode == activeMode ? kLanePlaysExclusive : kLaneSilent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// auto-tag decision
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
||||||
const std::vector<NewItem>& newItems,
|
const std::vector<NewItem>& newItems,
|
||||||
const std::string& activeMode) {
|
const std::string& activeMode) {
|
||||||
@@ -153,14 +112,9 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
|||||||
}
|
}
|
||||||
for (const auto& item : newItems) {
|
for (const auto& item : newItems) {
|
||||||
if (item.guid.empty()) continue;
|
if (item.guid.empty()) continue;
|
||||||
if (item.onManualLane) continue; // manual-lane content is off-limits to auto-tag
|
if (item.onManualLane) continue;
|
||||||
|
|
||||||
// ADOPTION (strand guard): a new item on a track whose PRE-EXISTING content
|
// Adopt the track's single pre-existing mode (strand guard — see header).
|
||||||
// resolves to exactly one mode adopts THAT mode, so a drop onto a track already
|
|
||||||
// showing content never pushes it multi-mode and never triggers a lane split that
|
|
||||||
// would silence the pre-existing, previously-visible items. A track with no prior
|
|
||||||
// content (empty trackModes) or one already carrying a deliberate multi-mode split
|
|
||||||
// (>1) falls back to the active-mode rule.
|
|
||||||
const std::string& target =
|
const std::string& target =
|
||||||
item.trackModes.size() == 1 ? *item.trackModes.begin() : activeMode;
|
item.trackModes.size() == 1 ? *item.trackModes.begin() : activeMode;
|
||||||
tags.push_back(AutoTag{item.guid, target});
|
tags.push_back(AutoTag{item.guid, target});
|
||||||
@@ -173,27 +127,19 @@ std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
|||||||
std::vector<ItemRetagOp> ops;
|
std::vector<ItemRetagOp> ops;
|
||||||
const bool untag = targetMode.empty(); // empty target ⇒ untag (→ Arrange default)
|
const bool untag = targetMode.empty(); // empty target ⇒ untag (→ Arrange default)
|
||||||
for (const RetagItem& item : selected) {
|
for (const RetagItem& item : selected) {
|
||||||
if (item.guid.empty()) continue; // defensive; a real item always has a GUID
|
if (item.guid.empty()) continue;
|
||||||
if (item.onManualLane) continue; // manual-lane item is EXEMPT — never retagged
|
if (item.onManualLane) continue;
|
||||||
ops.push_back(ItemRetagOp{item.guid, untag, untag ? std::string{} : targetMode});
|
ops.push_back(ItemRetagOp{item.guid, untag, untag ? std::string{} : targetMode});
|
||||||
}
|
}
|
||||||
return ops;
|
return ops;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// lane minting decision
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||||
const std::vector<LaneTrack>& tracks) {
|
const std::vector<LaneTrack>& tracks) {
|
||||||
LaneMintPlan plan;
|
LaneMintPlan plan;
|
||||||
|
|
||||||
// Precompute, per track GUID, the count of modes it is VISIBLE in and the set of
|
// Per track GUID, the modes it's visible in (tree-aware) — captures the
|
||||||
// those mode ids — tree-aware, so a content-bearing folder's DERIVED visibility
|
// folder-derived-visibility split trigger, not just own-item mode span.
|
||||||
// (visibleTracks marks a parent visible in every mode a descendant is visible in)
|
|
||||||
// is captured, not only the track's own item mode-span. This is the visibility
|
|
||||||
// trigger source (b): a folder derived-visible in >= 2 modes must lane-separate its
|
|
||||||
// own media even when that media is single-mode. Computed once for all tracks.
|
|
||||||
std::map<std::string, std::set<std::string>> visibleModesOf;
|
std::map<std::string, std::set<std::string>> visibleModesOf;
|
||||||
for (const Mode& mode : model.modes().all()) {
|
for (const Mode& mode : model.modes().all()) {
|
||||||
const std::set<std::string> vis = model.visibleTracks(tree, mode.id);
|
const std::set<std::string> vis = model.visibleTracks(tree, mode.id);
|
||||||
@@ -204,58 +150,26 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
|||||||
for (const LaneTrack& track : tracks) {
|
for (const LaneTrack& track : tracks) {
|
||||||
if (track.trackGuid.empty()) continue;
|
if (track.trackGuid.empty()) continue;
|
||||||
|
|
||||||
// SHOW-BOTH escape hatch: never force-split. A show-both track is visible in
|
if (model.membership().isShowBoth(track.trackGuid)) continue; // never force-split
|
||||||
// every mode ON PURPOSE and its content is meant to play across all of them, so
|
|
||||||
// neither the visibility trigger nor the own-item-span trigger confines it. Skip
|
|
||||||
// it entirely (no split/mint/assign) so its items stay cross-mode-visible.
|
|
||||||
if (model.membership().isShowBoth(track.trackGuid)) continue;
|
|
||||||
|
|
||||||
// Collect the DISTINCT modes the track's managed-eligible OWN 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> ownItemModes;
|
std::set<std::string> ownItemModes;
|
||||||
for (const LaneItem& item : track.items) {
|
for (const LaneItem& item : track.items) {
|
||||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||||
if (item.onManualLane) continue; // exempt — user's hand-managed lane
|
if (item.onManualLane) continue; // exempt
|
||||||
ownItemModes.insert(item.modeId);
|
ownItemModes.insert(item.modeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A track with NO managed-eligible own media never splits: there is nothing to
|
if (ownItemModes.empty()) continue; // no own media, nothing to confine
|
||||||
// confine (lane separation projects OWN items across modes). A folder derived-
|
|
||||||
// visible in many modes but carrying no own content stays whole-track visibility-
|
|
||||||
// only (D1 parent handling) — this guards the "carries its own media" clause.
|
|
||||||
if (ownItemModes.empty()) continue;
|
|
||||||
|
|
||||||
// The two visibility sources, OR'd:
|
|
||||||
// (a) own items span >= 2 modes (W3-A trigger), and
|
|
||||||
// (b) the track is derived-visible in >= 2 modes (the folder-media case).
|
|
||||||
// A track qualifies for a split if EITHER makes it multi-mode.
|
|
||||||
const auto visIt = visibleModesOf.find(track.trackGuid);
|
const auto visIt = visibleModesOf.find(track.trackGuid);
|
||||||
const std::size_t visibleModeCount =
|
const std::size_t visibleModeCount =
|
||||||
visIt == visibleModesOf.end() ? 0 : visIt->second.size();
|
visIt == visibleModesOf.end() ? 0 : visIt->second.size();
|
||||||
const bool multiMode = ownItemModes.size() >= 2 || visibleModeCount >= 2;
|
const bool multiMode = ownItemModes.size() >= 2 || visibleModeCount >= 2;
|
||||||
|
|
||||||
// Single-mode (visible in exactly one mode, own items single-mode): whole-track
|
if (!multiMode) continue; // single-mode: D1 whole-track parking still separates
|
||||||
// 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 (!multiMode) continue;
|
|
||||||
|
|
||||||
// Lazy-mint: lanes to mint = ONLY the modes the track's OWN items actually occupy —
|
const std::set<std::string>& laneModes = ownItemModes; // lazy-mint: own modes only
|
||||||
// never an empty reserved lane for a mode the track is merely derived-visible in.
|
|
||||||
// A folder whose own item is Design-only but which is derived-visible in Arrange too
|
|
||||||
// mints a Design lane ONLY (holding the item); it mints NO Arrange lane. Confinement
|
|
||||||
// still holds: with only a Design lane present, toggling to Arrange drives that lane's
|
|
||||||
// C_LANEPLAYS to 0 (it hides+silences) and no lane plays, so the track reads as an
|
|
||||||
// empty normal track — the Design item does not leak. The Arrange lane is minted on
|
|
||||||
// demand the moment an Arrange item first lands (a later mint tick sees ownItemModes
|
|
||||||
// gain Arrange). The visibility trigger above still decides WHETHER to split; it no
|
|
||||||
// longer inflates WHICH lanes are minted.
|
|
||||||
const std::set<std::string>& laneModes = ownItemModes;
|
|
||||||
|
|
||||||
// Transition to lane-split: one managed lane per own-content mode (durable key =
|
|
||||||
// laneNameForMode(mode)), owned by that mode.
|
|
||||||
plan.splits.push_back(LaneMintPlan::TrackSplit{
|
plan.splits.push_back(LaneMintPlan::TrackSplit{
|
||||||
track.trackGuid, static_cast<int>(laneModes.size())});
|
track.trackGuid, static_cast<int>(laneModes.size())});
|
||||||
for (const std::string& mode : laneModes) {
|
for (const std::string& mode : laneModes) {
|
||||||
@@ -263,13 +177,9 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
|||||||
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign EVERY managed-eligible OWN item onto its tagged mode's lane — including
|
|
||||||
// the pre-existing single-mode items, so a folder carrying one own Design item
|
|
||||||
// while derived-visible in Arrange still lanes that item to the Design lane (it
|
|
||||||
// then hides+silences whenever Arrange is active — the exact failing-case fix).
|
|
||||||
for (const LaneItem& item : track.items) {
|
for (const LaneItem& item : track.items) {
|
||||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||||
if (item.onManualLane) continue; // exempt — never reassigned
|
if (item.onManualLane) continue;
|
||||||
plan.assigns.push_back(LaneAssign{
|
plan.assigns.push_back(LaneAssign{
|
||||||
item.guid, track.trackGuid, laneNameForMode(item.modeId)});
|
item.guid, track.trackGuid, laneNameForMode(item.modeId)});
|
||||||
}
|
}
|
||||||
@@ -278,13 +188,7 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
|||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// planner helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
||||||
// Parking contract: hide both panels, out of the mix, FX bypassed, every FX
|
|
||||||
// offline. All fixed zeros — park never consults a snapshot.
|
|
||||||
TrackPlan p;
|
TrackPlan p;
|
||||||
p.flags = {
|
p.flags = {
|
||||||
{guid, Flag::ShowInTcp, 0},
|
{guid, Flag::ShowInTcp, 0},
|
||||||
@@ -298,8 +202,6 @@ TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
|
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
|
||||||
// Restore contract: every driven flag returns to its SNAPSHOTTED value — never
|
|
||||||
// a hardcoded "on"/default. A flag captured at 0 restores to 0.
|
|
||||||
TrackPlan p;
|
TrackPlan p;
|
||||||
p.flags = {
|
p.flags = {
|
||||||
{guid, Flag::ShowInTcp, snap.showInTcp},
|
{guid, Flag::ShowInTcp, snap.showInTcp},
|
||||||
@@ -319,15 +221,9 @@ std::string nextModeId(const ModeRegistry& modes, const std::string& currentMode
|
|||||||
if (all[i].id == currentModeId)
|
if (all[i].id == currentModeId)
|
||||||
return all[(i + 1) % all.size()].id; // wrap past the last
|
return all[(i + 1) % all.size()].id; // wrap past the last
|
||||||
}
|
}
|
||||||
// Active mode not in the registry (stale/unknown) — jump to the first mode as a
|
return all.front().id; // stale/unknown current id -> jump to the first mode
|
||||||
// sane home rather than returning "".
|
|
||||||
return all.front().id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ViewModeModel
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {}
|
ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {}
|
||||||
|
|
||||||
bool ViewModeModel::setActiveMode(const std::string& modeId) {
|
bool ViewModeModel::setActiveMode(const std::string& modeId) {
|
||||||
@@ -350,8 +246,7 @@ const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::size_t ViewModeModel::reconcile(const std::set<std::string>& liveGuids) {
|
std::size_t ViewModeModel::reconcile(const std::set<std::string>& liveGuids) {
|
||||||
// Prune snapshots for GUIDs the project no longer contains (see header for the
|
// See header: snapshots are pruned, membership is not (undo-delete rationale).
|
||||||
// deliberate snapshot-yes / membership-no asymmetry and the undo-delete rationale).
|
|
||||||
std::size_t removed = 0;
|
std::size_t removed = 0;
|
||||||
for (auto it = snapshots_.begin(); it != snapshots_.end();) {
|
for (auto it = snapshots_.begin(); it != snapshots_.end();) {
|
||||||
if (liveGuids.count(it->first) == 0) {
|
if (liveGuids.count(it->first) == 0) {
|
||||||
@@ -368,7 +263,7 @@ bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string
|
|||||||
const Membership* m = membership_.query(guid);
|
const Membership* m = membership_.query(guid);
|
||||||
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
|
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
|
||||||
if (m->showBoth) return true; // show-both ⇒ every mode
|
if (m->showBoth) return true; // show-both ⇒ every mode
|
||||||
if (m->modeIds.empty()) return modeId == kArrangeModeId; // show-both-cleared, no mode
|
if (m->modeIds.empty()) return modeId == kArrangeModeId;
|
||||||
return m->modeIds.count(modeId) > 0;
|
return m->modeIds.count(modeId) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,28 +271,18 @@ std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
|
|||||||
const std::string& modeId) const {
|
const std::string& modeId) const {
|
||||||
std::set<std::string> visible;
|
std::set<std::string> visible;
|
||||||
|
|
||||||
// Pass 1: every node — leaf OR parent — that belongs to the mode by its OWN
|
// Pass 1: nodes visible by their own membership (leaf rule, or an
|
||||||
// membership is visible. For a leaf this is the tagged/show-both/untagged-Arrange
|
// untagged/Arrange-default folder).
|
||||||
// rule; for a parent it means an untagged folder (which carries its own FX/media
|
|
||||||
// and defaults to Arrange) shows in Arrange even when none of its children do.
|
|
||||||
// Parents ALSO become visible in pass 2 by derivation from a visible descendant;
|
|
||||||
// the two rules are OR'd, so an untagged folder of all-Design leaves shows in both
|
|
||||||
// Arrange (own default) and Design (derived).
|
|
||||||
for (const auto& node : tree.nodes) {
|
for (const auto& node : tree.nodes) {
|
||||||
if (leafBelongsToMode(node.guid, modeId))
|
if (leafBelongsToMode(node.guid, modeId))
|
||||||
visible.insert(node.guid);
|
visible.insert(node.guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: a parent is also visible if any descendant is visible. Walk each
|
// Pass 2: propagate up parent chains so a parent with any visible
|
||||||
// currently-visible node up its parent chain and mark ancestors. Seeding from the
|
// descendant is visible too (OR'd with pass 1). Cycle-guarded.
|
||||||
// full pass-1 set means a parent made visible by its own membership propagates its
|
|
||||||
// visibility up the remaining ancestors too. Parent chains are read from the
|
|
||||||
// supplied tree only (no REAPER access). A cycle-guard bounds the walk in case a
|
|
||||||
// malformed tree links a node to itself.
|
|
||||||
std::map<std::string, std::string> parentOf;
|
std::map<std::string, std::string> parentOf;
|
||||||
for (const auto& node : tree.nodes) parentOf[node.guid] = node.parentGuid;
|
for (const auto& node : tree.nodes) parentOf[node.guid] = node.parentGuid;
|
||||||
|
|
||||||
// Snapshot the pass-1 visible set so we don't re-walk parents we add mid-loop.
|
|
||||||
const std::vector<std::string> seeds(visible.begin(), visible.end());
|
const std::vector<std::string> seeds(visible.begin(), visible.end());
|
||||||
for (const auto& node : seeds) {
|
for (const auto& node : seeds) {
|
||||||
auto it = parentOf.find(node);
|
auto it = parentOf.find(node);
|
||||||
@@ -415,52 +300,29 @@ std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
|
|||||||
TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
|
TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
|
||||||
TogglePlan plan;
|
TogglePlan plan;
|
||||||
|
|
||||||
// The mode system manages EVERY leaf, not just tagged ones. An untagged leaf is
|
// Enumerate the tree (not membership_.all()) so untagged leaves — absent
|
||||||
// an Arrange member (leafBelongsToMode resolves that), so it must park when the
|
// from the membership index but still Arrange members — park/restore too.
|
||||||
// target mode is not Arrange and restore when it is — the same full park/restore
|
|
||||||
// a tagged leaf gets. Enumerating the FolderTree (not membership_.all()) is what
|
|
||||||
// brings untagged leaves — which are absent from the membership index — under
|
|
||||||
// management. Parents are visibility-only (handled by visibleTracks + the shell's
|
|
||||||
// parent-visibility pass) and show-both leaves are the always-visible escape;
|
|
||||||
// neither is ever parked.
|
|
||||||
for (const auto& node : tree.nodes) {
|
for (const auto& node : tree.nodes) {
|
||||||
if (node.isParent) continue; // parents are derived, never parked
|
if (node.isParent) continue;
|
||||||
const std::string& guid = node.guid;
|
const std::string& guid = node.guid;
|
||||||
if (membership_.isShowBoth(guid)) continue; // show-both leaves are never parked
|
if (membership_.isShowBoth(guid)) continue;
|
||||||
|
|
||||||
const bool active = leafBelongsToMode(guid, targetMode);
|
const bool active = leafBelongsToMode(guid, targetMode);
|
||||||
if (active) {
|
if (active) {
|
||||||
// Returning to visibility: restore from snapshot if we have one. No
|
|
||||||
// snapshot ⇒ the track was never parked, nothing to restore.
|
|
||||||
if (const TrackSnapshot* snap = snapshot(guid))
|
if (const TrackSnapshot* snap = snapshot(guid))
|
||||||
plan.restore.push_back(makeRestorePlan(guid, *snap));
|
plan.restore.push_back(makeRestorePlan(guid, *snap));
|
||||||
} else {
|
} else {
|
||||||
// Inactive leaf (tagged into another mode, or untagged in a non-Arrange
|
// fxOffline is empty here; the D2 shell expands it via TrackFX_GetCount.
|
||||||
// mode) ⇒ park. fxOffline is intentionally empty here: the D2 shell
|
|
||||||
// expands per-FX offline writes using TrackFX_GetCount. The pure model
|
|
||||||
// has no access to REAPER FX counts at plan time; makeParkPlan(guid, 0)
|
|
||||||
// emits only the scalar flags as a result.
|
|
||||||
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
|
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// D2 item-level projection: emit a C_LANEPLAYS op for every MANAGED lane. The
|
// One C_LANEPLAYS op per MANAGED lane; manual lanes are skipped entirely.
|
||||||
// active mode's lane plays exclusively; every other managed lane is silenced+hidden
|
|
||||||
// (laneModeState). MANUAL lanes are skipped entirely — the load-bearing invariant:
|
|
||||||
// a toggle never drives a lane the tool did not mint (the fixed-lane analog of
|
|
||||||
// "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
|
#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)
|
std::set<std::pair<std::string, std::string>> seenTrackMode; // (trackGuid, mode)
|
||||||
#endif
|
#endif
|
||||||
for (const auto& [ref, ownership] : lanes_.all()) {
|
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||||
if (!ownership.isManaged()) continue; // manual lanes are off-limits
|
if (!ownership.isManaged()) continue;
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
assert(seenTrackMode.insert({ref.trackGuid, *ownership.managedMode}).second &&
|
assert(seenTrackMode.insert({ref.trackGuid, *ownership.managedMode}).second &&
|
||||||
"two managed lanes on one track claim the same mode (exclusivity broken)");
|
"two managed lanes on one track claim the same mode (exclusivity broken)");
|
||||||
@@ -473,9 +335,6 @@ TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string&
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::set<LaneRef> ViewModeModel::lanesTouchedByToggle() const {
|
std::set<LaneRef> ViewModeModel::lanesTouchedByToggle() const {
|
||||||
// Managed-only: exactly the lanes a toggle is permitted to drive. A manual lane —
|
|
||||||
// absent OR recorded manual in the ownership index — is never returned, so the shell
|
|
||||||
// can never write C_LANEPLAYS to a lane the user hand-manages.
|
|
||||||
std::set<LaneRef> touched;
|
std::set<LaneRef> touched;
|
||||||
for (const auto& [ref, ownership] : lanes_.all()) {
|
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||||
if (ownership.isManaged()) touched.insert(ref);
|
if (ownership.isManaged()) touched.insert(ref);
|
||||||
@@ -488,14 +347,9 @@ bool ViewModeModel::operator==(const ViewModeModel& o) const {
|
|||||||
activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_;
|
activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// JSON — writer
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Shared core/json emit helpers (Q-W1): same escape set + %d rendering as the
|
// Shared core/json emit helpers (Q-W1) — byte-identical escape/int rendering.
|
||||||
// prior file-local writer, so the emitted blob is byte-identical.
|
|
||||||
using json::writeEscaped;
|
using json::writeEscaped;
|
||||||
using json::writeIntArray;
|
using json::writeIntArray;
|
||||||
std::string intToStr(int v) { return json::numToStr(v); }
|
std::string intToStr(int v) { return json::numToStr(v); }
|
||||||
@@ -510,7 +364,6 @@ std::string ViewModeModel::serialize() const {
|
|||||||
root.keyRaw("version", intToStr(1));
|
root.keyRaw("version", intToStr(1));
|
||||||
root.keyStr("activeMode", activeModeId_);
|
root.keyStr("activeMode", activeModeId_);
|
||||||
|
|
||||||
// modes
|
|
||||||
root.keyBegin("modes");
|
root.keyBegin("modes");
|
||||||
out += '[';
|
out += '[';
|
||||||
{
|
{
|
||||||
@@ -571,10 +424,7 @@ std::string ViewModeModel::serialize() const {
|
|||||||
}
|
}
|
||||||
out += ']';
|
out += ']';
|
||||||
|
|
||||||
// lanes: array of { trackGuid, laneKey, managed(bool), mode(str, managed only) }.
|
// lanes: array of { trackGuid, laneKey, managed(bool), mode(str, managed only) }
|
||||||
// A manual lane omits "mode"; managed carries the owning mode id. Emitting an
|
|
||||||
// explicit "managed" bool keeps a manual lane distinguishable from a managed lane
|
|
||||||
// whose mode string is (illegally) empty — the parser rejects the latter.
|
|
||||||
root.keyBegin("lanes");
|
root.keyBegin("lanes");
|
||||||
out += '[';
|
out += '[';
|
||||||
{
|
{
|
||||||
@@ -590,23 +440,12 @@ std::string ViewModeModel::serialize() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
out += ']';
|
out += ']';
|
||||||
} // root closes here (see bank_model note on NRVO + deferred close)
|
} // root closes here (NRVO + deferred close, mirrors bank_model)
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// JSON — parser (recursive descent; false on any malformed input, never UB)
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The model DOMAIN grammar over the shared core/json lexical layer (Q-W1).
|
|
||||||
|
|
||||||
// The registry starts seeded (Arrange + Design). Deserialization must reproduce the
|
|
||||||
// serialized set exactly, so we replace the seeded contents with the parsed ones —
|
|
||||||
// add() dedups by id, so a serialized Arrange/Design would otherwise be rejected as
|
|
||||||
// duplicates and the ordinals/names would not round-trip. We therefore parse into a
|
|
||||||
// fresh vector and swap. `reg` is passed empty (see parseModel).
|
|
||||||
bool parseModes(json::Reader& r, ModeRegistry& reg) {
|
bool parseModes(json::Reader& r, ModeRegistry& reg) {
|
||||||
if (!r.consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
r.skipWs();
|
r.skipWs();
|
||||||
@@ -659,18 +498,9 @@ bool parseMembership(json::Reader& r, MembershipIndex& idx) {
|
|||||||
} while (r.consume(','));
|
} while (r.consume(','));
|
||||||
if (!r.consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
if (!haveGuid || guid.empty()) return false;
|
if (!haveGuid || guid.empty()) return false;
|
||||||
// Install the entry verbatim (tag() would clear a multi-mode set and drop
|
// Install verbatim (tag() would clobber a multi-mode set / show-both).
|
||||||
// show-both). A serialized entry is trusted to already satisfy the model's
|
// Stale mode ids / stale GUIDs are tolerated by design — only
|
||||||
// invariants.
|
// activeMode is validated (below).
|
||||||
//
|
|
||||||
// Deliberate tolerance: we do NOT validate that membership modeIds reference
|
|
||||||
// registered modes, and we do not validate snapshot GUIDs against the index.
|
|
||||||
// Stale-GUID and stale-mode tolerance is a stated invariant of this model —
|
|
||||||
// a deserialized entry is treated as trusted data, not as live cross-checked
|
|
||||||
// state. Rejecting stale entries here would violate that invariant. The one
|
|
||||||
// exception is activeMode (validated below in parseModel): a persisted active
|
|
||||||
// mode that no longer exists has an immediate behavioral consequence, so it
|
|
||||||
// is caught and the parse is rejected.
|
|
||||||
if (!idx.restore(guid, mem)) return false;
|
if (!idx.restore(guid, mem)) return false;
|
||||||
} while (r.consume(','));
|
} while (r.consume(','));
|
||||||
return r.consume(']');
|
return r.consume(']');
|
||||||
@@ -721,10 +551,8 @@ bool parseLanes(json::Reader& r, LaneOwnershipIndex& idx) {
|
|||||||
else if (!r.skipValue()) return false;
|
else if (!r.skipValue()) return false;
|
||||||
} while (r.consume(','));
|
} while (r.consume(','));
|
||||||
if (!r.consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
// Both keys mandatory and non-empty (they form the lane's identity). A managed
|
// Both keys mandatory/non-empty; managed must carry a mode, manual must not
|
||||||
// lane must carry a non-empty mode; a manual lane must not claim one. Enforcing
|
// — keeps a round-tripped index byte-for-byte identical to the source.
|
||||||
// this on parse keeps a round-tripped index byte-for-byte identical to the
|
|
||||||
// serialized one and rejects a malformed managed-without-mode entry.
|
|
||||||
if (!haveTrack || !haveLane || !haveManaged) return false;
|
if (!haveTrack || !haveLane || !haveManaged) return false;
|
||||||
if (trackGuid.empty() || laneKey.empty()) return false;
|
if (trackGuid.empty() || laneKey.empty()) return false;
|
||||||
if (managed) {
|
if (managed) {
|
||||||
@@ -769,11 +597,7 @@ bool parseModel(json::Reader& r, ViewModeModel& out) {
|
|||||||
} else if (key == "lanes") {
|
} else if (key == "lanes") {
|
||||||
if (!parseLanes(r, lanes)) return false;
|
if (!parseLanes(r, lanes)) return false;
|
||||||
} else {
|
} else {
|
||||||
// Unknown keys and the "version" field are skipped here.
|
if (!r.skipValue()) return false; // unknown keys / "version" placeholder
|
||||||
// "version" is serialized as a forward-compat placeholder — there is no
|
|
||||||
// active version gate yet; all persisted data is parsed the same way
|
|
||||||
// regardless of the value. A future gate would add a version branch here.
|
|
||||||
if (!r.skipValue()) return false;
|
|
||||||
}
|
}
|
||||||
} while (r.consume(','));
|
} while (r.consume(','));
|
||||||
|
|
||||||
|
|||||||
+164
-386
@@ -1,38 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// view_mode_model — the pure core of the Design View feature, deliberately free of any
|
// Pure core of the Design View feature — mirror of bank_model: mode registry,
|
||||||
// REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the mirror of
|
// GUID-keyed membership, folder-tree-aware visibility, park/restore planner,
|
||||||
// bank_model: it owns the mode registry, the GUID-keyed membership index, the
|
// and JSON round-trip. Folder structure is an INPUT (the D2 shell reads
|
||||||
// folder-tree-aware visibility derivation, the parking/restore planner, and the
|
// REAPER's I_FOLDERDEPTH); this model never fetches or stores REAPER's live
|
||||||
// JSON round-trip of all of it.
|
// tree. See src/core/view/CLAUDE.md for the settled invariants.
|
||||||
//
|
|
||||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
|
||||||
// vendor/ includes. Standard library only. The folder structure is an INPUT
|
|
||||||
// supplied by the D2 shell (which reads REAPER's I_FOLDERDEPTH); this model never
|
|
||||||
// fetches or stores REAPER's live tree — folder structure is REAPER's truth and
|
|
||||||
// changes underneath us, so it is passed in per query, not held.
|
|
||||||
//
|
|
||||||
// -- Representation decisions (design latitude exercised; invariants below) -----
|
|
||||||
//
|
|
||||||
// * A mode is (stable string id, display name, ordinal). Arrange (id "arrange",
|
|
||||||
// ordinal 0) and Design (id "design", ordinal 1) are seeded. Arrange is the
|
|
||||||
// fallback home for every untagged leaf; structurally it is just another mode.
|
|
||||||
//
|
|
||||||
// * Membership is GUID -> { mode ids } (a set, not a bool) plus a per-track
|
|
||||||
// show-both flag. Normally a leaf is in exactly one mode; multiple only via the
|
|
||||||
// parent-derivation rule (computed, not stored) or the show-both escape hatch.
|
|
||||||
// An untagged GUID is NOT in the index and belongs to Arrange by default.
|
|
||||||
//
|
|
||||||
// * The planner drives exactly four scalar flags (showInTcp, showInMixer,
|
|
||||||
// mainSend, fxEnable) plus a per-FX offline list. Park values are fixed zeros
|
|
||||||
// (defined by the parking contract), so PARK ops need no snapshot. RESTORE ops
|
|
||||||
// come entirely FROM a TrackSnapshot captured before parking — never a hardcoded
|
|
||||||
// default. This is where the restore-contract invariant lives and is tested.
|
|
||||||
//
|
|
||||||
// * The snapshot stores the full prior per-FX offline vector so a save-while-parked
|
|
||||||
// project round-trips and restores each FX to its exact prior offline state. The
|
|
||||||
// pure model does NOT need REAPER FX counts to plan a park (park offlines all N,
|
|
||||||
// which the shell expands from TrackFX_GetCount); it only needs them to restore,
|
|
||||||
// and it gets them from the snapshot it captured.
|
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -56,31 +27,26 @@ struct Mode {
|
|||||||
bool operator==(const Mode& o) const;
|
bool operator==(const Mode& o) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ordered registry of modes. Arrange + Design are seeded on construction. Add more
|
// Ordered registry of modes. Arrange + Design are seeded on construction; ids
|
||||||
// to prove the model is N-mode, not boolean. Ids are unique; adding a duplicate id
|
// are unique, adding a duplicate id is rejected.
|
||||||
// is rejected.
|
|
||||||
class ModeRegistry {
|
class ModeRegistry {
|
||||||
public:
|
public:
|
||||||
ModeRegistry(); // seeds Arrange (ordinal 0) + Design (ordinal 1)
|
ModeRegistry(); // seeds Arrange (ordinal 0) + Design (ordinal 1)
|
||||||
|
|
||||||
// Adds a mode. Rejects (returns false, no mutation) an empty or duplicate id.
|
|
||||||
bool add(const Mode& mode);
|
bool add(const Mode& mode);
|
||||||
|
|
||||||
// Returns the mode with `id`, or nullptr. Invalidated by any mutating call.
|
|
||||||
const Mode* query(const std::string& id) const;
|
const Mode* query(const std::string& id) const;
|
||||||
|
|
||||||
bool contains(const std::string& id) const { return query(id) != nullptr; }
|
bool contains(const std::string& id) const { return query(id) != nullptr; }
|
||||||
|
|
||||||
// All modes in ordinal order (ties broken by insertion order).
|
|
||||||
const std::vector<Mode>& all() const { return modes_; }
|
const std::vector<Mode>& all() const { return modes_; }
|
||||||
|
|
||||||
std::size_t size() const { return modes_.size(); }
|
std::size_t size() const { return modes_.size(); }
|
||||||
|
|
||||||
bool operator==(const ModeRegistry& o) const { return modes_ == o.modes_; }
|
bool operator==(const ModeRegistry& o) const { return modes_ == o.modes_; }
|
||||||
|
|
||||||
// An empty registry (no seed modes). Deserialization parses the persisted mode
|
// Empty registry (no seed modes) for deserialization, so the parsed
|
||||||
// set into this and then owns it; the default ctor's seed would otherwise make
|
// Arrange/Design don't collide with the default ctor's seeded ones.
|
||||||
// the serialized Arrange/Design collide on add() and fail to round-trip.
|
|
||||||
static ModeRegistry makeEmpty() { return ModeRegistry(EmptyTag{}); }
|
static ModeRegistry makeEmpty() { return ModeRegistry(EmptyTag{}); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -100,30 +66,12 @@ struct Membership {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// -- Lane ownership (Phase D2 / two-canvas item-level projection) -------------
|
// Item-level (fixed-lane) lane ownership. Mode operations touch only managed
|
||||||
//
|
// lanes; manual lanes are the user's own comping lanes and stay untouched —
|
||||||
// D2 extends the track-level projection to the ITEM level via REAPER fixed lanes
|
// the fixed-lane analog of never-touch-mute/solo. Lane identity is an opaque
|
||||||
// (I_FREEMODE=2). On a track shared by two stances, each mode owns a fixed lane; a
|
// key the shell supplies; this model bakes in no I_FIXEDLANE ordinal assumption.
|
||||||
// toggle shows/plays only the active mode's lane. This is the item-visibility analog
|
|
||||||
// of D1's track parking, and it carries the same load-bearing guarantee:
|
|
||||||
//
|
|
||||||
// THE TOOL DRIVES ONLY WHAT IT MINTED. A fixed-lane track is also REAPER's native
|
|
||||||
// comping surface — a user may keep their OWN manual lanes (comp takes, alternate
|
|
||||||
// reads). Mode operations touch ONLY managed lanes; manual lanes are never shown,
|
|
||||||
// hidden, silenced, or re-laned, and their C_LANEPLAYS stays exactly as set. This
|
|
||||||
// is the fixed-lane analog of "never touch B_MUTE/I_SOLO" and "never touch master".
|
|
||||||
//
|
|
||||||
// LANE IDENTITY IS AN OPAQUE, STABLE KEY SUPPLIED BY THE SHELL (boundary). The pure
|
|
||||||
// index keys a lane by (track GUID + a lane key string). The lane key is an OPAQUE
|
|
||||||
// identifier the shell provides; this model does NOT assume lane ordinals are stable
|
|
||||||
// and bakes in NO I_FIXEDLANE renumber/reorder assumptions. Whether the shell derives
|
|
||||||
// the key from a raw I_FIXEDLANE ordinal or a more durable identity — and how it keeps
|
|
||||||
// the index from going stale across lane reorder/renumber/deletion — is a Wave-2 SHELL
|
|
||||||
// design point (CONTEXT.md §Lane-identity fragility). The pure model's only contract:
|
|
||||||
// the same lane key denotes the same lane across calls.
|
|
||||||
|
|
||||||
// One lane's ownership: managed by a specific mode, or manual (user-minted, outside
|
// One lane's ownership: managed by a specific mode, or manual (user-minted).
|
||||||
// the mode system). `managedMode` present ⇒ managed by that mode id; absent ⇒ manual.
|
|
||||||
struct LaneOwnership {
|
struct LaneOwnership {
|
||||||
std::optional<std::string> managedMode; // set ⇒ managed by this mode; unset ⇒ manual
|
std::optional<std::string> managedMode; // set ⇒ managed by this mode; unset ⇒ manual
|
||||||
|
|
||||||
@@ -133,10 +81,10 @@ struct LaneOwnership {
|
|||||||
bool operator==(const LaneOwnership& o) const { return managedMode == o.managedMode; }
|
bool operator==(const LaneOwnership& o) const { return managedMode == o.managedMode; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// A lane's composite key: (track GUID, opaque lane key). Ordered so it can key a map.
|
// A lane's composite key: (track GUID, opaque lane key).
|
||||||
struct LaneRef {
|
struct LaneRef {
|
||||||
std::string trackGuid;
|
std::string trackGuid;
|
||||||
std::string laneKey; // opaque, shell-supplied; NOT assumed to be a stable ordinal
|
std::string laneKey; // opaque, shell-supplied; not assumed to be a stable ordinal
|
||||||
|
|
||||||
bool operator<(const LaneRef& o) const {
|
bool operator<(const LaneRef& o) const {
|
||||||
if (trackGuid != o.trackGuid) return trackGuid < o.trackGuid;
|
if (trackGuid != o.trackGuid) return trackGuid < o.trackGuid;
|
||||||
@@ -147,34 +95,22 @@ struct LaneRef {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// (track GUID, lane key) -> ownership. Managed lanes name their owning mode; manual
|
// (track GUID, lane key) -> ownership, GUID-keyed and portable. A lane ABSENT
|
||||||
// lanes are user-minted and off-limits to every mode operation. GUID-keyed and
|
// from the index is treated as manual by default (never minted by the tool),
|
||||||
// portable, it rides in the "reasampler" view_state alongside the membership index.
|
// so the managed-only guarantee holds even before the index is populated.
|
||||||
// A lane ABSENT from the index has no recorded ownership — the model treats an absent
|
|
||||||
// lane as manual by default (the tool never minted it), so the managed-only guarantee
|
|
||||||
// holds even before the index is populated.
|
|
||||||
class LaneOwnershipIndex {
|
class LaneOwnershipIndex {
|
||||||
public:
|
public:
|
||||||
// Records lane (trackGuid, laneKey) as MANAGED by `modeId`, replacing any prior
|
|
||||||
// ownership. Returns false if any argument is empty.
|
|
||||||
bool setManaged(const std::string& trackGuid, const std::string& laneKey,
|
bool setManaged(const std::string& trackGuid, const std::string& laneKey,
|
||||||
const std::string& modeId);
|
const std::string& modeId);
|
||||||
|
|
||||||
// Records lane (trackGuid, laneKey) as MANUAL (user-minted), replacing any prior
|
|
||||||
// ownership. Returns false if trackGuid or laneKey is empty.
|
|
||||||
bool setManual(const std::string& trackGuid, const std::string& laneKey);
|
bool setManual(const std::string& trackGuid, const std::string& laneKey);
|
||||||
|
|
||||||
// Removes the lane from the index entirely (⇒ treated as manual-by-default again).
|
// Removes the lane entirely (⇒ manual-by-default again). Returns true if present.
|
||||||
// Returns true if it was present.
|
|
||||||
bool remove(const std::string& trackGuid, const std::string& laneKey);
|
bool remove(const std::string& trackGuid, const std::string& laneKey);
|
||||||
|
|
||||||
// The ownership for a lane, or nullptr if the lane has no recorded entry (⇒ manual
|
|
||||||
// by default). Invalidated by any mutating call.
|
|
||||||
const LaneOwnership* query(const std::string& trackGuid, const std::string& laneKey) const;
|
const LaneOwnership* query(const std::string& trackGuid, const std::string& laneKey) const;
|
||||||
|
|
||||||
// True if the lane is recorded MANAGED (by any mode). A lane absent from the index
|
// Load-bearing predicate the toggle planner gates on: absent ⇒ not managed.
|
||||||
// is NOT managed (manual by default) — this is the load-bearing predicate the
|
|
||||||
// toggle planner and the "which lanes may this toggle touch" query gate on.
|
|
||||||
bool isManaged(const std::string& trackGuid, const std::string& laneKey) const {
|
bool isManaged(const std::string& trackGuid, const std::string& laneKey) const {
|
||||||
const LaneOwnership* o = query(trackGuid, laneKey);
|
const LaneOwnership* o = query(trackGuid, laneKey);
|
||||||
return o && o->isManaged();
|
return o && o->isManaged();
|
||||||
@@ -191,44 +127,32 @@ private:
|
|||||||
std::map<LaneRef, LaneOwnership> entries_; // (guid, laneKey) -> ownership
|
std::map<LaneRef, LaneOwnership> entries_; // (guid, laneKey) -> ownership
|
||||||
};
|
};
|
||||||
|
|
||||||
// The play/show state a managed lane takes for a given active mode, matching REAPER's
|
// C_LANEPLAYS value for a managed lane under the given active mode: the lane
|
||||||
// item/track-side C_LANEPLAYS values (SDK: 0=lane silent+hidden, 1=lane plays
|
// plays exclusively iff its owning mode is active, else silent+hidden. Callers
|
||||||
// exclusively). A managed lane owned by the ACTIVE mode plays (1); every other managed
|
// must only pass MANAGED lanes; manual lanes never reach this decision.
|
||||||
// lane is silenced+hidden (0) — consistent with exclusive membership and D1's "a mode
|
|
||||||
// flip is a real change, not cosmetic." Exposed as a free function for direct testing.
|
|
||||||
// managedMode == activeMode ⇒ 1 (plays exclusively)
|
|
||||||
// otherwise ⇒ 0 (does not play; hidden + silent)
|
|
||||||
// The caller must only pass MANAGED lanes here; manual lanes never reach this decision.
|
|
||||||
inline constexpr int kLanePlaysExclusive = 1; // C_LANEPLAYS: plays exclusively
|
inline constexpr int kLanePlaysExclusive = 1; // C_LANEPLAYS: plays exclusively
|
||||||
inline constexpr int kLaneSilent = 0; // C_LANEPLAYS: does not play (hidden+silent)
|
inline constexpr int kLaneSilent = 0; // C_LANEPLAYS: does not play (hidden+silent)
|
||||||
int laneModeState(const std::string& managedMode, const std::string& activeMode);
|
int laneModeState(const std::string& managedMode, const std::string& activeMode);
|
||||||
|
|
||||||
// GUID-keyed membership index. Untagged GUIDs are absent and belong to Arrange.
|
// GUID-keyed membership index. Untagged GUIDs are absent and belong to Arrange.
|
||||||
// Keyed by track GUID string, never index (reorder-safe).
|
|
||||||
class MembershipIndex {
|
class MembershipIndex {
|
||||||
public:
|
public:
|
||||||
// Tags `guid` into `modeId`, replacing any prior mode set (a leaf lives in one
|
// Tags `guid` into `modeId`, replacing any prior mode set. Returns false if
|
||||||
// mode; use showBoth for the cross-mode case). No-op-safe on repeated calls.
|
// guid or modeId is empty.
|
||||||
// Returns false if guid or modeId is empty.
|
|
||||||
bool tag(const std::string& guid, const std::string& modeId);
|
bool tag(const std::string& guid, const std::string& modeId);
|
||||||
|
|
||||||
// Removes `guid` from the index entirely (returns it to the Arrange default).
|
// Removes `guid` entirely (returns it to the Arrange default).
|
||||||
// Returns true if it was present.
|
|
||||||
bool untag(const std::string& guid);
|
bool untag(const std::string& guid);
|
||||||
|
|
||||||
// Sets the show-both flag for `guid`. Tags the guid into no new mode; if the
|
// Sets the show-both flag; creates an untagged (Arrange-default) entry if
|
||||||
// guid is untagged it is created with an empty mode set (Arrange default) so
|
// `guid` had none, so show-both alone is representable.
|
||||||
// show-both alone is representable. Returns false if guid is empty.
|
|
||||||
bool setShowBoth(const std::string& guid, bool showBoth);
|
bool setShowBoth(const std::string& guid, bool showBoth);
|
||||||
|
|
||||||
// Installs a complete membership record verbatim (multi-mode set + show-both),
|
// Installs a complete membership record verbatim, replacing any existing
|
||||||
// replacing any existing entry for `guid`. Used by deserialization to rebuild a
|
// entry. Used by deserialization to rebuild a trusted entry without tag()'s
|
||||||
// trusted, already-valid entry without tag()'s single-mode clobbering. Returns
|
// single-mode clobbering.
|
||||||
// false if guid is empty.
|
|
||||||
bool restore(const std::string& guid, const Membership& membership);
|
bool restore(const std::string& guid, const Membership& membership);
|
||||||
|
|
||||||
// Returns the membership for `guid`, or nullptr if untagged. Invalidated by any
|
|
||||||
// mutating call.
|
|
||||||
const Membership* query(const std::string& guid) const;
|
const Membership* query(const std::string& guid) const;
|
||||||
|
|
||||||
bool isShowBoth(const std::string& guid) const {
|
bool isShowBoth(const std::string& guid) const {
|
||||||
@@ -250,40 +174,32 @@ private:
|
|||||||
std::map<std::string, Membership> entries_; // guid -> membership
|
std::map<std::string, Membership> entries_; // guid -> membership
|
||||||
};
|
};
|
||||||
|
|
||||||
// -- Folder tree (INPUT, not stored) ----------------------------------------
|
// 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
|
||||||
// The shell builds this from I_FOLDERDEPTH each time and passes it to a visibility
|
// membership or any descendant leaf does, and is never parked. The master
|
||||||
// query. A node is a leaf or a parent; a parent is visible in a mode if it belongs
|
// track is implicit (always visible, untouched) and is not a node here.
|
||||||
// to that mode by its own membership OR any of its descendant leaves does, and is
|
|
||||||
// never parked. The master track is
|
|
||||||
// modeled implicitly (always visible, never touched) and is NOT a node here.
|
|
||||||
struct FolderNode {
|
struct FolderNode {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
std::string parentGuid; // empty ⇒ top-level (child of master / project root)
|
std::string parentGuid; // empty ⇒ top-level (child of master / project root)
|
||||||
bool isParent = false; // true if this node has descendant tracks (a folder)
|
bool isParent = false; // true if this node has descendant tracks (a folder)
|
||||||
};
|
};
|
||||||
|
|
||||||
// A flat parent↔child description of the current track tree. Order is arrange-view
|
// Arrange-view order; parentGuid links each node to its immediate parent folder.
|
||||||
// order; parentGuid links each node to its immediate parent folder.
|
|
||||||
struct FolderTree {
|
struct FolderTree {
|
||||||
std::vector<FolderNode> nodes;
|
std::vector<FolderNode> nodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// -- Snapshot + planner ------------------------------------------------------
|
// 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
|
||||||
// The prior value of every tool-driven flag on one track, captured BEFORE parking.
|
// faithfully round-trips whatever REAPER reported (defensive against
|
||||||
// Restore uses these values verbatim — the restore contract's source of truth.
|
// non-0/1 values).
|
||||||
// Flags mirror REAPER's numeric representation (0/1 for the bools) so the shell
|
|
||||||
// applies them without translation; ints, not bools, so a snapshot faithfully
|
|
||||||
// round-trips whatever REAPER reported (defensive against non-0/1 values).
|
|
||||||
struct TrackSnapshot {
|
struct TrackSnapshot {
|
||||||
int showInTcp = 0; // B_SHOWINTCP prior value
|
int showInTcp = 0; // B_SHOWINTCP prior value
|
||||||
int showInMixer = 0; // B_SHOWINMIXER prior value
|
int showInMixer = 0; // B_SHOWINMIXER prior value
|
||||||
int mainSend = 0; // B_MAINSEND prior value
|
int mainSend = 0; // B_MAINSEND prior value
|
||||||
int fxEnable = 0; // I_FXEN prior value
|
int fxEnable = 0; // I_FXEN prior value
|
||||||
|
|
||||||
// Prior per-FX offline state, index = fx slot. Lets restore return each FX to
|
// Prior per-FX offline state, index = fx slot.
|
||||||
// exactly its captured offline value rather than a blanket "online".
|
|
||||||
std::vector<int> fxOffline;
|
std::vector<int> fxOffline;
|
||||||
|
|
||||||
bool operator==(const TrackSnapshot& o) const {
|
bool operator==(const TrackSnapshot& o) const {
|
||||||
@@ -293,8 +209,8 @@ struct TrackSnapshot {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Which scalar flag a TrackFlagOp drives. FX-offline is carried separately (it is
|
// Which scalar flag a TrackFlagOp drives. FX-offline is carried separately (it
|
||||||
// per-slot, variable length), see TrackParkPlan::fxOffline.
|
// is per-slot, variable length) — see TrackParkPlan::fxOffline.
|
||||||
enum class Flag {
|
enum class Flag {
|
||||||
ShowInTcp, // B_SHOWINTCP
|
ShowInTcp, // B_SHOWINTCP
|
||||||
ShowInMixer, // B_SHOWINMIXER
|
ShowInMixer, // B_SHOWINMIXER
|
||||||
@@ -324,13 +240,10 @@ struct FxOfflineOp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// One managed-lane play/show write the shell must apply. The shell translates this
|
// One managed-lane play/show write the shell must apply (translated into
|
||||||
// into the REAPER lane setters (track-side C_LANEPLAYS:N and, per item, I_FIXEDLANE /
|
// C_LANEPLAYS / I_FIXEDLANE / B_FIXEDLANE_HIDDEN). Emitted for MANAGED lanes
|
||||||
// C_LANEPLAYS; B_FIXEDLANE_HIDDEN follows from the play state). `lanePlays` is a
|
// only — never a manual lane; enforced in planToggle and mirrored by
|
||||||
// C_LANEPLAYS value: kLanePlaysExclusive when the active mode owns the lane,
|
// lanesTouchedByToggle.
|
||||||
// kLaneSilent otherwise. The pure model emits these for MANAGED lanes ONLY — never a
|
|
||||||
// manual lane (the fixed-lane analog of "never touch mute/solo"), enforced in
|
|
||||||
// planToggle and mirrored by lanesTouchedByToggle.
|
|
||||||
struct LanePlayOp {
|
struct LanePlayOp {
|
||||||
std::string trackGuid;
|
std::string trackGuid;
|
||||||
std::string laneKey; // opaque, shell-supplied
|
std::string laneKey; // opaque, shell-supplied
|
||||||
@@ -341,38 +254,33 @@ struct LanePlayOp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The complete set of operations to park one inactive leaf, or restore one leaf.
|
// The complete set of operations to park one inactive leaf, or restore one
|
||||||
// Park uses fixed zeros (parking contract); restore uses a snapshot's values.
|
// leaf. Park uses fixed zeros; restore uses a snapshot's values. fxOffline is
|
||||||
// fxOffline is emitted per known FX slot: on park, from the snapshot's slot count
|
// per known FX slot: on park all slots go offline (from the snapshot's slot
|
||||||
// (all -> offline); on restore, each slot back to its captured value.
|
// count); on restore each slot returns to its captured value.
|
||||||
struct TrackPlan {
|
struct TrackPlan {
|
||||||
std::vector<TrackFlagOp> flags;
|
std::vector<TrackFlagOp> flags;
|
||||||
std::vector<FxOfflineOp> fxOffline;
|
std::vector<FxOfflineOp> fxOffline;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The plan for a whole toggle to a target mode: which tracks to park, and which to
|
// The plan for a toggle to a target mode. Parents and show-both leaves never
|
||||||
// restore from their snapshots. Parents and show-both leaves never appear here —
|
// appear (derived-visible, never parked — see visibleTracks). Untagged
|
||||||
// they are derived-visible and never parked (visibility is answered separately by
|
// leaves DO appear: an untagged leaf is an Arrange member, so it parks in
|
||||||
// visibleTracks). Untagged LEAVES DO appear: an untagged leaf is an Arrange member,
|
// every non-Arrange mode and restores in Arrange.
|
||||||
// so it parks in every non-Arrange mode and restores in Arrange — the mode system
|
|
||||||
// manages all leaves, not only tagged ones.
|
|
||||||
struct TogglePlan {
|
struct TogglePlan {
|
||||||
std::vector<TrackPlan> park; // inactive leaves -> parked (fixed zeros)
|
std::vector<TrackPlan> park; // inactive leaves -> parked (fixed zeros)
|
||||||
std::vector<TrackPlan> restore; // active leaves returning -> snapshot values
|
std::vector<TrackPlan> restore; // active leaves returning -> snapshot values
|
||||||
|
|
||||||
// D2 item-level projection: per managed lane, the C_LANEPLAYS state for the target
|
// Per managed lane, the C_LANEPLAYS state for the target mode. Managed
|
||||||
// mode (active mode's lane plays; every other managed lane silenced+hidden). MANAGED
|
// lanes only. Empty when no fixed lanes exist, so a lane-free project
|
||||||
// lanes ONLY — a manual lane never appears here. Empty when no managed lanes exist,
|
// produces an identical plan to before fixed-lane support.
|
||||||
// so a D1-only project (no fixed lanes) produces an identical plan to before.
|
|
||||||
std::vector<LanePlayOp> lanes;
|
std::vector<LanePlayOp> lanes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// -- The view mode model -----------------------------------------------------
|
// Owns the mode registry, membership index, active mode, and durable
|
||||||
//
|
// per-track snapshots (kept while parked so a save-while-parked project
|
||||||
// Owns the mode registry, the membership index, the active mode, and the durable
|
// restores correctly). Visibility and the toggle plan are computed against a
|
||||||
// per-track snapshots (kept for tracks currently parked so a save-while-parked
|
// supplied FolderTree — the tree is never stored.
|
||||||
// project restores correctly). Visibility and the toggle plan are computed against
|
|
||||||
// a supplied FolderTree — the tree is never stored.
|
|
||||||
class ViewModeModel {
|
class ViewModeModel {
|
||||||
public:
|
public:
|
||||||
ViewModeModel(); // Arrange + Design seeded; active mode = Arrange
|
ViewModeModel(); // Arrange + Design seeded; active mode = Arrange
|
||||||
@@ -385,82 +293,60 @@ public:
|
|||||||
const LaneOwnershipIndex& lanes() const { return lanes_; }
|
const LaneOwnershipIndex& lanes() const { return lanes_; }
|
||||||
|
|
||||||
const std::string& activeModeId() const { return activeModeId_; }
|
const std::string& activeModeId() const { return activeModeId_; }
|
||||||
// Sets the active mode. Returns false (no change) if the id is not registered.
|
// Returns false (no change) if the id is not registered.
|
||||||
bool setActiveMode(const std::string& modeId);
|
bool setActiveMode(const std::string& modeId);
|
||||||
|
|
||||||
// Records / clears the pre-park snapshot for a track. The shell calls store
|
// The shell calls store before it parks a track, so restore survives a save.
|
||||||
// before it parks a track; the model persists it so restore survives a save.
|
|
||||||
void storeSnapshot(const std::string& guid, const TrackSnapshot& snap);
|
void storeSnapshot(const std::string& guid, const TrackSnapshot& snap);
|
||||||
void clearSnapshot(const std::string& guid);
|
void clearSnapshot(const std::string& guid);
|
||||||
const TrackSnapshot* snapshot(const std::string& guid) const;
|
const TrackSnapshot* snapshot(const std::string& guid) const;
|
||||||
const std::map<std::string, TrackSnapshot>& snapshots() const { return snapshots_; }
|
const std::map<std::string, TrackSnapshot>& snapshots() const { return snapshots_; }
|
||||||
|
|
||||||
// Prunes orphaned per-track state: drops every snapshot whose GUID is NOT in
|
// Drops every snapshot whose GUID is NOT in `liveGuids`. Returns the count
|
||||||
// `liveGuids` (the set of GUIDs the shell currently enumerates from the project).
|
// removed.
|
||||||
// Returns the number of snapshots removed. The shell calls this before planning a
|
|
||||||
// toggle; because reapply-on-load also routes through the shell's applyMode, this
|
|
||||||
// reconciles on project open too.
|
|
||||||
//
|
//
|
||||||
// Why snapshots and NOT membership: a parked track's snapshot is dead weight once
|
// Snapshots are pruned, membership is not: a parked track's snapshot is
|
||||||
// the track is deleted — it can never be restored, and if REAPER reuses that GUID
|
// dead weight once the track is deleted (can never restore; a reused GUID
|
||||||
// for a different track a stale snapshot would drive an INCORRECT restore. So it
|
// would drive an incorrect restore). Membership survives because REAPER's
|
||||||
// must be pruned. Membership is deliberately KEPT: REAPER's undo of a track delete
|
// undo of a track delete restores the SAME GUID — dropping the tag on
|
||||||
// restores the SAME GUID, so dropping the Design tag on delete would silently lose
|
// delete would lose it on undo. A never-restored track leaves only a
|
||||||
// it on undo-delete. Keeping membership means an undone delete brings the track
|
// dormant membership entry, which is a fine trade against losing tags on
|
||||||
// back correctly tagged and it re-snapshots + re-parks cleanly on the next toggle.
|
// undo. Folder restructure is self-healing (tree rebuilt every toggle) and
|
||||||
// A genuinely-deleted-and-never-restored track leaves only a tiny dormant
|
// is not what this handles.
|
||||||
// membership entry — acceptable, and far better than losing tags on undo. Folder
|
|
||||||
// RESTRUCTURE (moving tracks without deleting) is already self-healing: the tree is
|
|
||||||
// rebuilt from I_FOLDERDEPTH every toggle, so a restructure leaves every GUID live
|
|
||||||
// and reconcile is a no-op over it. This handles DELETION specifically.
|
|
||||||
std::size_t reconcile(const std::set<std::string>& liveGuids);
|
std::size_t reconcile(const std::set<std::string>& liveGuids);
|
||||||
|
|
||||||
// Does `guid` belong to `modeId`? A leaf belongs if it is tagged into modeId,
|
// A leaf belongs if tagged into modeId, show-both, or untagged with modeId
|
||||||
// is show-both (belongs everywhere), or is untagged and modeId is Arrange (the
|
// == Arrange. No parent derivation here — see visibleTracks for that.
|
||||||
// default). Parent derivation is NOT applied here — this is the LEAF rule; use
|
|
||||||
// visibleTracks for the tree-aware answer.
|
|
||||||
bool leafBelongsToMode(const std::string& guid, const std::string& modeId) const;
|
bool leafBelongsToMode(const std::string& guid, const std::string& modeId) const;
|
||||||
|
|
||||||
// The set of track GUIDs visible in `modeId`, tree-aware: active leaves,
|
// Tree-aware visible set: active leaves, show-both leaves, and every
|
||||||
// show-both leaves, and every parent that EITHER belongs to the mode by its own
|
// parent that belongs to the mode itself or has a visible descendant.
|
||||||
// membership OR has at least one descendant visible in the mode. Untagged nodes
|
// Untagged nodes count as Arrange. Stale tree GUIDs are tolerated; the
|
||||||
// (leaf or folder) count as Arrange, so an untagged folder carrying its own
|
// master is not represented (always visible, untouched).
|
||||||
// FX/media shows in Arrange even when none of its children do, and additionally
|
|
||||||
// shows in a child's mode by derivation. Stale GUIDs in the tree are tolerated.
|
|
||||||
// The master is not represented (always visible; the shell never touches it).
|
|
||||||
std::set<std::string> visibleTracks(const FolderTree& tree,
|
std::set<std::string> visibleTracks(const FolderTree& tree,
|
||||||
const std::string& modeId) const;
|
const std::string& modeId) const;
|
||||||
|
|
||||||
// Plans a toggle to `targetMode` by enumerating EVERY leaf in the supplied tree.
|
// Enumerates every leaf in `tree`; a leaf inactive in `targetMode` is
|
||||||
// A leaf inactive in the target mode — tagged into another mode, or untagged and
|
// parked (fixed zeros), one becoming active with a stored snapshot is
|
||||||
// the target isn't Arrange — is parked with fixed zeros; a leaf that becomes
|
// restored from it. Parents and show-both leaves are never parked.
|
||||||
// active AND has a stored snapshot is restored from it. Parents (visibility-only)
|
// Untagged leaves are Arrange members and park/restore accordingly. Tree
|
||||||
// and show-both leaves (always visible) are never parked; the master is not in
|
// membership is the enumeration source, so stale membership GUIDs absent
|
||||||
// the tree. Untagged leaves ARE managed: they are Arrange members, so they park
|
// from the tree are ignored.
|
||||||
// in non-Arrange modes and restore in Arrange. Tree membership is the enumeration
|
|
||||||
// source, so stale membership GUIDs absent from the tree are naturally ignored.
|
|
||||||
//
|
//
|
||||||
// Note: park plans emitted here have an empty fxOffline vector. The D2 shell
|
// Park plans here carry an empty fxOffline vector — the D2 shell expands
|
||||||
// expands per-FX offline writes using TrackFX_GetCount — the pure model has no
|
// per-FX offline writes via TrackFX_GetCount (not available to the pure model).
|
||||||
// access to REAPER FX counts at plan time.
|
|
||||||
TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;
|
TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;
|
||||||
|
|
||||||
// The managed-only "which lanes may this toggle touch" query: the set of lane refs
|
// Managed lanes only, from the ownership index — the set a toggle may
|
||||||
// a toggle is permitted to drive — MANAGED lanes ONLY, from the ownership index.
|
// drive. Independent of the folder tree (lane ownership isn't a tree
|
||||||
// Manual lanes are NEVER in the result, regardless of target mode. This is the pure,
|
// property); the target mode decides each lane's play VALUE, not the set.
|
||||||
// testable decision behind the load-bearing invariant; the shell reads live lane
|
|
||||||
// state and applies C_LANEPLAYS only to lanes this query returns. Independent of the
|
|
||||||
// folder tree (lane ownership is not a tree property) — the target mode does not
|
|
||||||
// filter the SET (every managed lane is touchable), only the play VALUE each takes
|
|
||||||
// (see planToggle / laneModeState).
|
|
||||||
std::set<LaneRef> lanesTouchedByToggle() const;
|
std::set<LaneRef> lanesTouchedByToggle() const;
|
||||||
|
|
||||||
bool operator==(const ViewModeModel& o) const;
|
bool operator==(const ViewModeModel& o) const;
|
||||||
|
|
||||||
std::string serialize() const;
|
std::string serialize() const;
|
||||||
|
|
||||||
// Parses a JSON string produced by serialize(). std::nullopt on malformed
|
// std::nullopt on malformed input. deserialize(serialize(x)) == x on success.
|
||||||
// input. On success deserialize(serialize(x)) == x.
|
|
||||||
static std::optional<ViewModeModel> deserialize(const std::string& json);
|
static std::optional<ViewModeModel> deserialize(const std::string& json);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -471,61 +357,38 @@ private:
|
|||||||
std::map<std::string, TrackSnapshot> snapshots_; // guid -> pre-park snapshot
|
std::map<std::string, TrackSnapshot> snapshots_; // guid -> pre-park snapshot
|
||||||
};
|
};
|
||||||
|
|
||||||
// Builds the fixed-zero park plan for one leaf. Offlines `fxCount` slots. Exposed
|
// Fixed-zero park plan for one leaf, offlining `fxCount` slots.
|
||||||
// for the shell and for direct testing of the parking contract.
|
|
||||||
TrackPlan makeParkPlan(const std::string& guid, int fxCount);
|
TrackPlan makeParkPlan(const std::string& guid, int fxCount);
|
||||||
|
|
||||||
// Builds the restore plan for one leaf from its snapshot — every flag set to its
|
// Restore plan for one leaf from its snapshot — every flag to its captured
|
||||||
// captured value, never a default. Exposed for the shell and for testing the
|
// value, never a default.
|
||||||
// restore-contract invariant directly.
|
|
||||||
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
|
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
|
||||||
|
|
||||||
// -- Auto-tag decision (Phase D2) --------------------------------------------
|
// 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.
|
||||||
//
|
//
|
||||||
// New content — both new tracks and new items — is tagged to whatever mode is active
|
// Manual-lane exemption: an item landing on a MANUAL lane is off-limits.
|
||||||
// when it is created; pre-existing content defaults to Arrange. The DECISION is pure:
|
|
||||||
// the Wave-2 shell detects new GUIDs by diffing project state on the panel timer and
|
|
||||||
// asks this function what to tag. Pre-existing content (a GUID the shell does not
|
|
||||||
// report as new) never reaches here and stays at its index state (Arrange by default).
|
|
||||||
//
|
//
|
||||||
// Manual-lane exemption: an item that landed in a MANUAL lane is off-limits to auto-tag
|
// Adoption (strand fix): a new item on a track that already carries
|
||||||
// — auto-tag governs normal timeline content, not hand-managed lanes. The shell marks
|
// pre-existing content adopts that content's single mode rather than blindly
|
||||||
// such an item `onManualLane = true` (it knows the item's lane and consults the
|
// taking the active mode — otherwise the track would go multi-mode, get
|
||||||
// ownership index); the decision then emits NO tag for it. New tracks and new items on
|
// lane-split, and strand the pre-existing (previously visible) items on a
|
||||||
// managed/no lane follow the active-mode rule.
|
// 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
|
||||||
// -- Pre-existing-content adoption (strand fix) -------------------------------
|
// already spans multiple modes (an existing deliberate split).
|
||||||
//
|
|
||||||
// A new item dropped onto a track that ALREADY carries currently-visible content must
|
|
||||||
// not silently push that track into a different mode. If the pre-existing content
|
|
||||||
// resolves to ONE mode and the new item were blindly tagged to the (different) ACTIVE
|
|
||||||
// mode, the track would become multi-mode, planLaneMinting would split it, and the
|
|
||||||
// toggle would silence whichever lane the active mode does not own — stranding the
|
|
||||||
// pre-existing, previously-visible items on a C_LANEPLAYS=0 lane with no user intent.
|
|
||||||
//
|
|
||||||
// The rule: a new item ADOPTS the single mode of the pre-existing content already on its
|
|
||||||
// track. Only when the track carries no pre-existing managed-eligible content (an empty
|
|
||||||
// or brand-new track), or when that content already spans multiple modes (an existing
|
|
||||||
// deliberate split, which the new item joins under the active mode), does the new item
|
|
||||||
// fall back to the active-mode rule. Deliberate two-take splits are unaffected: those go
|
|
||||||
// through the explicit item mode-move actions (planItemRetag), never auto-tag.
|
|
||||||
// The shell reports each new item's track pre-existing-content modes in `trackModes`.
|
|
||||||
|
|
||||||
// One new item the shell detected this poll. Its lane disposition decides exemption; its
|
// One new item the shell detected this poll.
|
||||||
// track's pre-existing content modes decide adoption (see above).
|
|
||||||
struct NewItem {
|
struct NewItem {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
bool onManualLane = false; // true ⇒ EXEMPT from auto-tag (hand-managed lane)
|
bool onManualLane = false; // true ⇒ EXEMPT from auto-tag
|
||||||
// The distinct modes the PRE-EXISTING (not-new-this-tick) managed-eligible content on
|
// Distinct modes the pre-existing (not-new-this-tick) content on this
|
||||||
// this item's track resolves to. Empty ⇒ the item's track carried no prior content, so
|
// item's track resolves to. Empty ⇒ take active mode. Exactly one ⇒
|
||||||
// the item takes the active mode. Exactly one ⇒ ADOPT that mode (the strand guard).
|
// adopt it. More than one ⇒ already a deliberate split, take active mode.
|
||||||
// More than one ⇒ the track is already a deliberate split; the item takes the active
|
|
||||||
// mode. The shell fills this by resolving each pre-existing item's mode from membership.
|
|
||||||
std::set<std::string> trackModes;
|
std::set<std::string> trackModes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// One membership write the auto-tag decision produced: tag `guid` into `modeId`. The
|
// One membership write: tag `guid` into `modeId`.
|
||||||
// shell applies it to the MembershipIndex (a new track/item joins the active mode).
|
|
||||||
struct AutoTag {
|
struct AutoTag {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
std::string modeId;
|
std::string modeId;
|
||||||
@@ -533,42 +396,27 @@ struct AutoTag {
|
|||||||
bool operator==(const AutoTag& o) const { return guid == o.guid && modeId == o.modeId; }
|
bool operator==(const AutoTag& o) const { return guid == o.guid && modeId == o.modeId; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// The pure auto-tag decision: given the new track GUIDs and new items detected this
|
// Every new track is tagged to `activeMode`. Every new item is tagged unless
|
||||||
// poll plus the active mode, produce the membership writes. Every new track is tagged
|
// exempt (manual lane); its target is the adopted single mode of its track's
|
||||||
// to `activeMode`. Every new item is tagged UNLESS it landed on a manual lane (exempt);
|
// pre-existing content, else `activeMode`. Empty `activeMode` yields no tags.
|
||||||
// its target mode is the single mode of its track's pre-existing content (adoption — the
|
// Empty GUIDs are skipped. Mutates nothing.
|
||||||
// strand guard) when that content resolves to exactly one mode, otherwise `activeMode`.
|
|
||||||
// An empty `activeMode` yields no tags (nothing to tag into). Empty GUIDs are skipped.
|
|
||||||
// The result is a plan the shell applies; this function mutates nothing.
|
|
||||||
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
||||||
const std::vector<NewItem>& newItems,
|
const std::vector<NewItem>& newItems,
|
||||||
const std::string& activeMode);
|
const std::string& activeMode);
|
||||||
|
|
||||||
// -- Item-level mode-move decision (Phase D2 / Wave 3-B) ---------------------
|
// Item-level mode-move decision (bindable "Move selected items -> mode"
|
||||||
//
|
// actions): which selected items to retag, and to what. Manual-lane items
|
||||||
// The bindable item actions (Move selected items -> Design / -> Arrange / Untag)
|
// (shell-reported `onManualLane`) are exempt — never retagged, never re-laned,
|
||||||
// retag the CURRENT item selection's membership, then re-drive the minting/apply
|
// upholding the managed-lanes-only invariant under an explicit user action too.
|
||||||
// path so each moved item lands on its target mode's managed lane. The DECISION —
|
|
||||||
// which selected items to retag, and to what — is pure and unit-tested here; the
|
|
||||||
// shell only reads the item selection (GUID + manual-lane disposition) and applies
|
|
||||||
// the resulting membership writes + re-lane pass.
|
|
||||||
//
|
|
||||||
// MANAGED-LANES-ONLY INVARIANT (upheld at the source, exactly as auto-tag does): an
|
|
||||||
// item the shell reports as already on a MANUAL lane is EXEMPT — it is never retagged,
|
|
||||||
// never untagged, never re-laned. The tool drives only what it minted, even under an
|
|
||||||
// explicit user action. The shell reports `onManualLane` per item and this decision
|
|
||||||
// emits NO op for such items; the shell then skips them entirely.
|
|
||||||
|
|
||||||
// One selected item the shell reports for the retag decision: its GUID and whether it
|
// One selected item the shell reports for the retag decision.
|
||||||
// currently sits on a MANUAL lane (⇒ EXEMPT: no membership change, no re-lane).
|
|
||||||
struct RetagItem {
|
struct RetagItem {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
bool onManualLane = false; // true ⇒ EXEMPT from the item mode-move actions
|
bool onManualLane = false; // true ⇒ EXEMPT
|
||||||
};
|
};
|
||||||
|
|
||||||
// One membership op the item mode-move decision produced for one selected item. `untag`
|
// One membership op: `untag` removes the item (Arrange default); otherwise
|
||||||
// true ⇒ remove the item from the index (return it to the Arrange default); otherwise
|
// tags it into `modeId`.
|
||||||
// tag it into `modeId`. The shell applies each verbatim to the MembershipIndex.
|
|
||||||
struct ItemRetagOp {
|
struct ItemRetagOp {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
bool untag = false; // true ⇒ untag; false ⇒ tag into modeId
|
bool untag = false; // true ⇒ untag; false ⇒ tag into modeId
|
||||||
@@ -579,77 +427,48 @@ struct ItemRetagOp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The pure item mode-move decision: given the selected items and a target mode, produce
|
// Empty `targetMode` means untag (Move -> Arrange and Untag collapse to the
|
||||||
// the membership ops. An EMPTY `targetMode` means UNTAG (the "Untag selected items" and
|
// same act, mirroring the track-level doUntag). Manual-lane and empty-GUID
|
||||||
// "Move -> Arrange" actions collapse to the same act — Arrange is the absence of a tag,
|
// items are skipped. Mutates nothing.
|
||||||
// mirroring the track-level doUntag). A non-empty `targetMode` tags each eligible item
|
|
||||||
// into it. Manual-lane items are skipped (no op emitted); items with an empty GUID are
|
|
||||||
// skipped (defensive). The function mutates nothing — it returns a plan the shell applies.
|
|
||||||
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
||||||
const std::string& targetMode);
|
const std::string& targetMode);
|
||||||
|
|
||||||
// -- Lane minting decision (Phase D2 / Wave 3) -------------------------------
|
// 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.
|
||||||
//
|
//
|
||||||
// D1 parks a whole track when it holds content of only ONE mode. The moment a track
|
// "Visible in more than one mode" has two independent triggers, either
|
||||||
// is VISIBLE IN MORE THAN ONE MODE while carrying its OWN media, whole-track parking
|
// splits the track: (a) the track's own items span >= 2 modes, or (b) the
|
||||||
// can no longer keep the stances separate (the track shows in every mode it is visible
|
// track is a content-bearing folder derived-visible in >= 2 modes
|
||||||
// in, so its items leak across all of them), so the projection drops to the ITEM level:
|
// (visibleTracks) even though its own item is single-mode — the folder case
|
||||||
// the track becomes a fixed-lane track, each involved mode gets its own MANAGED lane,
|
// a naive own-item-span check would miss.
|
||||||
// and each item is assigned to its mode's lane. A toggle then shows+plays only the
|
|
||||||
// active mode's lane.
|
|
||||||
//
|
//
|
||||||
// "Visible in more than one mode" has TWO sources, and both trigger a split:
|
// Show-both tracks are skipped outright (never force-split — the point of
|
||||||
// (1) the track's OWN managed-eligible items span >= 2 modes (a leaf carrying both
|
// show-both is staying audible everywhere). Manual-lane items are exempt.
|
||||||
// an Arrange take and a Design take), OR
|
// Lanes are minted LAZILY — only for modes the track's own items actually
|
||||||
// (2) the track is a content-bearing FOLDER whose descendant leaves span modes, so
|
// occupy, never an empty reserved lane for a merely-derived-visible mode;
|
||||||
// it is DERIVED-VISIBLE in >= 2 modes (ViewModeModel::visibleTracks) even though
|
// confinement still holds because an absent lane never plays.
|
||||||
// its own single item is single-mode. This second source is why the decision is
|
|
||||||
// folder-tree / visibility aware — mirroring visibleTracks — rather than looking
|
|
||||||
// only at the track's own item mode-span. Without it, one MIDI item or capture
|
|
||||||
// dropped straight onto such a folder sits on the default lane and leaks into
|
|
||||||
// every mode the folder derives visibility in.
|
|
||||||
//
|
//
|
||||||
// SHOW-BOTH is the deliberate escape hatch: a show-both track is visible in every mode
|
// Idempotent: re-reporting an already-split track yields the same mints and
|
||||||
// ON PURPOSE and its content is meant to play in all of them. It is NEVER force-split —
|
// assignments, so re-running detection does not thrash the project or undo
|
||||||
// neither the visibility trigger nor the own-item-span trigger confines its items to
|
// history.
|
||||||
// per-mode lanes. (Confining show-both content would contradict "stay audible across
|
|
||||||
// modes.") The decision skips show-both tracks entirely.
|
|
||||||
//
|
|
||||||
// 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, builds the
|
|
||||||
// FolderTree (via the existing view_tree helper, exactly as the D1 shell does), calls
|
|
||||||
// this with the model + tree, 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
|
// One item the shell reports for the minting decision.
|
||||||
// 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 {
|
struct LaneItem {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
std::string modeId; // the mode this item's content belongs to
|
std::string modeId; // the mode this item's content belongs to
|
||||||
bool onManualLane = false; // true ⇒ EXEMPT (user's hand-managed lane)
|
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
|
// One track the shell reports: its GUID plus the items on it.
|
||||||
// enumerating the track's media items and resolving each item's mode from membership.
|
|
||||||
struct LaneTrack {
|
struct LaneTrack {
|
||||||
std::string trackGuid;
|
std::string trackGuid;
|
||||||
std::vector<LaneItem> items;
|
std::vector<LaneItem> items;
|
||||||
};
|
};
|
||||||
|
|
||||||
// One item→lane assignment the shell must apply (I_FIXEDLANE = the lane the durable
|
// One item→lane assignment the shell must apply (I_FIXEDLANE = the lane the
|
||||||
// key `laneKey` currently occupies; the shell resolves key→ordinal exactly as the
|
// durable key `laneKey` currently occupies). Only managed-eligible items appear.
|
||||||
// C_LANEPLAYS apply path does). Only managed-eligible items appear here.
|
|
||||||
struct LaneAssign {
|
struct LaneAssign {
|
||||||
std::string itemGuid;
|
std::string itemGuid;
|
||||||
std::string trackGuid;
|
std::string trackGuid;
|
||||||
@@ -660,8 +479,8 @@ struct LaneAssign {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// One managed lane the shell must mint on a track: its durable key (== the name to
|
// One managed lane the shell must mint: its durable key (== the P_LANENAME to
|
||||||
// stamp via P_LANENAME) and the mode that owns it (recorded in the ownership index).
|
// stamp) and the mode that owns it (an ownership-index write).
|
||||||
struct LaneMint {
|
struct LaneMint {
|
||||||
std::string trackGuid;
|
std::string trackGuid;
|
||||||
std::string laneKey; // == laneNameForMode(modeId); the P_LANENAME to stamp
|
std::string laneKey; // == laneNameForMode(modeId); the P_LANENAME to stamp
|
||||||
@@ -672,15 +491,13 @@ struct LaneMint {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The complete lane-minting plan for the tracks the shell reported. Empty (all three
|
// The complete plan; empty when no track needs splitting (D1 behavior
|
||||||
// vectors) when NO track needs splitting — a single-mode-only project produces an empty
|
// unchanged). The shell wraps application in one Undo block (visible
|
||||||
// plan and the shell does nothing (D1 behavior unchanged). The shell wraps the whole
|
// structural mutation).
|
||||||
// application in ONE Undo block because it is a visible structural mutation.
|
|
||||||
struct LaneMintPlan {
|
struct LaneMintPlan {
|
||||||
// Tracks to switch into fixed-lane mode, each with the number of managed lanes to
|
// Tracks to switch into fixed-lane mode (I_FREEMODE=2, I_NUMFIXEDLANES >=
|
||||||
// ensure (I_FREEMODE=2, I_NUMFIXEDLANES >= laneCount). Only tracks that need a
|
// laneCount). Idempotent — an already-split track still appears, but the
|
||||||
// split appear; a track already carrying the tool's managed lanes for exactly the
|
// shell's ensure is then a no-op.
|
||||||
// involved modes still appears (idempotent — the shell's ensure is a no-op then).
|
|
||||||
struct TrackSplit {
|
struct TrackSplit {
|
||||||
std::string trackGuid;
|
std::string trackGuid;
|
||||||
int laneCount = 0; // number of managed lanes this track needs
|
int laneCount = 0; // number of managed lanes this track needs
|
||||||
@@ -694,55 +511,16 @@ struct LaneMintPlan {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The pure lane-minting decision, folder-tree / visibility aware. `model` supplies the
|
// `model` supplies membership + show-both state; `tree` supplies folder
|
||||||
// membership + show-both state; `tree` supplies the folder structure so a content-bearing
|
// structure for the derived-visibility trigger. Items with an empty GUID or
|
||||||
// folder's DERIVED visibility is accounted for (mirrors ViewModeModel::visibleTracks).
|
// modeId are skipped (defensive). Mutates nothing.
|
||||||
// For each reported track:
|
|
||||||
// * SHOW-BOTH tracks are skipped outright — never force-split (the escape hatch: their
|
|
||||||
// content is meant to stay audible in every mode). No split, mint, or assignment.
|
|
||||||
// * Ignore items on manual lanes entirely (exempt — the managed-only invariant).
|
|
||||||
// * A track splits iff it CARRIES OWN managed-eligible media AND is VISIBLE IN >= 2
|
|
||||||
// MODES. Visibility spans two sources, either of which qualifies:
|
|
||||||
// (a) the track's own managed-eligible items span >= 2 modes (leaf carrying an
|
|
||||||
// Arrange take and a Design take), OR
|
|
||||||
// (b) the track is derived-visible in >= 2 modes per visibleTracks (a content-
|
|
||||||
// bearing folder whose descendant leaves span modes) — the missed case.
|
|
||||||
// * A track visible in exactly ONE mode (single-mode leaf, single-mode folder) stays
|
|
||||||
// whole-track-parked (D1) — NO split. This is the single-mode-track rule.
|
|
||||||
// * On a split: one TrackSplit (laneCount == number of lanes to mint), one LaneMint per
|
|
||||||
// mode the track's OWN items occupy, and one LaneAssign per managed-eligible OWN item
|
|
||||||
// onto ITS tagged mode's lane — INCLUDING pre-existing items, so a folder carrying one
|
|
||||||
// own Design item while derived-visible in Arrange too still lanes that item to the
|
|
||||||
// Design lane (it then hides+silences whenever Arrange is active).
|
|
||||||
// * LAZY-MINT: lanes are minted ONLY for modes the track's own items actually occupy —
|
|
||||||
// never an empty reserved lane for a mode the track is merely derived-visible in. So a
|
|
||||||
// folder whose own item is Design-only but which is derived-visible in Arrange mints a
|
|
||||||
// Design lane ONLY (holding the item), NOT an empty Arrange lane. Confinement still
|
|
||||||
// holds: with only a Design lane present, toggling to Arrange drives that lane's
|
|
||||||
// C_LANEPLAYS to 0 (hide+silence) and no lane plays, so the track reads as an empty
|
|
||||||
// normal track and the Design item does not leak. The Arrange lane is minted on demand
|
|
||||||
// when an Arrange item first lands. The derived-visibility trigger still decides WHETHER
|
|
||||||
// to split; it no longer inflates WHICH lanes are minted.
|
|
||||||
//
|
|
||||||
// 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 ViewModeModel& model, const FolderTree& tree,
|
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||||
const std::vector<LaneTrack>& tracks);
|
const std::vector<LaneTrack>& tracks);
|
||||||
|
|
||||||
// The next mode id in the registry's ordinal order, cycling past `currentModeId`
|
// Next mode id in ordinal order, cycling past `currentModeId` and wrapping
|
||||||
// and wrapping to the first mode after the last (Arrange -> Design -> Arrange with
|
// after the last. Empty registry -> "". currentModeId not present -> the
|
||||||
// the two seed modes; the same cycle scales to N modes with no call-site change).
|
// first mode's id. Free function (not a model member) so it is testable
|
||||||
// This is the pure decision behind the "toggle active mode" action: the shell reads
|
// against a bare ModeRegistry.
|
||||||
// the model's active mode, asks for the next one, and applies it.
|
|
||||||
// * empty registry -> "" (nothing to cycle to)
|
|
||||||
// * currentModeId not present -> the first mode's id (a sane home to jump to)
|
|
||||||
// Exposed as a free function (not a model member) so it is unit-testable against a
|
|
||||||
// bare ModeRegistry without a full ViewModeModel.
|
|
||||||
std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId);
|
std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// view_tree — pure folder-depth walk. See view_tree.h.
|
// See view_tree.h.
|
||||||
|
|
||||||
#include "core/view/view_tree.h"
|
#include "core/view/view_tree.h"
|
||||||
|
|
||||||
@@ -8,11 +8,8 @@ FolderTree buildFolderTree(const std::vector<TrackFolderEntry>& entries) {
|
|||||||
FolderTree tree;
|
FolderTree tree;
|
||||||
tree.nodes.reserve(entries.size());
|
tree.nodes.reserve(entries.size());
|
||||||
|
|
||||||
// Stack of currently-open folder-parent GUIDs. The top is the immediate parent
|
// Stack of open folder-parent GUIDs; top is the next track's parent. A
|
||||||
// of the next track. A folder-parent track opens its folder AFTER contributing
|
// folder-parent's push and a closer's pop both trail their own assignment.
|
||||||
// its own node (its own parent is the enclosing folder), so the push trails the
|
|
||||||
// assignment. A closing track belongs to the folder it closes, so the pop also
|
|
||||||
// trails the assignment.
|
|
||||||
std::vector<std::string> open;
|
std::vector<std::string> open;
|
||||||
|
|
||||||
for (const TrackFolderEntry& e : entries) {
|
for (const TrackFolderEntry& e : entries) {
|
||||||
@@ -23,11 +20,8 @@ FolderTree buildFolderTree(const std::vector<TrackFolderEntry>& entries) {
|
|||||||
tree.nodes.push_back(node);
|
tree.nodes.push_back(node);
|
||||||
|
|
||||||
if (e.folderDepth == 1) {
|
if (e.folderDepth == 1) {
|
||||||
open.push_back(e.guid); // this track's folder opens for what follows
|
open.push_back(e.guid);
|
||||||
} else if (e.folderDepth < 0) {
|
} else if (e.folderDepth < 0) {
|
||||||
// Closes |folderDepth| levels after this (already-assigned) track.
|
|
||||||
// Clamp to the stack size so a malformed/stale depth stream can't
|
|
||||||
// underflow — the walk stays total.
|
|
||||||
int levels = -e.folderDepth;
|
int levels = -e.folderDepth;
|
||||||
while (levels-- > 0 && !open.empty()) {
|
while (levels-- > 0 && !open.empty()) {
|
||||||
open.pop_back();
|
open.pop_back();
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// view_tree — the ONE genuinely pure piece of the D2 view shell: turning REAPER's
|
// Pure I_FOLDERDEPTH -> FolderTree walk; REAPER reads stay in shell/view.
|
||||||
// linear I_FOLDERDEPTH stream into the parent<->child FolderTree the pure model
|
// See src/core/view/CLAUDE.md.
|
||||||
// consumes. The REAPER reads (GetTrack / GetTrackGUID / I_FOLDERDEPTH) stay in
|
|
||||||
// view.cpp; this tree arithmetic is REAPER-free so the fiddly folder-depth walk is
|
|
||||||
// unit-tested outside the DAW (mirrors capture_paths splitting the path math out).
|
|
||||||
//
|
|
||||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
|
||||||
// vendor/ includes. Standard library + view_mode_model.h (for FolderTree) only.
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -15,19 +9,15 @@
|
|||||||
|
|
||||||
namespace reasampler::view {
|
namespace reasampler::view {
|
||||||
|
|
||||||
// One track's contribution to the folder walk, read from REAPER in arrange order.
|
// One track's contribution, read from REAPER in arrange order. folderDepth is
|
||||||
// folderDepth is I_FOLDERDEPTH verbatim: 0 = normal, 1 = folder parent (opens a
|
// I_FOLDERDEPTH verbatim: 0 normal, 1 opens a folder, <0 closes |depth| levels.
|
||||||
// folder after this track), <0 = closes |folderDepth| folder levels after this
|
|
||||||
// track (-1 last in innermost, -2 last in innermost + next-innermost, ...).
|
|
||||||
struct TrackFolderEntry {
|
struct TrackFolderEntry {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
int folderDepth = 0;
|
int folderDepth = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Walks the ordered entries, tracking the open-folder stack, and assigns each
|
// Assigns each node its parentGuid (empty = top level) and isParent. Total: a
|
||||||
// node its immediate parentGuid (empty = top level) and isParent (opens a folder).
|
// malformed depth stream (close deeper than the stack) clamps rather than faults.
|
||||||
// Pure and total: tolerates malformed depth streams (a close deeper than the stack
|
|
||||||
// is clamped to empty) so a corrupt/stale project can never fault the shell.
|
|
||||||
FolderTree buildFolderTree(const std::vector<TrackFolderEntry>& entries);
|
FolderTree buildFolderTree(const std::vector<TrackFolderEntry>& entries);
|
||||||
|
|
||||||
} // namespace reasampler::view
|
} // namespace reasampler::view
|
||||||
|
|||||||
+130
-269
@@ -1,12 +1,7 @@
|
|||||||
// view.cpp — REAPER-facing Design View shell (Phase D2). See view.h.
|
// See view.h. Compiled into the reaper_reasampler module; includes
|
||||||
//
|
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Tree arithmetic lives in view_tree (pure); this file owns REAPER reads/writes
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
// and the snapshot-before-park ordering.
|
||||||
// pointers; here they are extern (CLAUDE.md §contract).
|
|
||||||
//
|
|
||||||
// The tree arithmetic (I_FOLDERDEPTH -> FolderTree) lives in the pure view_tree
|
|
||||||
// module so it is unit-tested outside the DAW; this file owns only the REAPER
|
|
||||||
// reads/writes and the snapshot-before-park ordering.
|
|
||||||
|
|
||||||
#include "shell/view/view.h"
|
#include "shell/view/view.h"
|
||||||
|
|
||||||
@@ -37,8 +32,8 @@
|
|||||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||||
#define REAPERAPI_WANT_UpdateArrange
|
#define REAPERAPI_WANT_UpdateArrange
|
||||||
#define REAPERAPI_WANT_UpdateTimeline
|
#define REAPERAPI_WANT_UpdateTimeline
|
||||||
// Lane minting (D2 Wave 3): enumerate a track's items and read/write item-side lane
|
// Lane minting (D2 Wave 3): item-side lane reads/writes to assign each item to
|
||||||
// state to assign each item to its mode's managed lane.
|
// its mode's managed lane.
|
||||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||||
@@ -47,7 +42,6 @@
|
|||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
|
|
||||||
using view::buildFolderTree;
|
using view::buildFolderTree;
|
||||||
using view::isOnManualLane;
|
using view::isOnManualLane;
|
||||||
using view::managedLaneKey;
|
using view::managedLaneKey;
|
||||||
@@ -56,35 +50,24 @@ using view::TrackFolderEntry;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Track fixed-lane mode value (I_FREEMODE=2). See SDK: 0=normal, 1=free item
|
// I_FREEMODE value for fixed lanes. SDK: 0=normal, 1=free item positioning, 2=fixed lanes.
|
||||||
// positioning, 2=fixed lanes.
|
|
||||||
constexpr int kFreeModeFixedLanes = 2;
|
constexpr int kFreeModeFixedLanes = 2;
|
||||||
|
|
||||||
// C_LANESCOLLAPSED display value (char*). SDK: 1=lanes collapsed,
|
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
||||||
// 2=track displays as non-fixed-lanes but hidden lanes exist. Value 2 is the lever that
|
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
||||||
// makes a tool-split track read like a NORMAL single-lane track showing only the playing
|
// but displays as non-fixed-lane).
|
||||||
// lane — the inactive/silenced managed lanes are present but not drawn as separate rows.
|
|
||||||
constexpr int kLanesDisplayAsNormal = 2;
|
constexpr int kLanesDisplayAsNormal = 2;
|
||||||
|
|
||||||
// C_LANESETTINGS bit (char* bitmask). SDK: &32=hide lane buttons. We OR this in (never
|
// C_LANESETTINGS &32 = hide per-lane buttons; OR'd in, never clobbering the
|
||||||
// clobber the whole mask) to strip the per-lane button chrome from a tool-split track, so
|
// mask. Deliberately NOT setting &1 (auto-remove empty lanes): the lazy-mint
|
||||||
// it reads as an ordinary track. We deliberately do NOT set &1 (auto-remove empty lanes at
|
// decision never mints an empty lane, so &1 buys nothing and risks REAPER
|
||||||
// bottom): a managed lane whose item is later deleted would be silently removed out from
|
// silently removing a managed lane out from under the ownership index.
|
||||||
// under the ownership index. The lazy-mint decision already avoids ever minting an empty
|
|
||||||
// lane, so &1 buys nothing and risks a reconcile hazard.
|
|
||||||
constexpr int kLaneSettingsHideButtons = 32;
|
constexpr int kLaneSettingsHideButtons = 32;
|
||||||
|
|
||||||
// Drives a TOOL-SPLIT track's display transparent: C_LANESCOLLAPSED=2 (render like a normal
|
// Makes a tool-split track's display read as an ordinary track. Gated by every
|
||||||
// single-lane track showing only the playing lane) + OR C_LANESETTINGS &32 (hide lane
|
// caller on the tool-driven transition INTO fixed lanes (freeMode != 2 before
|
||||||
// buttons). Both are char* params driven through the double API, same convention as
|
// the flip) — a track already in fixed-lane mode (the user's own) never
|
||||||
// C_LANEPLAYS:N. C_LANESETTINGS is read-modify-write so any pre-existing bit is preserved.
|
// reaches this, so a user's comp-lane display prefs are never stomped.
|
||||||
//
|
|
||||||
// MANAGED-VS-MANUAL BOUNDARY (load-bearing): these are TRACK-LEVEL settings that affect the
|
|
||||||
// whole track including a user's own manual comp lanes. Every caller gates this on the
|
|
||||||
// tool-driven transition INTO fixed lanes (freeMode != 2 before the flip), so a track the
|
|
||||||
// user already had in fixed-lane mode never reaches it and the user's comp-lane display
|
|
||||||
// prefs are never stomped. Idempotent: a re-run finds the track already at I_FREEMODE==2,
|
|
||||||
// the transition branch is skipped, and these writes do not fire again.
|
|
||||||
void applyTransparentLaneDisplay(MediaTrack* tr) {
|
void applyTransparentLaneDisplay(MediaTrack* tr) {
|
||||||
SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED",
|
SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED",
|
||||||
static_cast<double>(kLanesDisplayAsNormal));
|
static_cast<double>(kLanesDisplayAsNormal));
|
||||||
@@ -93,8 +76,6 @@ void applyTransparentLaneDisplay(MediaTrack* tr) {
|
|||||||
static_cast<double>(settings | kLaneSettingsHideButtons));
|
static_cast<double>(settings | kLaneSettingsHideButtons));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The parmname for each planner Flag. All four are documented bool*/int* track
|
|
||||||
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
|
|
||||||
const char* flagParm(Flag f) {
|
const char* flagParm(Flag f) {
|
||||||
switch (f) {
|
switch (f) {
|
||||||
case Flag::ShowInTcp: return "B_SHOWINTCP";
|
case Flag::ShowInTcp: return "B_SHOWINTCP";
|
||||||
@@ -105,11 +86,9 @@ const char* flagParm(Flag f) {
|
|||||||
return "B_SHOWINTCP"; // unreachable; keeps the compiler quiet
|
return "B_SHOWINTCP"; // unreachable; keeps the compiler quiet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the arrange-ordered track list and their I_FOLDERDEPTH, keyed by GUID.
|
// The master track is not enumerated by GetTrack (index space excludes it),
|
||||||
// The master track is NOT enumerated by GetTrack (index space is the non-master
|
// so it can never enter the tree — the master-untouched invariant holds by
|
||||||
// tracks), so it can never enter the tree — the master-untouched invariant holds
|
// construction. Also caches each MediaTrack* by GUID for later resolve().
|
||||||
// by construction. Also caches the MediaTrack* per GUID so later apply steps
|
|
||||||
// resolve a GUID back to its handle without a second linear scan.
|
|
||||||
std::vector<TrackFolderEntry> readFolderEntries(
|
std::vector<TrackFolderEntry> readFolderEntries(
|
||||||
ReaProject* proj,
|
ReaProject* proj,
|
||||||
std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||||
@@ -137,10 +116,8 @@ MediaTrack* resolve(const std::vector<std::pair<std::string, MediaTrack*>>& hand
|
|||||||
return nullptr; // stale/deleted GUID — pruned by being skipped
|
return nullptr; // stale/deleted GUID — pruned by being skipped
|
||||||
}
|
}
|
||||||
|
|
||||||
// Captures a track's prior driven-flag state BEFORE it is parked. Reads only the
|
// Captures prior driven-flag state before parking. Never reads B_MUTE/I_SOLO;
|
||||||
// four owned flags + per-FX offline; never B_MUTE/I_SOLO, never the master (not
|
// ints preserve whatever REAPER reported (TrackSnapshot's defensive contract).
|
||||||
// reachable here). ints preserve whatever REAPER reported (defensive per D1's
|
|
||||||
// TrackSnapshot contract).
|
|
||||||
TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||||
TrackSnapshot snap;
|
TrackSnapshot snap;
|
||||||
snap.showInTcp = static_cast<int>(GetMediaTrackInfo_Value(tr, "B_SHOWINTCP"));
|
snap.showInTcp = static_cast<int>(GetMediaTrackInfo_Value(tr, "B_SHOWINTCP"));
|
||||||
@@ -156,16 +133,14 @@ TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
|||||||
return snap;
|
return snap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the planner's scalar-flag writes. B_* are bool* params, I_FXEN is int*,
|
|
||||||
// all driven through the double API — marshal the plan's int value to double.
|
|
||||||
void applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags) {
|
void applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags) {
|
||||||
for (const TrackFlagOp& op : flags) {
|
for (const TrackFlagOp& op : flags) {
|
||||||
SetMediaTrackInfo_Value(tr, flagParm(op.flag), static_cast<double>(op.value));
|
SetMediaTrackInfo_Value(tr, flagParm(op.flag), static_cast<double>(op.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parks a track's FX offline: the pure park plan leaves fxOffline empty by design;
|
// The pure park plan leaves fxOffline empty by design; expand it here from the
|
||||||
// the shell expands it from the live FX count and offlines every slot.
|
// live FX count.
|
||||||
void parkFxOffline(MediaTrack* tr) {
|
void parkFxOffline(MediaTrack* tr) {
|
||||||
int fxCount = TrackFX_GetCount(tr);
|
int fxCount = TrackFX_GetCount(tr);
|
||||||
for (int fx = 0; fx < fxCount; ++fx) {
|
for (int fx = 0; fx < fxCount; ++fx) {
|
||||||
@@ -173,15 +148,13 @@ void parkFxOffline(MediaTrack* tr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restores per-FX offline from the snapshot verbatim — each slot back to its
|
// Restores per-FX offline from the snapshot, bounds-checked against the live
|
||||||
// captured value, never a blanket "online". Bounds-checked against the live FX
|
// FX count (prune-safe if the chain changed while parked).
|
||||||
// count in case the plugin chain changed while parked (prune-safe).
|
|
||||||
//
|
//
|
||||||
// HAZARD (deferred, PLAN "reconcile on delete/restructure"): the remap is by
|
// HAZARD (open, tracked in docs/TODO.md): this remaps by slot INDEX, not
|
||||||
// slot INDEX, not plugin identity. If the FX chain changed while the track was
|
// plugin identity. If the FX chain reshuffled while parked, snapshot slot k
|
||||||
// parked, snapshot slot k is restored onto whatever plugin now occupies slot k —
|
// restores onto whatever plugin now occupies slot k. Accepted for now;
|
||||||
// the bounds-check guards against out-of-range, not against a reshuffled chain.
|
// identity-based reconciliation is future hardening.
|
||||||
// Acceptable for D2; full identity-based reconciliation is future hardening.
|
|
||||||
void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline) {
|
void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline) {
|
||||||
int fxCount = TrackFX_GetCount(tr);
|
int fxCount = TrackFX_GetCount(tr);
|
||||||
for (const FxOfflineOp& op : fxOffline) {
|
for (const FxOfflineOp& op : fxOffline) {
|
||||||
@@ -190,18 +163,14 @@ void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Managed-lane application (D2 Wave 2) ------------------------------------
|
// Managed-lane application: the pure planner keys LanePlayOps by the lane's
|
||||||
//
|
// DURABLE name; REAPER's C_LANEPLAYS:N is keyed by current ordinal, which
|
||||||
// The pure planner emits LanePlayOps keyed by (trackGuid, laneKey) where laneKey is
|
// renumbers on reorder. So every write here re-resolves durable key -> current
|
||||||
// the lane's DURABLE name (lane_keys convention: "reasampler:<mode>"). REAPER's
|
// ordinal first. A lane whose name lacks the managed prefix never enters this
|
||||||
// C_LANEPLAYS:N is keyed by the lane's CURRENT ORDINAL, which renumbers on reorder.
|
// map and so can never be driven.
|
||||||
// So before applying, we build the ordinal<->key reconcile for a track by reading each
|
|
||||||
// lane's P_LANENAME:n; the write then targets the correct current ordinal for a given
|
|
||||||
// durable key even after a reorder (design point #2). A lane whose name lacks the
|
|
||||||
// managed prefix is manual and never appears in this map, so it can never be driven.
|
|
||||||
|
|
||||||
// Reads lane index `laneIdx`'s durable name off track `tr` (P_LANENAME:n). Empty if
|
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
||||||
// the lane is unnamed or the param is unavailable (non-fixed-lane track).
|
// unavailable (non-fixed-lane track).
|
||||||
std::string laneName(MediaTrack* tr, int laneIdx) {
|
std::string laneName(MediaTrack* tr, int laneIdx) {
|
||||||
char parm[32];
|
char parm[32];
|
||||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||||
@@ -210,9 +179,8 @@ std::string laneName(MediaTrack* tr, int laneIdx) {
|
|||||||
return std::string(buf);
|
return std::string(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps each MANAGED lane's durable key -> its current ordinal on `tr`, by walking the
|
// Managed lane durable key -> current ordinal on `tr`. Manual lanes are
|
||||||
// track's I_NUMFIXEDLANES lanes and reading each name. Manual (unprefixed/unnamed)
|
// omitted, so a key absent from the map must not be driven.
|
||||||
// lanes are omitted, so a key absent from the map is a lane the tool must not drive.
|
|
||||||
std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
||||||
std::map<std::string, int> byKey;
|
std::map<std::string, int> byKey;
|
||||||
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||||
@@ -223,37 +191,25 @@ std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
|||||||
return byKey;
|
return byKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drives one managed lane on `tr` to `lanePlays` (C_LANEPLAYS value) via the
|
// Track-side C_LANEPLAYS:N alone hides+silences every item on lane N (SDK:
|
||||||
// TRACK-SIDE C_LANEPLAYS:N write. Track-side C_LANEPLAYS:N alone produces the
|
// item-side C_LANEPLAYS is read-only, so no per-item write exists or is
|
||||||
// hide+silence effect for all items on lane N — no per-item write is needed or
|
// needed). B_FIXEDLANE_HIDDEN is also read-only — hide/show follows from
|
||||||
// possible (item-side C_LANEPLAYS is marked read-only in the SDK).
|
// C_LANEPLAYS=0/1, never written directly.
|
||||||
// B_FIXEDLANE_HIDDEN is READ-ONLY (SDK) — hide/show follows from C_LANEPLAYS=0/1,
|
|
||||||
// never written directly. Non-destructive: only reversible play/show flags; no item
|
|
||||||
// is moved or deleted.
|
|
||||||
//
|
|
||||||
// DAW-VERIFY: confirm that track-side C_LANEPLAYS:N alone hides+silences all items
|
|
||||||
// on lane N without a per-item write. (SDK marks item-side C_LANEPLAYS as read-only;
|
|
||||||
// the track-side write is the documented mechanism.)
|
|
||||||
void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
||||||
char parm[32];
|
char parm[32];
|
||||||
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
||||||
SetMediaTrackInfo_Value(tr, parm, static_cast<double>(lanePlays));
|
SetMediaTrackInfo_Value(tr, parm, static_cast<double>(lanePlays));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the plan's managed-lane ops. Groups ops by track, resolves each op's durable
|
// Groups ops by track, reconciles each op's durable laneKey to the track's
|
||||||
// laneKey to the track's current ordinal (skipping any key not present on the live
|
// current ordinal (a stale/renamed/deleted key is pruned, never mis-driven),
|
||||||
// track — a stale/renamed/deleted managed lane is pruned, never mis-driven), enables
|
// enables fixed-lane mode on any track carrying a managed lane, and drives
|
||||||
// fixed-lane mode on any track that carries a managed lane, and drives C_LANEPLAYS.
|
// C_LANEPLAYS. UpdateTimeline() is the caller's job when this returns true
|
||||||
// UpdateTimeline() is called ONCE at the end (SDK: required after I_FREEMODE changes).
|
// (SDK: required after an I_FREEMODE change).
|
||||||
// Returns true if any track's I_FREEMODE was (re)set to fixed lanes (⇒ needs timeline
|
|
||||||
// refresh). MANAGED lanes only — plan.lanes never contains a manual lane (pure planner
|
|
||||||
// gates on the ownership index), and a manual lane's name never resolves to a key here,
|
|
||||||
// so the invariant is enforced twice.
|
|
||||||
bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid,
|
bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid,
|
||||||
const std::vector<LanePlayOp>& lanes) {
|
const std::vector<LanePlayOp>& lanes) {
|
||||||
if (lanes.empty()) return false;
|
if (lanes.empty()) return false;
|
||||||
|
|
||||||
// Group op indices by track guid so we read each track's lane map once.
|
|
||||||
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
||||||
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
||||||
|
|
||||||
@@ -262,22 +218,18 @@ bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handle
|
|||||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||||
if (!tr) continue; // stale GUID — prune
|
if (!tr) continue; // stale GUID — prune
|
||||||
|
|
||||||
// Ensure fixed-lane mode is on before driving lane play state. A track carrying
|
// Every track reaching here already owns a managed lane (planToggle
|
||||||
// a managed lane must be in I_FREEMODE=2; set it only if not already, and flag
|
// only emits ops for managed lanes), so re-asserting fixed-lane mode
|
||||||
// that a timeline refresh is owed. Every track reaching this loop is already in the
|
// is always a tool-driven (re)split — never a user's untouched
|
||||||
// managed-lane ownership index (planToggle only emits ops for managed lanes), so a
|
// manual-fixed-lane track — and gets the same transparent display.
|
||||||
// track here is one the TOOL split — a re-assert of fixed-lane mode is a tool-driven
|
|
||||||
// (re)split and must carry the same transparent display, mirroring applyMintPlan's
|
|
||||||
// transition branch. It is never a user's untouched manual-fixed-lane track.
|
|
||||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||||
if (freeMode != kFreeModeFixedLanes) {
|
if (freeMode != kFreeModeFixedLanes) {
|
||||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
||||||
static_cast<double>(kFreeModeFixedLanes));
|
static_cast<double>(kFreeModeFixedLanes));
|
||||||
applyTransparentLaneDisplay(tr); // tool-managed track ⇒ read like a normal track
|
applyTransparentLaneDisplay(tr);
|
||||||
touchedFreeMode = true;
|
touchedFreeMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconcile durable keys -> current ordinals on THIS track, then drive each op.
|
|
||||||
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||||
for (const LanePlayOp* op : ops) {
|
for (const LanePlayOp* op : ops) {
|
||||||
auto it = ordinals.find(op->laneKey);
|
auto it = ordinals.find(op->laneKey);
|
||||||
@@ -288,20 +240,12 @@ bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handle
|
|||||||
return touchedFreeMode;
|
return touchedFreeMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Managed-lane minting (D2 Wave 3) ----------------------------------------
|
// Managed-lane minting: the DECISION (which tracks split, which lanes, which
|
||||||
//
|
// item goes where) is planLaneMinting; this shell only reads live per-item
|
||||||
// Mints one managed fixed lane per mode on any track that now holds content of MORE
|
// mode+lane state, calls it, and applies the resulting writes.
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Item GUID + fixed-lane name reads come from the shared item_read seam (item_read.h):
|
// Maps every item GUID on `tr` to its handle in one pass (avoids a per-item
|
||||||
// itemGuid(it) and itemLaneName(tr, it). view.cpp no longer carries its own copies.
|
// re-scan in the assign loop).
|
||||||
|
|
||||||
// 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*> itemHandlesByGuid(MediaTrack* tr) {
|
||||||
std::map<std::string, MediaItem*> byGuid;
|
std::map<std::string, MediaItem*> byGuid;
|
||||||
const int itemCount = CountTrackMediaItems(tr);
|
const int itemCount = CountTrackMediaItems(tr);
|
||||||
@@ -314,23 +258,18 @@ std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
|||||||
return byGuid;
|
return byGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves the mode one item's content belongs to, from the model's membership index.
|
// An untagged item is Arrange by default (mirrors leafBelongsToMode). A
|
||||||
// An item tagged into exactly one mode returns that mode; an untagged item is an
|
// show-both/multi-mode item resolves to its first mode id — unusual for lane
|
||||||
// Arrange member by default (mirrors leafBelongsToMode's untagged rule). A show-both or
|
// content, and any one mode is sufficient for the decision.
|
||||||
// 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) {
|
std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) {
|
||||||
const std::set<std::string> modes = model.membership().modesOf(itemGuid);
|
const std::set<std::string> modes = model.membership().modesOf(itemGuid);
|
||||||
if (modes.empty()) return kArrangeModeId; // untagged ⇒ Arrange default
|
if (modes.empty()) return kArrangeModeId;
|
||||||
return *modes.begin();
|
return *modes.begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds the per-track LaneItem picture the pure decision consumes. For each track and
|
// Builds the per-track LaneItem picture the pure decision consumes. Manual-
|
||||||
// each item: resolve the item's mode from membership, and — only on a track already in
|
// lane reads are skipped on a non-fixed-lane track (isOnManualLane is false
|
||||||
// fixed-lane mode — read whether it sits on a MANUAL lane (exempt). On a non-fixed-lane
|
// there regardless of name).
|
||||||
// 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(
|
std::vector<LaneTrack> readLaneTracks(
|
||||||
const ViewModeModel& model,
|
const ViewModeModel& model,
|
||||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||||
@@ -353,9 +292,6 @@ std::vector<LaneTrack> readLaneTracks(
|
|||||||
LaneItem li;
|
LaneItem li;
|
||||||
li.guid = ig;
|
li.guid = ig;
|
||||||
li.modeId = itemModeFromMembership(model, 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 ? itemLaneName(tr, it) : std::string{};
|
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
||||||
li.onManualLane = isOnManualLane(fixedLane, ln);
|
li.onManualLane = isOnManualLane(fixedLane, ln);
|
||||||
lt.items.push_back(std::move(li));
|
lt.items.push_back(std::move(li));
|
||||||
@@ -365,12 +301,9 @@ std::vector<LaneTrack> readLaneTracks(
|
|||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assigns item `it` to the managed lane whose durable key resolves to a current ordinal
|
// Idempotent: writes I_FIXEDLANE only when it differs from the item's current
|
||||||
// on `tr` (via managedLaneOrdinals). Idempotent: writes I_FIXEDLANE only when it differs
|
// lane. Non-destructive — only this reversible flag is written, never a move
|
||||||
// from the item's current lane, so a re-run does not thrash the item or the undo state.
|
// in time or across tracks.
|
||||||
// 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) {
|
bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
||||||
const int current = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
const int current = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||||
if (current == laneOrdinal) return false; // already there — no-op
|
if (current == laneOrdinal) return false; // already there — no-op
|
||||||
@@ -378,22 +311,16 @@ bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the pure LaneMintPlan to the live project. For each track that must split:
|
// Applies the pure LaneMintPlan. Returns true if any project write actually
|
||||||
// enables fixed lanes, ensures the lane count, stamps each managed lane's durable name,
|
// changed state (⇒ caller keeps the Undo block and refreshes the timeline).
|
||||||
// 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
|
// The plan only ever names managed-prefixed lanes and only ever assigns
|
||||||
// ever assigns managed-eligible items (manual-lane items were reported exempt and are
|
// managed-eligible items; I_NUMFIXEDLANES is only ever GROWN, never shrunk,
|
||||||
// absent from the plan). We only ever GROW I_NUMFIXEDLANES to fit the managed lanes and
|
// so a user's existing manual lanes are never renamed or reassigned.
|
||||||
// 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,
|
bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
// Group mints + assigns by track so each track is set up once.
|
|
||||||
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
||||||
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
||||||
std::map<std::string, std::vector<const LaneAssign*>> assignsByTrack;
|
std::map<std::string, std::vector<const LaneAssign*>> assignsByTrack;
|
||||||
@@ -403,37 +330,26 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
|||||||
MediaTrack* tr = resolve(handleByGuid, split.trackGuid);
|
MediaTrack* tr = resolve(handleByGuid, split.trackGuid);
|
||||||
if (!tr) continue; // stale GUID — prune
|
if (!tr) continue; // stale GUID — prune
|
||||||
|
|
||||||
// Enable fixed-lane mode if not already (SDK: UpdateTimeline() owed after). The
|
// A track not already in fixed-lane mode is one the tool is splitting
|
||||||
// pre-write freeMode read is ALSO the managed-vs-manual boundary signal: a track that
|
// now, so it owns the display; a track already at I_FREEMODE==2 (the
|
||||||
// was NOT in fixed-lane mode here is one the TOOL is splitting now, so the tool owns
|
// user's own, or a prior tool run) skips this and keeps its display prefs.
|
||||||
// its lane display and drives it transparent. A track already at I_FREEMODE==2 (user
|
|
||||||
// had fixed lanes, or a prior tool run) skips this branch — its C_LANESCOLLAPSED /
|
|
||||||
// C_LANESETTINGS are left exactly as the user set them.
|
|
||||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||||
if (freeMode != kFreeModeFixedLanes) {
|
if (freeMode != kFreeModeFixedLanes) {
|
||||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
|
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
|
||||||
applyTransparentLaneDisplay(tr); // tool-split track ⇒ read like a normal track
|
applyTransparentLaneDisplay(tr);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure enough lanes for the managed set WITHOUT shrinking: a track may already
|
// Grow-only: a track may already carry the user's manual lanes, so the
|
||||||
// carry the user's manual lanes, so only GROW the count, never reduce it (which
|
// lane count only ever increases; managed lanes occupy the tail ordinals.
|
||||||
// would delete a user lane). The managed lanes we mint occupy the tail ordinals.
|
|
||||||
// laneCount tracks the live I_NUMFIXEDLANES as we grow it: read ONCE here, then
|
|
||||||
// each mint appends at laneCount and bumps it. No per-mint I_NUMFIXEDLANES re-read
|
|
||||||
// is needed — nextOrdinal and laneCount are the same running value.
|
|
||||||
int laneCount = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
int laneCount = 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);
|
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.
|
|
||||||
for (const LaneMint* m : mintsByTrack[split.trackGuid]) {
|
for (const LaneMint* m : mintsByTrack[split.trackGuid]) {
|
||||||
model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership
|
model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership
|
||||||
if (present.count(m->laneKey)) continue; // already minted — idempotent
|
if (present.count(m->laneKey)) continue; // already minted — idempotent
|
||||||
|
|
||||||
// Append at the current tail ordinal, grow the tracked count, stamp its name.
|
|
||||||
const int laneIdx = laneCount++;
|
const int laneIdx = laneCount++;
|
||||||
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast<double>(laneCount));
|
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast<double>(laneCount));
|
||||||
char parm[32];
|
char parm[32];
|
||||||
@@ -445,10 +361,6 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
|||||||
changed = true;
|
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, int> ordinals = managedLaneOrdinals(tr);
|
||||||
const std::map<std::string, MediaItem*> itemsByGuid = itemHandlesByGuid(tr);
|
const std::map<std::string, MediaItem*> itemsByGuid = itemHandlesByGuid(tr);
|
||||||
for (const LaneAssign* a : assignsByTrack[split.trackGuid]) {
|
for (const LaneAssign* a : assignsByTrack[split.trackGuid]) {
|
||||||
@@ -465,21 +377,18 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||||
// Reject an unregistered target before touching the project (no partial apply).
|
|
||||||
if (!model.modes().contains(targetModeId)) {
|
if (!model.modes().contains(targetModeId)) {
|
||||||
return false;
|
return false; // reject before touching the project — no partial apply
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||||
FolderTree tree = buildFolderTree(entries);
|
FolderTree tree = buildFolderTree(entries);
|
||||||
|
|
||||||
// Reconcile orphaned model state BEFORE planning: prune snapshots whose track was
|
// Prune snapshots for tracks no longer in the live enumeration before
|
||||||
// deleted from the project (its GUID no longer appears in the live enumeration).
|
// planning (membership is intentionally left alone — see model.reconcile).
|
||||||
// handleByGuid holds every currently-enumerated track GUID, so its keys are the
|
// Because reapply-on-load routes through applyMode, this also reconciles
|
||||||
// authoritative live set. Membership is intentionally NOT pruned (undo-delete
|
// on project open.
|
||||||
// restores the same GUID — see ViewModeModel::reconcile). Because reapply-on-load
|
|
||||||
// routes through applyMode, this also reconciles on project open.
|
|
||||||
std::set<std::string> liveGuids;
|
std::set<std::string> liveGuids;
|
||||||
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
|
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
|
||||||
model.reconcile(liveGuids);
|
model.reconcile(liveGuids);
|
||||||
@@ -488,31 +397,25 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
|
|
||||||
Undo_BeginBlock2(proj);
|
Undo_BeginBlock2(proj);
|
||||||
|
|
||||||
// PARK: snapshot BEFORE mutating, store into the model (so restore survives a
|
// PARK: snapshot before mutating, store into the model, then apply.
|
||||||
// save-while-parked), then apply the park writes + expand the FX-offline loop.
|
|
||||||
for (const TrackPlan& tp : plan.park) {
|
for (const TrackPlan& tp : plan.park) {
|
||||||
// Every op in a TrackPlan targets the same track; take the guid from the
|
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
||||||
// first flag op (the pure park plan always emits the four flag ops).
|
|
||||||
if (tp.flags.empty()) continue;
|
|
||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||||
if (!tr) continue; // stale GUID — prune
|
if (!tr) continue; // stale GUID — prune
|
||||||
|
|
||||||
// Snapshot ONCE, at the first park. If a snapshot already exists the track is
|
// Snapshot ONCE, at first park: a snapshot already present means the
|
||||||
// still parked from a prior apply, and its live flags are the PARKED (hidden)
|
// track is still parked from a prior apply, so its live flags are the
|
||||||
// values — recapturing here would overwrite the true pre-park state with zeros,
|
// parked values — recapturing would overwrite the true pre-park state
|
||||||
// so a later restore would restore the track to hidden and it would vanish for
|
// with zeros and a later restore would hide it for good. Restore
|
||||||
// good. Re-applying the park flags to an already-parked track is idempotent and
|
// clears the snapshot, so the next genuine park recaptures fresh state.
|
||||||
// fine; only the snapshot must not be recaptured. Restore clears the snapshot,
|
|
||||||
// so the next genuine park recaptures fresh state.
|
|
||||||
if (model.snapshot(guid) == nullptr)
|
if (model.snapshot(guid) == nullptr)
|
||||||
model.storeSnapshot(guid, snapshotTrack(tr));
|
model.storeSnapshot(guid, snapshotTrack(tr));
|
||||||
applyFlags(tr, tp.flags);
|
applyFlags(tr, tp.flags);
|
||||||
parkFxOffline(tr);
|
parkFxOffline(tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESTORE: apply the snapshot-sourced flag + per-FX offline writes verbatim,
|
// RESTORE: apply verbatim, then drop the consumed snapshot.
|
||||||
// then drop the now-consumed snapshot so a re-park recaptures fresh state.
|
|
||||||
for (const TrackPlan& tp : plan.restore) {
|
for (const TrackPlan& tp : plan.restore) {
|
||||||
if (tp.flags.empty()) continue;
|
if (tp.flags.empty()) continue;
|
||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
@@ -524,21 +427,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
model.clearSnapshot(guid);
|
model.clearSnapshot(guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MANAGED LANES (D2 item-level projection): drive C_LANEPLAYS so the active mode's
|
// MANAGED LANES: drive C_LANEPLAYS so the active mode's lane plays+shows
|
||||||
// managed lane plays+shows and every inactive-mode managed lane is silenced+hidden.
|
// and every other managed lane is silenced+hidden. Empty for a D1-only
|
||||||
// plan.lanes carries MANAGED lanes only (the pure planner gates on the ownership
|
// project, leaving that behavior byte-identical.
|
||||||
// index); applyLaneOps additionally resolves each op's durable key against the live
|
|
||||||
// track's lane names, so a manual lane — which never carries the managed prefix —
|
|
||||||
// can never be driven. Empty for a D1-only project (no fixed lanes), leaving D1
|
|
||||||
// behavior byte-identical. UpdateTimeline() is owed only if a track's I_FREEMODE
|
|
||||||
// was (re)set to fixed lanes (SDK requirement); deferred to the refresh block below.
|
|
||||||
const bool laneModeChanged = applyLaneOps(handleByGuid, plan.lanes);
|
const bool laneModeChanged = applyLaneOps(handleByGuid, plan.lanes);
|
||||||
|
|
||||||
// PARENT VISIBILITY (never parked): visibleTracks() marks a parent visible when
|
// PARENT VISIBILITY (never parked): recomputed every toggle, never
|
||||||
// a descendant leaf is visible in the target mode OR the parent belongs to the
|
// snapshotted. Only the two visibility flags — never mainSend/FX on a parent.
|
||||||
// mode by its own membership (untagged folder → Arrange default). Recomputed
|
|
||||||
// every toggle rather than snapshotted. Drive only the two visibility flags;
|
|
||||||
// never touch B_MAINSEND/I_FXEN/FX-offline on a parent.
|
|
||||||
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
||||||
for (const FolderNode& node : tree.nodes) {
|
for (const FolderNode& node : tree.nodes) {
|
||||||
if (!node.isParent) continue;
|
if (!node.isParent) continue;
|
||||||
@@ -549,10 +444,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
SetMediaTrackInfo_Value(tr, "B_SHOWINMIXER", show);
|
SetMediaTrackInfo_Value(tr, "B_SHOWINMIXER", show);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the undo label from the ACTUAL target mode's display name, so activating
|
// Target is guaranteed registered (checked at entry); fall back to the id
|
||||||
// Arrange doesn't leave an "activate Design view" undo point (and vice versa).
|
// defensively if that ever changes.
|
||||||
// The target is guaranteed registered (checked at entry), so query() is non-null;
|
|
||||||
// fall back to the id defensively if that ever changes.
|
|
||||||
const Mode* targetMode = model.modes().query(targetModeId);
|
const Mode* targetMode = model.modes().query(targetModeId);
|
||||||
const std::string undoLabel =
|
const std::string undoLabel =
|
||||||
"ReaSampler: activate " +
|
"ReaSampler: activate " +
|
||||||
@@ -560,18 +453,14 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
|
|
||||||
model.setActiveMode(targetModeId);
|
model.setActiveMode(targetModeId);
|
||||||
|
|
||||||
// Force REAPER to rebuild the TCP + MCP so visibility/park changes appear now,
|
// Force REAPER to rebuild the TCP/MCP now rather than on the next user
|
||||||
// not on the user's next TCP interaction. TrackList_AdjustWindows(false) does the
|
// interaction: TrackList_AdjustWindows(false) does the full relayout owed
|
||||||
// major (full) relayout required when tracks appear/disappear from the panels;
|
// when tracks appear/disappear; UpdateArrange() repaints.
|
||||||
// UpdateArrange() repaints the arrange view. Both are documented for exactly this
|
|
||||||
// "you changed track-info flags, now refresh the panels" case.
|
|
||||||
TrackList_AdjustWindows(false);
|
TrackList_AdjustWindows(false);
|
||||||
UpdateArrange();
|
UpdateArrange();
|
||||||
|
|
||||||
// A fixed-lane mode change (I_FREEMODE -> 2) requires UpdateTimeline() to take
|
// UpdateTimeline() is owed only when a track was actually toggled into
|
||||||
// visible effect (SDK). Call it only when we actually toggled a track into fixed
|
// fixed lanes this apply (SDK requirement for I_FREEMODE changes).
|
||||||
// lanes this apply; the C_LANEPLAYS writes themselves are picked up by the arrange
|
|
||||||
// refresh above.
|
|
||||||
if (laneModeChanged) UpdateTimeline();
|
if (laneModeChanged) UpdateTimeline();
|
||||||
|
|
||||||
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
|
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
|
||||||
@@ -581,63 +470,41 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||||
// The minting decision is now folder-tree / visibility aware: it needs the tree to
|
// The tree is needed to detect a content-bearing folder derived-visible in
|
||||||
// detect a content-bearing folder derived-visible in >1 mode (which must lane-separate
|
// >1 mode, exactly as applyMode builds it.
|
||||||
// its own media even when that media is single-mode). Build it exactly as applyMode does.
|
|
||||||
const FolderTree tree = buildFolderTree(entries);
|
const FolderTree tree = buildFolderTree(entries);
|
||||||
|
|
||||||
// Build the live per-track item picture and run the PURE decision. A track visible in
|
|
||||||
// exactly one mode produces no split; a track visible in >1 mode while carrying its own
|
|
||||||
// media (own items span modes, OR a folder derived-visible across modes) produces mints
|
|
||||||
// + assignments. Manual-lane items are reported exempt inside readLaneTracks; show-both
|
|
||||||
// tracks are skipped inside the decision.
|
|
||||||
const std::vector<LaneTrack> tracks = readLaneTracks(model, handleByGuid);
|
const std::vector<LaneTrack> tracks = readLaneTracks(model, handleByGuid);
|
||||||
const LaneMintPlan plan = planLaneMinting(model, tree, tracks);
|
const LaneMintPlan plan = planLaneMinting(model, tree, tracks);
|
||||||
if (plan.empty()) return false; // nothing to mint — no Undo point for a no-op tick
|
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);
|
Undo_BeginBlock2(proj);
|
||||||
const bool changed = applyMintPlan(model, plan, handleByGuid);
|
const bool changed = applyMintPlan(model, plan, handleByGuid);
|
||||||
|
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
// The plan was non-empty but every REAPER write was already satisfied. Close the
|
// Plan was non-empty but every write was already satisfied — discard
|
||||||
// block with no description so REAPER discards the empty undo point rather than
|
// the empty undo point rather than flooding history every detect tick.
|
||||||
// flooding history with a no-change entry every detection tick.
|
|
||||||
Undo_EndBlock2(proj, "", 0);
|
Undo_EndBlock2(proj, "", 0);
|
||||||
|
|
||||||
// BUT the arrange still needs a redraw. On the detect-tick caller (bankPanelRefresh)
|
// The arrange still needs a redraw: this no-op path is reached when a
|
||||||
// mintManagedLanes runs only when this tick just tagged new content, and a NON-EMPTY
|
// freshly-inserted item already landed on the active mode's playing
|
||||||
// plan means that content sits on a managed-split track. The idempotent no-op path is
|
// lane (REAPER places new items on the playing lane), so
|
||||||
// reached when a freshly-inserted item ALREADY landed on the active mode's playing
|
// assignItemToLane wrote nothing even though the item needs to appear
|
||||||
// lane (REAPER places a new item on the playing lane; the active mode's lane IS the
|
// there now. UpdateArrange() alone (no I_FREEMODE change happened, so
|
||||||
// playing lane, so assignItemToLane sees I_FIXEDLANE unchanged and writes nothing).
|
// UpdateTimeline isn't owed) is a repaint, not a mutation — stays
|
||||||
// The item is correctly placed and confined, but the arrange was never told to
|
// outside the undo block.
|
||||||
// repaint it onto the lane — so it stayed invisible until a manual mode toggle forced
|
|
||||||
// applyMode's refresh. Force the redraw here so the item appears immediately without a
|
|
||||||
// toggle. UpdateArrange() only repaints (no I_FREEMODE transition happened on this
|
|
||||||
// path, so UpdateTimeline is not owed); it is NOT a project mutation, so it stays
|
|
||||||
// outside the undo block and adds no history entry. On the action caller (doMoveItems)
|
|
||||||
// this is a harmless repaint immediately before its own reapplyActiveMode() refresh.
|
|
||||||
UpdateArrange();
|
UpdateArrange();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reapply the active mode's lane visibility so the freshly-minted lanes take their
|
// Reapply the active mode's lane visibility so freshly-minted lanes take
|
||||||
// correct play/show state immediately: the active mode's lane plays+shows, every
|
// their play/show state immediately. applyMode is deliberately NOT reused
|
||||||
// other managed lane hides+silences. Reusing planToggle's lane ops keeps the drive
|
// here — it would re-park/restore whole tracks and recompute parent
|
||||||
// logic in one place; applyLaneOps also (re)asserts I_FREEMODE and drives C_LANEPLAYS.
|
// visibility, which a lane-only mint must not touch.
|
||||||
// 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());
|
const TogglePlan togglePlan = model.planToggle(FolderTree{}, model.activeModeId());
|
||||||
applyLaneOps(handleByGuid, togglePlan.lanes);
|
applyLaneOps(handleByGuid, togglePlan.lanes);
|
||||||
|
|
||||||
// I_FREEMODE was (re)set to fixed lanes on at least one track (the plan minted a
|
UpdateTimeline(); // a split happened this call — refresh is owed
|
||||||
// split), so a timeline refresh is owed (SDK). Repaint the arrange too so the new
|
|
||||||
// lane layout appears immediately.
|
|
||||||
UpdateTimeline();
|
|
||||||
UpdateArrange();
|
UpdateArrange();
|
||||||
|
|
||||||
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", -1);
|
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", -1);
|
||||||
@@ -648,12 +515,9 @@ void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
|||||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||||
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
||||||
|
|
||||||
// Walk every track's lanes; for each lane whose durable name carries the managed
|
// Pure read of REAPER state (no lane created, no I_FREEMODE/I_NUMFIXEDLANES/
|
||||||
// prefix, record it MANAGED-for-its-mode in the ownership index. This is a pure READ
|
// I_FIXEDLANE written) plus an ownership-index write, recovering managed
|
||||||
// of REAPER state (no lane is created, no I_FREEMODE/I_NUMFIXEDLANES/I_FIXEDLANE is
|
// classification from the durable name. An unprefixed lane is left alone.
|
||||||
// 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) {
|
for (const auto& [guid, tr] : handleByGuid) {
|
||||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||||
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
||||||
@@ -666,15 +530,12 @@ void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
|||||||
std::optional<std::string> mode = modeIdFromLaneName(name);
|
std::optional<std::string> mode = modeIdFromLaneName(name);
|
||||||
if (!mode) continue; // prefix-only/illegal name — skip defensively
|
if (!mode) continue; // prefix-only/illegal name — skip defensively
|
||||||
|
|
||||||
// UNREGISTERED-MODE GUARD: the durable name encodes a mode id, but that mode
|
// A mode id encoded in the name may no longer be registered (e.g.
|
||||||
// may no longer be a registered Mode (e.g. a mode removed from the registry
|
// removed since the project was saved). Recording it managed would
|
||||||
// after the project was saved with lanes minted for it). Recording it MANAGED
|
// make the toggle planner drive a lane keyed to a mode that can
|
||||||
// would make the toggle planner drive a lane keyed to a mode that can never be
|
// never be active — permanently silenced, orphaning its items. So
|
||||||
// the active mode — the lane would stay silenced+hidden forever, orphaning its
|
// skip: the lane stays off the index (manual-by-default) but keeps
|
||||||
// items with no way for the user to reach them. So we do NOT record it: the
|
// its name, so a later re-registration of the mode heals cleanly.
|
||||||
// lane is left off the ownership index and thus treated as manual-by-default
|
|
||||||
// (never driven). Its durable name is preserved on the track, so if the mode is
|
|
||||||
// ever re-registered a later reconcile recovers the ownership cleanly.
|
|
||||||
if (!model.modes().contains(*mode)) continue;
|
if (!model.modes().contains(*mode)) continue;
|
||||||
model.lanes().setManaged(guid, *key, *mode);
|
model.lanes().setManaged(guid, *key, *mode);
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-78
@@ -1,95 +1,37 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// view — the REAPER-facing shell of the Design View feature (Phase D2). It is the
|
// REAPER-facing shell of Design View (D2): reads the live folder tree, runs
|
||||||
// mirror of the capture shell: the ViewModeModel (pure, D1) holds the mode/
|
// ViewModeModel's pure planner, and applies the resulting flag / per-FX /
|
||||||
// membership/snapshot state and emits the toggle plan; this shell reads the live
|
// lane writes. The .cpp is the sole REAPER-facing TU here (CLAUDE.md contract:
|
||||||
// project's folder tree, snapshots the tracks it is about to park, runs the model's
|
// only main.cpp defines the API pointers). See src/shell/view/CLAUDE.md for
|
||||||
// planner, and applies the resulting flag + per-FX-offline writes to REAPER.
|
// the enforced invariants (never touch master/mute/solo, snapshot-based restore).
|
||||||
//
|
|
||||||
// It includes view_mode_model (pure) but NO REAPER headers — the .cpp is the one
|
|
||||||
// REAPER-facing translation unit (CLAUDE.md §contract: only main.cpp defines the
|
|
||||||
// API pointers; every other .cpp gets them extern). Callers (persist, actions)
|
|
||||||
// depend on this seam without dragging the SDK into their include sites.
|
|
||||||
//
|
|
||||||
// Hard invariants this shell enforces (CONTEXT.md §Design View, precision
|
|
||||||
// invariants) — verified in self-review, never crossed:
|
|
||||||
// * Never touches the master track's visibility (SDK forbids B_SHOWINTCP/
|
|
||||||
// B_SHOWINMIXER on master); the master is never a node in the tree.
|
|
||||||
// * Never reads or writes B_MUTE / I_SOLO on any track.
|
|
||||||
// * Manages ALL leaves via the mode system: an untagged leaf is an Arrange member,
|
|
||||||
// so it is fully parked in non-Arrange modes and restored in Arrange, identically
|
|
||||||
// to a tagged leaf. show-both is the always-visible escape; parents are
|
|
||||||
// visibility-only (derived); the master is never touched.
|
|
||||||
// * Snapshots every to-be-parked track's prior flags BEFORE parking, storing
|
|
||||||
// them into the model so restore is faithful and survives a save-while-parked.
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/view/view_mode_model.h"
|
#include "core/view/view_mode_model.h"
|
||||||
|
|
||||||
// REAPER's opaque project handle. Forward-declared to keep this header SDK-free;
|
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||||
// the .cpp includes reaper_plugin_functions.h and sees the real class.
|
|
||||||
class ReaProject;
|
class ReaProject;
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// Applies `targetModeId` to the live project `proj`:
|
// Snapshots each about-to-park track's flags into `model`, runs planToggle,
|
||||||
// 1. Reads the arrange-ordered track list, builds the FolderTree from
|
// applies park/restore writes plus parent visibility flags, then sets the
|
||||||
// I_FOLDERDEPTH (via the pure buildFolderTree helper).
|
// active mode. Wrapped in one Undo block. Returns false (no mutation) if
|
||||||
// 2. Runs model.planToggle(tree, targetModeId).
|
// `targetModeId` isn't registered. `proj` == nullptr means the current project.
|
||||||
// 3. For each track about to be PARKED: snapshots its current B_SHOWINTCP /
|
|
||||||
// B_SHOWINMIXER / B_MAINSEND / I_FXEN and per-FX offline state, stores the
|
|
||||||
// snapshot into the model, THEN applies the park writes (expanding the
|
|
||||||
// per-FX offline loop from TrackFX_GetCount, which the pure plan leaves empty).
|
|
||||||
// 4. For each track to RESTORE: applies the plan's snapshot-sourced flag + per-FX
|
|
||||||
// offline writes verbatim.
|
|
||||||
// 5. For each PARENT (folder) node: drives B_SHOWINTCP / B_SHOWINMIXER to 1 if the
|
|
||||||
// parent is in model.visibleTracks(tree, targetModeId), else 0 — derived from
|
|
||||||
// membership, never parked/snapshotted. Only the two visibility flags.
|
|
||||||
// 6. Sets the model's active mode to `targetModeId`.
|
|
||||||
// All track mutations are wrapped in Undo_BeginBlock2 / Undo_EndBlock2.
|
|
||||||
//
|
|
||||||
// Returns false (no mutation, active mode unchanged) if `targetModeId` is not a
|
|
||||||
// registered mode. `proj` may be nullptr to mean REAPER's current project.
|
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
||||||
|
|
||||||
// Mints managed fixed lanes for any track in `proj` that is VISIBLE IN MORE THAN ONE
|
// Splits any track visible in more than one mode while carrying its own media
|
||||||
// MODE while carrying its own media, and assigns each item to its mode's managed lane
|
// into fixed lanes (one managed lane per involved mode), assigns items, and
|
||||||
// (Phase D2 Wave 3; visibility trigger added by the folder-media fix).
|
// records ownership in `model`. Never touches manual lanes. Runs planLaneMinting
|
||||||
// 1. Enumerates every track + its items; resolves each item's mode from the model's
|
// (model + tree aware); wraps the mutation in one Undo block when non-empty.
|
||||||
// membership (untagged ⇒ Arrange) and reads whether it currently sits on a MANUAL
|
// Returns true if any lane was minted (repaint hint). `proj` == nullptr means
|
||||||
// lane (exempt). Builds the FolderTree (I_FOLDERDEPTH) so derived visibility counts.
|
// the current project.
|
||||||
// 2. Runs the pure planLaneMinting decision (model + tree aware). A track visible in
|
|
||||||
// exactly one mode is left whole-track-parked (D1) — NOT lane-split. A track visible
|
|
||||||
// in >1 mode while carrying own media splits: its own items span modes, OR it is a
|
|
||||||
// content-bearing folder derived-visible across modes. show-both tracks never 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);
|
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||||
|
|
||||||
// Reconciles the model's lane-ownership index against the live project's lanes on
|
// Recovers managed-lane ownership from durable P_LANENAME on project open —
|
||||||
// project open (Phase D2 Wave 3). REAPER's durable P_LANENAME is the source of truth for
|
// self-healing, without minting/reassigning anything and without touching
|
||||||
// lane identity across sessions (design point #2): a lane whose name carries the managed
|
// membership. A lane without the managed prefix is left untouched (manual).
|
||||||
// prefix is tool-managed and owned by the mode encoded in that name. This walks every
|
// `proj` == nullptr means the current project.
|
||||||
// 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);
|
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
Reference in New Issue
Block a user