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:
+11
-5
@@ -543,13 +543,19 @@ 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;
|
||||||
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||||
|
// The minting decision is now folder-tree / visibility aware: it needs the tree to
|
||||||
|
// detect a content-bearing folder derived-visible in >1 mode (which must lane-separate
|
||||||
|
// its own media even when that media is single-mode). Build it exactly as applyMode does.
|
||||||
|
const FolderTree tree = buildFolderTree(entries);
|
||||||
|
|
||||||
// Build the live per-track item picture and run the PURE decision. A single-mode
|
// Build the live per-track item picture and run the PURE decision. A track visible in
|
||||||
// track produces no split; a track that now holds >1 mode's content produces mints
|
// exactly one mode produces no split; a track visible in >1 mode while carrying its own
|
||||||
// + assignments. Manual-lane items are reported exempt inside readLaneTracks.
|
// 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(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
|
// Wrap the structural mutation in ONE Undo block (unlike the invisible membership
|
||||||
|
|||||||
+8
-5
@@ -52,13 +52,16 @@ namespace reasampler {
|
|||||||
// registered mode. `proj` may be nullptr to mean REAPER's current project.
|
// 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 now holds content of MORE
|
// Mints managed fixed lanes for any track in `proj` that is VISIBLE IN MORE THAN ONE
|
||||||
// THAN ONE mode, and assigns each item to its mode's managed lane (Phase D2 Wave 3).
|
// MODE while carrying its own media, and assigns each item to its mode's managed lane
|
||||||
|
// (Phase D2 Wave 3; visibility trigger added by the folder-media fix).
|
||||||
// 1. Enumerates every track + its items; resolves each item's mode from the model's
|
// 1. Enumerates every track + its items; resolves each item's mode from the model's
|
||||||
// membership (untagged ⇒ Arrange) and reads whether it currently sits on a MANUAL
|
// membership (untagged ⇒ Arrange) and reads whether it currently sits on a MANUAL
|
||||||
// lane (exempt).
|
// lane (exempt). Builds the FolderTree (I_FOLDERDEPTH) so derived visibility counts.
|
||||||
// 2. Runs the pure planLaneMinting decision. A track with content of only one mode
|
// 2. Runs the pure planLaneMinting decision (model + tree aware). A track visible in
|
||||||
// is left whole-track-parked (D1) — NOT lane-split.
|
// 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
|
// 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
|
// 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
|
// (P_LANENAME:n), records the lane MANAGED-for-its-mode in the model's ownership
|
||||||
|
|||||||
+59
-15
@@ -162,41 +162,85 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
|||||||
// lane minting decision
|
// lane minting decision
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
LaneMintPlan planLaneMinting(const std::vector<LaneTrack>& tracks) {
|
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||||
|
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
|
||||||
|
// those mode ids — tree-aware, so a content-bearing folder's DERIVED visibility
|
||||||
|
// (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;
|
||||||
|
for (const Mode& mode : model.modes().all()) {
|
||||||
|
const std::set<std::string> vis = model.visibleTracks(tree, mode.id);
|
||||||
|
for (const std::string& guid : vis)
|
||||||
|
visibleModesOf[guid].insert(mode.id);
|
||||||
|
}
|
||||||
|
|
||||||
for (const LaneTrack& track : tracks) {
|
for (const LaneTrack& track : tracks) {
|
||||||
if (track.trackGuid.empty()) continue;
|
if (track.trackGuid.empty()) continue;
|
||||||
|
|
||||||
// Collect the DISTINCT modes the track's managed-eligible items belong to, in
|
// SHOW-BOTH escape hatch: never force-split. A show-both track is visible in
|
||||||
|
// 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
|
// 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
|
// 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
|
// counted toward the multi-mode test and never reassigned (the managed-only
|
||||||
// invariant, upheld at the source of the decision).
|
// invariant, upheld at the source of the decision).
|
||||||
std::set<std::string> involvedModes;
|
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 — user's hand-managed lane
|
||||||
involvedModes.insert(item.modeId);
|
ownItemModes.insert(item.modeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single-mode (or empty) track: whole-track parking (D1) still separates the
|
// A track with NO managed-eligible own media never splits: there is nothing to
|
||||||
// stances. NO split, NO mint, NO assignment — this is the load-bearing
|
// confine (lane separation projects OWN items across modes). A folder derived-
|
||||||
// "don't lane-split single-mode tracks" rule.
|
// visible in many modes but carrying no own content stays whole-track visibility-
|
||||||
if (involvedModes.size() < 2) continue;
|
// only (D1 parent handling) — this guards the "carries its own media" clause.
|
||||||
|
if (ownItemModes.empty()) continue;
|
||||||
|
|
||||||
// Multi-mode track: transition to lane-split. One managed lane per involved
|
// The two visibility sources, OR'd:
|
||||||
// mode (durable key = laneNameForMode(mode)), owned by that mode.
|
// (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 std::size_t visibleModeCount =
|
||||||
|
visIt == visibleModesOf.end() ? 0 : visIt->second.size();
|
||||||
|
const bool multiMode = ownItemModes.size() >= 2 || visibleModeCount >= 2;
|
||||||
|
|
||||||
|
// Single-mode (visible in exactly one mode, own items single-mode): whole-track
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Lanes to mint = the UNION of the modes the own items belong to and the modes the
|
||||||
|
// track is visible in. A folder whose own item is Design-only but which is derived-
|
||||||
|
// visible in Arrange too mints BOTH a Design lane (holding the item) and an Arrange
|
||||||
|
// lane (reserving Arrange's stance slot), matching "each mode owns a fixed lane."
|
||||||
|
std::set<std::string> laneModes = ownItemModes;
|
||||||
|
if (visIt != visibleModesOf.end())
|
||||||
|
laneModes.insert(visIt->second.begin(), visIt->second.end());
|
||||||
|
|
||||||
|
// Transition to lane-split: one managed lane per involved 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>(involvedModes.size())});
|
track.trackGuid, static_cast<int>(laneModes.size())});
|
||||||
for (const std::string& mode : involvedModes) {
|
for (const std::string& mode : laneModes) {
|
||||||
plan.mints.push_back(
|
plan.mints.push_back(
|
||||||
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign EVERY managed-eligible item onto its mode's lane — including the
|
// Assign EVERY managed-eligible OWN item onto its tagged mode's lane — including
|
||||||
// pre-existing single-mode items, so a track that just gained a second mode
|
// the pre-existing single-mode items, so a folder carrying one own Design item
|
||||||
// retroactively lanes all of its content, not only the newly-added 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; // exempt — never reassigned
|
||||||
|
|||||||
+53
-19
@@ -522,16 +522,36 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
|||||||
// -- Lane minting decision (Phase D2 / Wave 3) -------------------------------
|
// -- Lane minting decision (Phase D2 / Wave 3) -------------------------------
|
||||||
//
|
//
|
||||||
// D1 parks a whole track when it holds content of only ONE mode. The moment a track
|
// 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
|
// is VISIBLE IN MORE THAN ONE MODE while carrying its OWN media, whole-track parking
|
||||||
// the stances separate (the track is visible in every mode its content belongs to),
|
// can no longer keep the stances separate (the track shows in every mode it is visible
|
||||||
// so the projection drops to the ITEM level: the track becomes a fixed-lane track,
|
// in, so its items leak across all of them), so the projection drops to the ITEM level:
|
||||||
// each involved mode gets its own MANAGED lane, and each item is assigned to its
|
// the track becomes a fixed-lane track, each involved mode gets its own MANAGED lane,
|
||||||
// mode's lane. A toggle then shows+plays only the active mode's 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.
|
// 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,
|
// The shell reads each track's items and their live mode+lane disposition, builds the
|
||||||
// and applies the resulting REAPER writes (I_FREEMODE / I_NUMFIXEDLANES / P_LANENAME /
|
// FolderTree (via the existing view_tree helper, exactly as the D1 shell does), calls
|
||||||
// I_FIXEDLANE) plus the ownership-index writes. The DECISION never lives in the shell.
|
// 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
|
// 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
|
// 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).
|
// * Ignore items on manual lanes entirely (exempt — the managed-only invariant).
|
||||||
// * Collect the DISTINCT modes the remaining (managed-eligible) items belong to.
|
// * A track splits iff it CARRIES OWN managed-eligible media AND is VISIBLE IN >= 2
|
||||||
// * If that set has < 2 modes, the track stays whole-track-parked (D1) — NO split,
|
// MODES. Visibility spans two sources, either of which qualifies:
|
||||||
// NO mint, NO assignment. This is the single-mode-track rule.
|
// (a) the track's own managed-eligible items span >= 2 modes (leaf carrying an
|
||||||
// * If it has >= 2 modes, the track transitions to lane-split: emit one TrackSplit
|
// Arrange take and a Design take), OR
|
||||||
// (laneCount == number of involved modes), one LaneMint per involved mode (durable
|
// (b) the track is derived-visible in >= 2 modes per visibleTracks (a content-
|
||||||
// key laneNameForMode(mode), owned by that mode), and one LaneAssign per managed-
|
// bearing folder whose descendant leaves span modes) — the missed case.
|
||||||
// eligible item onto its mode's lane — INCLUDING the pre-existing items, so a
|
// * A track visible in exactly ONE mode (single-mode leaf, single-mode folder) stays
|
||||||
// single-mode track that just gained a second mode retroactively lanes ALL its
|
// whole-track-parked (D1) — NO split. This is the single-mode-track rule.
|
||||||
// items, not only the newly-added one.
|
// * 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
|
// 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
|
// 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
|
// 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
|
// history (the shell only opens an Undo block when the plan is non-empty AND some
|
||||||
// write actually changes state — see the shell).
|
// 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`
|
// 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
|
// and wrapping to the first mode after the last (Arrange -> Design -> Arrange with
|
||||||
|
|||||||
@@ -1105,6 +1105,13 @@ static int splitLaneCount(const LaneMintPlan& p, const std::string& track) {
|
|||||||
return -1; // no split for this track
|
return -1; // no split for this track
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A plain LEAF track (not a folder) carrying its own items, with no tree derivation:
|
||||||
|
// an empty model + empty tree means visibleTracks contributes nothing, so the ONLY
|
||||||
|
// trigger is the track's own-item mode span — exactly the W3-A behavior. These helpers
|
||||||
|
// keep the W3-A leaf tests reading against a neutral model/tree.
|
||||||
|
static const ViewModeModel& bareModel() { static ViewModeModel m; return m; }
|
||||||
|
static const FolderTree& emptyTree() { static FolderTree t; return t; }
|
||||||
|
|
||||||
static void testLaneMintingSingleModeNoSplit() {
|
static void testLaneMintingSingleModeNoSplit() {
|
||||||
// A track whose items all belong to ONE mode is NOT lane-split — D1 whole-track
|
// A track whose items all belong to ONE mode is NOT lane-split — D1 whole-track
|
||||||
// parking still separates the stances. No split, no mint, no assignment.
|
// parking still separates the stances. No split, no mint, no assignment.
|
||||||
@@ -1114,12 +1121,12 @@ static void testLaneMintingSingleModeNoSplit() {
|
|||||||
LaneItem{"{i2}", kArrangeModeId, false},
|
LaneItem{"{i2}", kArrangeModeId, false},
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);
|
||||||
CHECK(plan.empty());
|
CHECK(plan.empty());
|
||||||
CHECK(splitLaneCount(plan, "{T}") == -1);
|
CHECK(splitLaneCount(plan, "{T}") == -1);
|
||||||
|
|
||||||
// An empty track (no items) is likewise never split.
|
// An empty track (no items) is likewise never split.
|
||||||
CHECK(planLaneMinting({LaneTrack{"{E}", {}}}).empty());
|
CHECK(planLaneMinting(bareModel(), emptyTree(), {LaneTrack{"{E}", {}}}).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testLaneMintingMultiModeMintsAndAssignsAll() {
|
static void testLaneMintingMultiModeMintsAndAssignsAll() {
|
||||||
@@ -1133,7 +1140,7 @@ static void testLaneMintingMultiModeMintsAndAssignsAll() {
|
|||||||
LaneItem{"{des1}", kDesignModeId, false}, // the newly-added 2nd-mode item
|
LaneItem{"{des1}", kDesignModeId, false}, // the newly-added 2nd-mode item
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);
|
||||||
CHECK(!plan.empty());
|
CHECK(!plan.empty());
|
||||||
|
|
||||||
// One split with two managed lanes (one per involved mode).
|
// One split with two managed lanes (one per involved mode).
|
||||||
@@ -1160,7 +1167,7 @@ static void testLaneMintingManualLaneExempt() {
|
|||||||
LaneItem{"{comp}", kDesignModeId, /*onManualLane=*/true}, // user's comp take
|
LaneItem{"{comp}", kDesignModeId, /*onManualLane=*/true}, // user's comp take
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);
|
||||||
|
|
||||||
// Split for the two managed modes; the manual item never appears in assigns.
|
// Split for the two managed modes; the manual item never appears in assigns.
|
||||||
CHECK(splitLaneCount(plan, "{T}") == 2);
|
CHECK(splitLaneCount(plan, "{T}") == 2);
|
||||||
@@ -1179,7 +1186,8 @@ static void testLaneMintingManualLaneExempt() {
|
|||||||
LaneItem{"{d}", kDesignModeId, /*onManualLane=*/true}, // only 2nd mode, exempt
|
LaneItem{"{d}", kDesignModeId, /*onManualLane=*/true}, // only 2nd mode, exempt
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
CHECK(planLaneMinting(t2).empty()); // managed-eligible content is single-mode ⇒ no split
|
// managed-eligible content is single-mode ⇒ no split (leaf, no tree derivation).
|
||||||
|
CHECK(planLaneMinting(bareModel(), emptyTree(), t2).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testLaneMintingThreeModesAndOwnershipKeys() {
|
static void testLaneMintingThreeModesAndOwnershipKeys() {
|
||||||
@@ -1197,7 +1205,8 @@ static void testLaneMintingThreeModesAndOwnershipKeys() {
|
|||||||
LaneItem{"{m}", "mixdown", false},
|
LaneItem{"{m}", "mixdown", false},
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
// Own items span three modes (leaf; empty tree ⇒ own-item-span is the sole trigger).
|
||||||
|
const LaneMintPlan plan = planLaneMinting(vm, FolderTree{}, tracks);
|
||||||
CHECK(splitLaneCount(plan, "{T}") == 3);
|
CHECK(splitLaneCount(plan, "{T}") == 3);
|
||||||
CHECK(plan.mints.size() == 3);
|
CHECK(plan.mints.size() == 3);
|
||||||
|
|
||||||
@@ -1221,6 +1230,153 @@ static void testLaneMintingThreeModesAndOwnershipKeys() {
|
|||||||
if (back) CHECK(back->lanes().size() == 3);
|
if (back) CHECK(back->lanes().size() == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Fix: content-bearing folder derived-visible in >1 mode splits its own media ----
|
||||||
|
//
|
||||||
|
// The exact failing case. A folder {F} has descendant leaves in BOTH modes ({LD} Design,
|
||||||
|
// {LA} Arrange) and carries ONE OWN item ({own}) tagged Design. W3-A's own-item-span test
|
||||||
|
// alone would NOT split {F} (its own content is single-mode Design), so the item leaked
|
||||||
|
// into every mode the folder was derived-visible in. The visibility-aware decision now
|
||||||
|
// mints managed lanes on {F} and lanes {own} onto the Design lane — so it hides+silences
|
||||||
|
// whenever Arrange is active. This is the load-bearing fix; assert it hard.
|
||||||
|
static void testLaneMintingFolderDerivedVisibleSplitsOwnMedia() {
|
||||||
|
ViewModeModel vm;
|
||||||
|
vm.membership().tag("{LD}", kDesignModeId); // a Design leaf under the folder
|
||||||
|
// {LA} left untagged ⇒ Arrange member; both stances thus live under {F}.
|
||||||
|
vm.membership().tag("{own}", kDesignModeId); // the folder's OWN dropped item (Design)
|
||||||
|
|
||||||
|
FolderTree tree;
|
||||||
|
tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
|
||||||
|
|
||||||
|
// Sanity: the folder really is derived-visible in BOTH modes (the precondition the
|
||||||
|
// W3-A trigger ignored). If this ever stops holding, the fix's premise is gone.
|
||||||
|
CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{F}") == 1);
|
||||||
|
CHECK(vm.visibleTracks(tree, kDesignModeId).count("{F}") == 1);
|
||||||
|
|
||||||
|
// The folder track {F} carries its own single Design item; its child leaves are the
|
||||||
|
// separate leaf tracks (not reported as items on {F}).
|
||||||
|
std::vector<LaneTrack> tracks{
|
||||||
|
LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}},
|
||||||
|
};
|
||||||
|
|
||||||
|
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
|
||||||
|
|
||||||
|
// {F} MUST split even though its own item is single-mode: it is visible in 2 modes.
|
||||||
|
CHECK(!plan.empty());
|
||||||
|
CHECK(splitLaneCount(plan, "{F}") == 2); // one lane per stance
|
||||||
|
CHECK(hasMint(plan, "{F}", kDesignModeId)); // Design lane (holds the item)
|
||||||
|
CHECK(hasMint(plan, "{F}", kArrangeModeId)); // Arrange lane (reserves the slot)
|
||||||
|
|
||||||
|
// The own item is confined to its tagged (Design) lane — the exact hide-in-Arrange fix.
|
||||||
|
CHECK(plan.assigns.size() == 1);
|
||||||
|
CHECK(hasAssign(plan, "{own}", "{F}", kDesignModeId));
|
||||||
|
for (const auto& a : plan.assigns)
|
||||||
|
CHECK(!(a.itemGuid == "{own}" && a.laneKey == laneNameForMode(kArrangeModeId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A folder carrying its OWN items that already span both modes → still split (the two
|
||||||
|
// triggers OR: own-item span AND derived visibility both point the same way here). Both
|
||||||
|
// own items separate to their tagged lanes.
|
||||||
|
static void testLaneMintingFolderOwnItemsSpanBothModes() {
|
||||||
|
ViewModeModel vm;
|
||||||
|
vm.membership().tag("{LD}", kDesignModeId);
|
||||||
|
vm.membership().tag("{d}", kDesignModeId);
|
||||||
|
// {a} untagged ⇒ Arrange.
|
||||||
|
|
||||||
|
FolderTree tree;
|
||||||
|
tree.nodes.push_back(FolderNode{"{F}", "", true});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false}); // untagged ⇒ Arrange
|
||||||
|
|
||||||
|
std::vector<LaneTrack> tracks{
|
||||||
|
LaneTrack{"{F}", {
|
||||||
|
LaneItem{"{a}", kArrangeModeId, false},
|
||||||
|
LaneItem{"{d}", kDesignModeId, false},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
|
||||||
|
CHECK(splitLaneCount(plan, "{F}") == 2);
|
||||||
|
CHECK(plan.assigns.size() == 2);
|
||||||
|
CHECK(hasAssign(plan, "{a}", "{F}", kArrangeModeId));
|
||||||
|
CHECK(hasAssign(plan, "{d}", "{F}", kDesignModeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHOW-BOTH escape hatch: a show-both track carrying its own items is visible in every
|
||||||
|
// mode ON PURPOSE and must NOT be force-split — its content stays cross-mode-visible.
|
||||||
|
// Even with own items that would otherwise span modes, the decision skips it entirely.
|
||||||
|
static void testLaneMintingShowBothNotForceSplit() {
|
||||||
|
ViewModeModel vm;
|
||||||
|
vm.membership().setShowBoth("{SB}", true);
|
||||||
|
|
||||||
|
// A show-both track whose OWN items even span two modes — the W3-A own-span trigger
|
||||||
|
// would fire, but show-both must override it (its items are meant to play everywhere).
|
||||||
|
std::vector<LaneTrack> tracks{
|
||||||
|
LaneTrack{"{SB}", {
|
||||||
|
LaneItem{"{a}", kArrangeModeId, false},
|
||||||
|
LaneItem{"{d}", kDesignModeId, false},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
const LaneMintPlan plan = planLaneMinting(vm, FolderTree{}, tracks);
|
||||||
|
CHECK(plan.empty()); // NOT split — the escape hatch holds
|
||||||
|
CHECK(splitLaneCount(plan, "{SB}") == -1);
|
||||||
|
|
||||||
|
// And a show-both FOLDER derived-visible in both modes carrying an own item: still not
|
||||||
|
// split. Visibility is the deliberate point of show-both.
|
||||||
|
ViewModeModel vm2;
|
||||||
|
vm2.membership().setShowBoth("{F}", true);
|
||||||
|
vm2.membership().tag("{LD}", kDesignModeId);
|
||||||
|
vm2.membership().tag("{own}", kDesignModeId);
|
||||||
|
FolderTree tree;
|
||||||
|
tree.nodes.push_back(FolderNode{"{F}", "", true});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
|
||||||
|
std::vector<LaneTrack> t2{LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}}};
|
||||||
|
CHECK(planLaneMinting(vm2, tree, t2).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single-mode LEAF visible in exactly one mode is still never split — the D1 whole-track
|
||||||
|
// parking case. A leaf under a folder, tagged Design, whose sibling is also Design: the
|
||||||
|
// leaf is visible in one mode only, carries its own Design item, and must NOT lane-split.
|
||||||
|
static void testLaneMintingSingleModeLeafVisibleOnceNoSplit() {
|
||||||
|
ViewModeModel vm;
|
||||||
|
vm.membership().tag("{L}", kDesignModeId);
|
||||||
|
vm.membership().tag("{own}", kDesignModeId);
|
||||||
|
|
||||||
|
FolderTree tree;
|
||||||
|
tree.nodes.push_back(FolderNode{"{F}", "", true});
|
||||||
|
tree.nodes.push_back(FolderNode{"{L}", "{F}", false}); // the leaf under test
|
||||||
|
|
||||||
|
// The leaf {L} is visible only in Design (its one tagged mode).
|
||||||
|
CHECK(vm.visibleTracks(tree, kDesignModeId).count("{L}") == 1);
|
||||||
|
CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{L}") == 0);
|
||||||
|
|
||||||
|
std::vector<LaneTrack> tracks{
|
||||||
|
LaneTrack{"{L}", {LaneItem{"{own}", kDesignModeId, false}}},
|
||||||
|
};
|
||||||
|
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
|
||||||
|
CHECK(plan.empty()); // single-mode, visible once ⇒ D1 whole-track parking, no split
|
||||||
|
}
|
||||||
|
|
||||||
|
// A content-EMPTY folder derived-visible in many modes carries NO own media, so there is
|
||||||
|
// nothing to lane-separate: it stays visibility-only (D1 parent handling), never split.
|
||||||
|
static void testLaneMintingEmptyFolderNotSplit() {
|
||||||
|
ViewModeModel vm;
|
||||||
|
vm.membership().tag("{LD}", kDesignModeId);
|
||||||
|
// {LA} untagged ⇒ Arrange; folder derived-visible in both modes but holds no own item.
|
||||||
|
|
||||||
|
FolderTree tree;
|
||||||
|
tree.nodes.push_back(FolderNode{"{F}", "", true});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
|
||||||
|
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
|
||||||
|
|
||||||
|
CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{F}") == 1);
|
||||||
|
CHECK(vm.visibleTracks(tree, kDesignModeId).count("{F}") == 1);
|
||||||
|
|
||||||
|
std::vector<LaneTrack> tracks{LaneTrack{"{F}", {}}}; // no own media
|
||||||
|
CHECK(planLaneMinting(vm, tree, tracks).empty());
|
||||||
|
}
|
||||||
|
|
||||||
// -- D2.6 JSON round-trip with lane index + membership -----------------------
|
// -- D2.6 JSON round-trip with lane index + membership -----------------------
|
||||||
|
|
||||||
static void testLaneJsonRoundTrip() {
|
static void testLaneJsonRoundTrip() {
|
||||||
@@ -1313,6 +1469,11 @@ int main() {
|
|||||||
testLaneMintingMultiModeMintsAndAssignsAll();
|
testLaneMintingMultiModeMintsAndAssignsAll();
|
||||||
testLaneMintingManualLaneExempt();
|
testLaneMintingManualLaneExempt();
|
||||||
testLaneMintingThreeModesAndOwnershipKeys();
|
testLaneMintingThreeModesAndOwnershipKeys();
|
||||||
|
testLaneMintingFolderDerivedVisibleSplitsOwnMedia();
|
||||||
|
testLaneMintingFolderOwnItemsSpanBothModes();
|
||||||
|
testLaneMintingShowBothNotForceSplit();
|
||||||
|
testLaneMintingSingleModeLeafVisibleOnceNoSplit();
|
||||||
|
testLaneMintingEmptyFolderNotSplit();
|
||||||
testLaneJsonRoundTrip();
|
testLaneJsonRoundTrip();
|
||||||
testLaneMalformedJson();
|
testLaneMalformedJson();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user