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:
+59
-15
@@ -162,41 +162,85 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
// 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;
|
||||
|
||||
// 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) {
|
||||
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
|
||||
// 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> involvedModes;
|
||||
std::set<std::string> ownItemModes;
|
||||
for (const LaneItem& item : track.items) {
|
||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||
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
|
||||
// stances. NO split, NO mint, NO assignment — this is the load-bearing
|
||||
// "don't lane-split single-mode tracks" rule.
|
||||
if (involvedModes.size() < 2) continue;
|
||||
// A track with NO managed-eligible own media never splits: there is nothing to
|
||||
// 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;
|
||||
|
||||
// Multi-mode track: transition to lane-split. One managed lane per involved
|
||||
// mode (durable key = laneNameForMode(mode)), owned by that mode.
|
||||
// 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 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{
|
||||
track.trackGuid, static_cast<int>(involvedModes.size())});
|
||||
for (const std::string& mode : involvedModes) {
|
||||
track.trackGuid, static_cast<int>(laneModes.size())});
|
||||
for (const std::string& mode : laneModes) {
|
||||
plan.mints.push_back(
|
||||
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
||||
}
|
||||
|
||||
// Assign EVERY managed-eligible item onto its mode's lane — including the
|
||||
// pre-existing single-mode items, so a track that just gained a second mode
|
||||
// retroactively lanes all of its content, not only the newly-added item.
|
||||
// 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) {
|
||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||
if (item.onManualLane) continue; // exempt — never reassigned
|
||||
|
||||
Reference in New Issue
Block a user