fix(view): lane-separate a content-bearing folder's own media when it's derived-visible in >1 mode
planLaneMinting now takes the model + folder tree and splits a track visible in 2+ modes that carries own media (own-item span OR folder derived-visibility), fixing an item dropped on a cross-mode folder leaking into every mode. Show-both tracks are never force-split.
This commit is contained in:
+53
-19
@@ -522,16 +522,36 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
// -- 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.
|
||||
// is VISIBLE IN MORE THAN ONE MODE while carrying its OWN media, whole-track parking
|
||||
// can no longer keep the stances separate (the track shows in every mode it is visible
|
||||
// in, so its items leak across all of them), 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.
|
||||
//
|
||||
// "Visible in more than one mode" has TWO sources, and both trigger a split:
|
||||
// (1) the track's OWN managed-eligible items span >= 2 modes (a leaf carrying both
|
||||
// an Arrange take and a Design take), OR
|
||||
// (2) the track is a content-bearing FOLDER whose descendant leaves span modes, so
|
||||
// it is DERIVED-VISIBLE in >= 2 modes (ViewModeModel::visibleTracks) even though
|
||||
// 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
|
||||
// ON PURPOSE and its content is meant to play in all of them. It is NEVER force-split —
|
||||
// neither the visibility trigger nor the own-item-span trigger confines its items to
|
||||
// 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, 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 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
|
||||
@@ -605,17 +625,30 @@ struct LaneMintPlan {
|
||||
}
|
||||
};
|
||||
|
||||
// The pure lane-minting decision. For each reported track:
|
||||
// The pure lane-minting decision, folder-tree / visibility aware. `model` supplies the
|
||||
// membership + show-both state; `tree` supplies the folder structure so a content-bearing
|
||||
// folder's DERIVED visibility is accounted for (mirrors ViewModeModel::visibleTracks).
|
||||
// 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).
|
||||
// * 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.
|
||||
// * 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
|
||||
// involved mode, 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). The lanes minted are the union of:
|
||||
// the modes the track's own items belong to, PLUS the modes the track is visible in —
|
||||
// so a folder whose own item is Design-only but which is derived-visible in Arrange
|
||||
// mints BOTH a Design lane (holding the item) and an Arrange lane (empty, reserving
|
||||
// the Arrange stance's slot), matching "each mode owns a fixed lane."
|
||||
//
|
||||
// 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
|
||||
@@ -624,7 +657,8 @@ struct LaneMintPlan {
|
||||
// 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);
|
||||
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user