fix(design-view): adopt pre-existing track mode on drop to prevent lane strand
A bank-panel capture dropped onto a track with pre-existing content was auto-tagged into the active mode, forcing a multi-mode lane split whose toggle silenced the pre-existing items. New items now adopt the single mode of their track's prior content; deliberate splits stay on the explicit item-move path.
This commit is contained in:
+53
-4
@@ -1750,12 +1750,16 @@ bool isFixedLaneTrack(MediaTrack* tr) {
|
||||
// Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set,
|
||||
// baseline input) and, for each item, records whether it sits on a manual lane so a
|
||||
// newly-detected item can be exempted from auto-tag without a second project walk.
|
||||
// `trackItemGuids` additionally maps each track GUID to the item GUIDs it carries, so a
|
||||
// newly-detected item's PRE-EXISTING siblings can be resolved (the adoption / strand
|
||||
// guard) without a second project walk.
|
||||
//
|
||||
// Manual-lane classification uses the single pure predicate isOnManualLane(isFixedLaneTrack,
|
||||
// laneName) from lane_keys — the same predicate the apply path consults — so the exemption
|
||||
// rule is defined in exactly one place and is unit-tested there.
|
||||
void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
std::map<std::string, bool>& itemOnManualLane) {
|
||||
std::map<std::string, bool>& itemOnManualLane,
|
||||
std::map<std::string, std::vector<std::string>>& trackItemGuids) {
|
||||
const int trackCount = CountTracks(proj);
|
||||
for (int t = 0; t < trackCount; ++t) {
|
||||
MediaTrack* tr = GetTrack(proj, t);
|
||||
@@ -1767,6 +1771,7 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
// track-level attribute and is the same for every item on the track.
|
||||
const bool fixedLane = isFixedLaneTrack(tr);
|
||||
|
||||
std::vector<std::string>& itemsOnTrack = trackItemGuids[tg];
|
||||
const int itemCount = CountTrackMediaItems(tr);
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||
@@ -1779,6 +1784,7 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
// false immediately for non-fixed-lane tracks regardless of name).
|
||||
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
||||
itemOnManualLane[ig] = isOnManualLane(fixedLane, ln);
|
||||
itemsOnTrack.push_back(ig);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1818,11 +1824,52 @@ bool detectNewContent() {
|
||||
|
||||
std::set<std::string> live;
|
||||
std::map<std::string, bool> itemOnManualLane;
|
||||
enumerateLiveGuids(proj, live, itemOnManualLane);
|
||||
std::map<std::string, std::vector<std::string>> trackItemGuids;
|
||||
enumerateLiveGuids(proj, live, itemOnManualLane, trackItemGuids);
|
||||
|
||||
const std::vector<std::string> added = g_panel.contentBaseline.observe(live);
|
||||
if (added.empty()) return false; // first poll after open, or nothing new this tick
|
||||
|
||||
ViewModeModel& model = g_panel.session->view();
|
||||
|
||||
// Which of `added` are items (the manual-lane map keys every item; track GUIDs never
|
||||
// appear there). Used below to exclude sibling new items from a track's PRE-EXISTING
|
||||
// mode set — a drop plus its own new siblings must not count each other as prior.
|
||||
const std::set<std::string> newItemGuids = [&] {
|
||||
std::set<std::string> s;
|
||||
for (const std::string& g : added)
|
||||
if (itemOnManualLane.count(g)) s.insert(g);
|
||||
return s;
|
||||
}();
|
||||
|
||||
// Item guid -> its track guid (reverse of trackItemGuids), so a new item's siblings
|
||||
// are found in one lookup.
|
||||
std::map<std::string, std::string> trackOfItem;
|
||||
for (const auto& [trackGuid, items] : trackItemGuids)
|
||||
for (const std::string& ig : items) trackOfItem[ig] = trackGuid;
|
||||
|
||||
// The distinct modes the PRE-EXISTING (not-new-this-tick) MANAGED-ELIGIBLE items on
|
||||
// `trackGuid` resolve to. Untagged siblings resolve to Arrange (leafBelongsToMode's
|
||||
// default); new siblings are excluded; manual-lane siblings are EXEMPT — exactly as
|
||||
// planLaneMinting ignores them when computing a track's own-item mode span, so the
|
||||
// adoption guard's view of the track matches the split decision's. Drives the adoption
|
||||
// / strand guard in autoTagNewContent.
|
||||
const auto preExistingTrackModes =
|
||||
[&](const std::string& trackGuid) -> std::set<std::string> {
|
||||
std::set<std::string> modes;
|
||||
auto it = trackItemGuids.find(trackGuid);
|
||||
if (it == trackItemGuids.end()) return modes;
|
||||
for (const std::string& sib : it->second) {
|
||||
if (newItemGuids.count(sib)) continue; // a sibling added THIS tick — not prior
|
||||
auto ml = itemOnManualLane.find(sib);
|
||||
if (ml != itemOnManualLane.end() && ml->second) continue; // manual lane — exempt
|
||||
const std::set<std::string> m = model.membership().modesOf(sib);
|
||||
if (m.empty()) modes.insert(kArrangeModeId); // untagged ⇒ Arrange default
|
||||
else modes.insert(m.begin(), m.end());
|
||||
}
|
||||
return modes;
|
||||
};
|
||||
|
||||
// Split the new GUIDs into tracks vs items so the pure decision can apply the
|
||||
// manual-lane exemption to items only. A GUID present in the item-lane map is an
|
||||
// item; otherwise it is a track (track GUIDs never appear in that map).
|
||||
@@ -1833,11 +1880,13 @@ bool detectNewContent() {
|
||||
if (it == itemOnManualLane.end()) {
|
||||
newTracks.push_back(g); // a track GUID
|
||||
} else {
|
||||
newItems.push_back(NewItem{g, it->second}); // an item; carries its exemption
|
||||
NewItem ni{g, it->second, {}};
|
||||
auto tk = trackOfItem.find(g);
|
||||
if (tk != trackOfItem.end()) ni.trackModes = preExistingTrackModes(tk->second);
|
||||
newItems.push_back(std::move(ni)); // an item; carries exemption + track modes
|
||||
}
|
||||
}
|
||||
|
||||
ViewModeModel& model = g_panel.session->view();
|
||||
const std::vector<AutoTag> tags =
|
||||
autoTagNewContent(newTracks, newItems, model.activeModeId());
|
||||
for (const AutoTag& tag : tags)
|
||||
|
||||
+10
-1
@@ -153,7 +153,16 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
for (const auto& item : newItems) {
|
||||
if (item.guid.empty()) continue;
|
||||
if (item.onManualLane) continue; // manual-lane content is off-limits to auto-tag
|
||||
tags.push_back(AutoTag{item.guid, activeMode});
|
||||
|
||||
// ADOPTION (strand guard): a new item on a track whose PRE-EXISTING content
|
||||
// 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 =
|
||||
item.trackModes.size() == 1 ? *item.trackModes.begin() : activeMode;
|
||||
tags.push_back(AutoTag{item.guid, target});
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
+30
-5
@@ -493,11 +493,35 @@ TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
|
||||
// such an item `onManualLane = true` (it knows the item's lane and consults the
|
||||
// ownership index); the decision then emits NO tag for it. New tracks and new items on
|
||||
// managed/no lane follow the active-mode rule.
|
||||
//
|
||||
// -- Pre-existing-content adoption (strand fix) -------------------------------
|
||||
//
|
||||
// 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.
|
||||
// One new item the shell detected this poll. Its lane disposition decides exemption; its
|
||||
// track's pre-existing content modes decide adoption (see above).
|
||||
struct NewItem {
|
||||
std::string guid;
|
||||
bool onManualLane = false; // true ⇒ EXEMPT from auto-tag (hand-managed lane)
|
||||
// The distinct modes the PRE-EXISTING (not-new-this-tick) managed-eligible content on
|
||||
// this item's track resolves to. Empty ⇒ the item's track carried no prior content, so
|
||||
// the item takes the active mode. Exactly one ⇒ ADOPT that mode (the strand guard).
|
||||
// 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;
|
||||
};
|
||||
|
||||
// One membership write the auto-tag decision produced: tag `guid` into `modeId`. The
|
||||
@@ -511,10 +535,11 @@ struct AutoTag {
|
||||
|
||||
// The pure auto-tag decision: given the new track GUIDs and new items detected this
|
||||
// poll plus the active mode, produce the membership writes. Every new track is tagged
|
||||
// to `activeMode`; every new item is tagged to `activeMode` UNLESS it landed on a
|
||||
// manual lane (exempt). 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.
|
||||
// to `activeMode`. Every new item is tagged UNLESS it landed on a manual lane (exempt);
|
||||
// its target mode is the single mode of its track's pre-existing content (adoption — the
|
||||
// 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,
|
||||
const std::vector<NewItem>& newItems,
|
||||
const std::string& activeMode);
|
||||
|
||||
Reference in New Issue
Block a user