feat(view): mint managed lanes to separate cross-mode content per stance (D2 W3-A)
Pure planLaneMinting decides which tracks hold >1 mode's content and which managed lane each item lands on; view.cpp applies it (I_FREEMODE/I_NUMFIXEDLANES/P_LANENAME/ I_FIXEDLANE) under one undo block, driven off the auto-tag detection tick. Manual lanes and their items are never touched. Load-time reconcile rebuilds ownership from durable lane names before reapplying active-mode visibility.
This commit is contained in:
@@ -519,6 +519,113 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
const std::vector<NewItem>& newItems,
|
||||
const std::string& activeMode);
|
||||
|
||||
// -- Lane minting decision (Phase D2 / Wave 3) -------------------------------
|
||||
//
|
||||
// D1 parks a whole track when it holds content of only ONE mode. The moment a track
|
||||
// would carry content of MORE THAN ONE mode, whole-track parking can no longer keep
|
||||
// the stances separate (the track is visible in every mode its content belongs to),
|
||||
// so the projection drops to the ITEM level: the track becomes a fixed-lane track,
|
||||
// each involved mode gets its own MANAGED lane, and each item is assigned to its
|
||||
// mode's lane. A toggle then shows+plays only the active mode's lane.
|
||||
//
|
||||
// This is the pure DECISION behind that transition — REAPER-free and unit-tested.
|
||||
// The shell reads each track's items and their live mode+lane disposition, calls this,
|
||||
// and applies the resulting REAPER writes (I_FREEMODE / I_NUMFIXEDLANES / P_LANENAME /
|
||||
// I_FIXEDLANE) plus the ownership-index writes. The DECISION never lives in the shell.
|
||||
//
|
||||
// THE MANAGED-LANES-ONLY INVARIANT is upheld here at the source: an item the shell
|
||||
// reports as already on a MANUAL lane is EXEMPT — it is never counted toward the
|
||||
// multi-mode test, never reassigned, and its lane is never minted-over. The plan only
|
||||
// ever names lanes with the managed prefix (laneNameForMode) and only ever moves
|
||||
// managed-eligible items. A track the user already lane-splits for their own comping
|
||||
// is handled by minting ADDITIONAL managed lanes alongside the user's manual lanes;
|
||||
// the manual lanes and the items on them are untouched (they are reported exempt).
|
||||
|
||||
// One item the shell reports for the minting decision: its GUID, the mode its
|
||||
// membership resolves to (untagged ⇒ Arrange, resolved by the shell via
|
||||
// leafBelongsToMode / the active-mode default), and whether it currently sits on a
|
||||
// MANUAL lane (⇒ exempt: never counted, never reassigned).
|
||||
struct LaneItem {
|
||||
std::string guid;
|
||||
std::string modeId; // the mode this item's content belongs to
|
||||
bool onManualLane = false; // true ⇒ EXEMPT (user's hand-managed lane)
|
||||
};
|
||||
|
||||
// One track the shell reports: its GUID plus the items on it. The shell builds this by
|
||||
// enumerating the track's media items and resolving each item's mode from membership.
|
||||
struct LaneTrack {
|
||||
std::string trackGuid;
|
||||
std::vector<LaneItem> items;
|
||||
};
|
||||
|
||||
// One item→lane assignment the shell must apply (I_FIXEDLANE = the lane the durable
|
||||
// key `laneKey` currently occupies; the shell resolves key→ordinal exactly as the
|
||||
// C_LANEPLAYS apply path does). Only managed-eligible items appear here.
|
||||
struct LaneAssign {
|
||||
std::string itemGuid;
|
||||
std::string trackGuid;
|
||||
std::string laneKey; // durable managed-lane key (laneNameForMode(modeId))
|
||||
|
||||
bool operator==(const LaneAssign& o) const {
|
||||
return itemGuid == o.itemGuid && trackGuid == o.trackGuid && laneKey == o.laneKey;
|
||||
}
|
||||
};
|
||||
|
||||
// One managed lane the shell must mint on a track: its durable key (== the name to
|
||||
// stamp via P_LANENAME) and the mode that owns it (recorded in the ownership index).
|
||||
struct LaneMint {
|
||||
std::string trackGuid;
|
||||
std::string laneKey; // == laneNameForMode(modeId); the P_LANENAME to stamp
|
||||
std::string modeId; // the owning mode (ownership-index managed-for-mode write)
|
||||
|
||||
bool operator==(const LaneMint& o) const {
|
||||
return trackGuid == o.trackGuid && laneKey == o.laneKey && modeId == o.modeId;
|
||||
}
|
||||
};
|
||||
|
||||
// The complete lane-minting plan for the tracks the shell reported. Empty (all three
|
||||
// vectors) when NO track needs splitting — a single-mode-only project produces an empty
|
||||
// plan and the shell does nothing (D1 behavior unchanged). The shell wraps the whole
|
||||
// application in ONE Undo block because it is a visible structural mutation.
|
||||
struct LaneMintPlan {
|
||||
// Tracks to switch into fixed-lane mode, each with the number of managed lanes to
|
||||
// ensure (I_FREEMODE=2, I_NUMFIXEDLANES >= laneCount). Only tracks that need a
|
||||
// split appear; a track already carrying the tool's managed lanes for exactly the
|
||||
// involved modes still appears (idempotent — the shell's ensure is a no-op then).
|
||||
struct TrackSplit {
|
||||
std::string trackGuid;
|
||||
int laneCount = 0; // number of managed lanes this track needs
|
||||
};
|
||||
std::vector<TrackSplit> splits;
|
||||
std::vector<LaneMint> mints; // managed lanes to mint (name + ownership write)
|
||||
std::vector<LaneAssign> assigns; // item→managed-lane assignments
|
||||
|
||||
bool empty() const {
|
||||
return splits.empty() && mints.empty() && assigns.empty();
|
||||
}
|
||||
};
|
||||
|
||||
// The pure lane-minting decision. For each reported track:
|
||||
// * Ignore items on manual lanes entirely (exempt — the managed-only invariant).
|
||||
// * Collect the DISTINCT modes the remaining (managed-eligible) items belong to.
|
||||
// * If that set has < 2 modes, the track stays whole-track-parked (D1) — NO split,
|
||||
// NO mint, NO assignment. This is the single-mode-track rule.
|
||||
// * If it has >= 2 modes, the track transitions to lane-split: emit one TrackSplit
|
||||
// (laneCount == number of involved modes), one LaneMint per involved mode (durable
|
||||
// key laneNameForMode(mode), owned by that mode), and one LaneAssign per managed-
|
||||
// eligible item onto its mode's lane — INCLUDING the pre-existing items, so a
|
||||
// single-mode track that just gained a second mode retroactively lanes ALL its
|
||||
// items, not only the newly-added one.
|
||||
//
|
||||
// Items with an empty GUID or empty modeId are skipped (defensive; a real item always
|
||||
// resolves to a mode). The function mutates nothing — it returns a plan the shell
|
||||
// applies. Idempotency: re-reporting an already-split track yields the same mints and
|
||||
// assignments; the shell's ensure/assign writes are no-ops when the state already
|
||||
// matches, so re-running the detection path does not thrash the project or the undo
|
||||
// history (the shell only opens an Undo block when the plan is non-empty AND some
|
||||
// write actually changes state — see the shell).
|
||||
LaneMintPlan planLaneMinting(const std::vector<LaneTrack>& tracks);
|
||||
|
||||
// The next mode id in the registry's ordinal order, cycling past `currentModeId`
|
||||
// and wrapping to the first mode after the last (Arrange -> Design -> Arrange with
|
||||
// the two seed modes; the same cycle scales to N modes with no call-site change).
|
||||
|
||||
Reference in New Issue
Block a user