Merge: make tool-managed lane splits visually transparent (collapse display + lazy-mint)

This commit is contained in:
2026-07-25 01:22:15 -04:00
4 changed files with 131 additions and 23 deletions
+46 -2
View File
@@ -53,6 +53,39 @@ namespace {
// positioning, 2=fixed lanes. // positioning, 2=fixed lanes.
constexpr int kFreeModeFixedLanes = 2; constexpr int kFreeModeFixedLanes = 2;
// C_LANESCOLLAPSED display value (char*). SDK: 1=lanes collapsed,
// 2=track displays as non-fixed-lanes but hidden lanes exist. Value 2 is the lever that
// makes a tool-split track read like a NORMAL single-lane track showing only the playing
// lane — the inactive/silenced managed lanes are present but not drawn as separate rows.
constexpr int kLanesDisplayAsNormal = 2;
// C_LANESETTINGS bit (char* bitmask). SDK: &32=hide lane buttons. We OR this in (never
// clobber the whole mask) to strip the per-lane button chrome from a tool-split track, so
// it reads as an ordinary track. We deliberately do NOT set &1 (auto-remove empty lanes at
// bottom): a managed lane whose item is later deleted would be silently removed out from
// under the ownership index. The lazy-mint decision already avoids ever minting an empty
// lane, so &1 buys nothing and risks a reconcile hazard.
constexpr int kLaneSettingsHideButtons = 32;
// Drives a TOOL-SPLIT track's display transparent: C_LANESCOLLAPSED=2 (render like a normal
// single-lane track showing only the playing lane) + OR C_LANESETTINGS &32 (hide lane
// buttons). Both are char* params driven through the double API, same convention as
// C_LANEPLAYS:N. C_LANESETTINGS is read-modify-write so any pre-existing bit is preserved.
//
// MANAGED-VS-MANUAL BOUNDARY (load-bearing): these are TRACK-LEVEL settings that affect the
// whole track including a user's own manual comp lanes. Every caller gates this on the
// tool-driven transition INTO fixed lanes (freeMode != 2 before the flip), so a track the
// user already had in fixed-lane mode never reaches it and the user's comp-lane display
// prefs are never stomped. Idempotent: a re-run finds the track already at I_FREEMODE==2,
// the transition branch is skipped, and these writes do not fire again.
void applyTransparentLaneDisplay(MediaTrack* tr) {
SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED",
static_cast<double>(kLanesDisplayAsNormal));
const int settings = static_cast<int>(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS"));
SetMediaTrackInfo_Value(tr, "C_LANESETTINGS",
static_cast<double>(settings | kLaneSettingsHideButtons));
}
// The parmname for each planner Flag. All four are documented bool*/int* track // The parmname for each planner Flag. All four are documented bool*/int* track
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API. // info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
const char* flagParm(Flag f) { const char* flagParm(Flag f) {
@@ -224,11 +257,16 @@ bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handle
// Ensure fixed-lane mode is on before driving lane play state. A track carrying // Ensure fixed-lane mode is on before driving lane play state. A track carrying
// a managed lane must be in I_FREEMODE=2; set it only if not already, and flag // a managed lane must be in I_FREEMODE=2; set it only if not already, and flag
// that a timeline refresh is owed. // that a timeline refresh is owed. Every track reaching this loop is already in the
// managed-lane ownership index (planToggle only emits ops for managed lanes), so a
// track here is one the TOOL split — a re-assert of fixed-lane mode is a tool-driven
// (re)split and must carry the same transparent display, mirroring applyMintPlan's
// transition branch. It is never a user's untouched manual-fixed-lane track.
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
if (freeMode != kFreeModeFixedLanes) { if (freeMode != kFreeModeFixedLanes) {
SetMediaTrackInfo_Value(tr, "I_FREEMODE", SetMediaTrackInfo_Value(tr, "I_FREEMODE",
static_cast<double>(kFreeModeFixedLanes)); static_cast<double>(kFreeModeFixedLanes));
applyTransparentLaneDisplay(tr); // tool-managed track ⇒ read like a normal track
touchedFreeMode = true; touchedFreeMode = true;
} }
@@ -371,10 +409,16 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
MediaTrack* tr = resolve(handleByGuid, split.trackGuid); MediaTrack* tr = resolve(handleByGuid, split.trackGuid);
if (!tr) continue; // stale GUID — prune if (!tr) continue; // stale GUID — prune
// Enable fixed-lane mode if not already (SDK: UpdateTimeline() owed after). // Enable fixed-lane mode if not already (SDK: UpdateTimeline() owed after). The
// pre-write freeMode read is ALSO the managed-vs-manual boundary signal: a track that
// was NOT in fixed-lane mode here is one the TOOL is splitting now, so the tool owns
// its lane display and drives it transparent. A track already at I_FREEMODE==2 (user
// had fixed lanes, or a prior tool run) skips this branch — its C_LANESCOLLAPSED /
// C_LANESETTINGS are left exactly as the user set them.
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
if (freeMode != kFreeModeFixedLanes) { if (freeMode != kFreeModeFixedLanes) {
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes)); SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
applyTransparentLaneDisplay(tr); // tool-split track ⇒ read like a normal track
changed = true; changed = true;
} }
+12 -8
View File
@@ -220,15 +220,19 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
// this is the load-bearing "don't lane-split single-mode tracks" rule. // this is the load-bearing "don't lane-split single-mode tracks" rule.
if (!multiMode) continue; if (!multiMode) continue;
// Lanes to mint = the UNION of the modes the own items belong to and the modes the // Lazy-mint: lanes to mint = ONLY the modes the track's OWN items actually occupy —
// track is visible in. A folder whose own item is Design-only but which is derived- // never an empty reserved lane for a mode the track is merely derived-visible in.
// visible in Arrange too mints BOTH a Design lane (holding the item) and an Arrange // A folder whose own item is Design-only but which is derived-visible in Arrange too
// lane (reserving Arrange's stance slot), matching "each mode owns a fixed lane." // mints a Design lane ONLY (holding the item); it mints NO Arrange lane. Confinement
std::set<std::string> laneModes = ownItemModes; // still holds: with only a Design lane present, toggling to Arrange drives that lane's
if (visIt != visibleModesOf.end()) // C_LANEPLAYS to 0 (it hides+silences) and no lane plays, so the track reads as an
laneModes.insert(visIt->second.begin(), visIt->second.end()); // empty normal track — the Design item does not leak. The Arrange lane is minted on
// demand the moment an Arrange item first lands (a later mint tick sees ownItemModes
// gain Arrange). The visibility trigger above still decides WHETHER to split; it no
// longer inflates WHICH lanes are minted.
const std::set<std::string>& laneModes = ownItemModes;
// Transition to lane-split: one managed lane per involved mode (durable key = // Transition to lane-split: one managed lane per own-content mode (durable key =
// laneNameForMode(mode)), owned by that mode. // laneNameForMode(mode)), owned by that mode.
plan.splits.push_back(LaneMintPlan::TrackSplit{ plan.splits.push_back(LaneMintPlan::TrackSplit{
track.trackGuid, static_cast<int>(laneModes.size())}); track.trackGuid, static_cast<int>(laneModes.size())});
+13 -8
View File
@@ -641,14 +641,19 @@ struct LaneMintPlan {
// * A track visible in exactly ONE mode (single-mode leaf, single-mode folder) stays // * 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. // 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 // * 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 the track's OWN items occupy, and one LaneAssign per managed-eligible OWN item
// mode's lane — INCLUDING pre-existing items, so a folder carrying one own Design item // onto ITS tagged mode's lane — INCLUDING pre-existing items, so a folder carrying one
// while derived-visible in Arrange too still lanes that item to the Design lane (it // own Design item while derived-visible in Arrange too still lanes that item to the
// then hides+silences whenever Arrange is active). The lanes minted are the union of: // Design lane (it then hides+silences whenever Arrange is active).
// the modes the track's own items belong to, PLUS the modes the track is visible in // * LAZY-MINT: lanes are minted ONLY for modes the track's own items actually occupy
// so a folder whose own item is Design-only but which is derived-visible in Arrange // never an empty reserved lane for a mode the track is merely derived-visible in. So a
// mints BOTH a Design lane (holding the item) and an Arrange lane (empty, reserving // folder whose own item is Design-only but which is derived-visible in Arrange mints a
// the Arrange stance's slot), matching "each mode owns a fixed lane." // Design lane ONLY (holding the item), NOT an empty Arrange lane. Confinement still
// holds: with only a Design lane present, toggling to Arrange drives that lane's
// C_LANEPLAYS to 0 (hide+silence) and no lane plays, so the track reads as an empty
// normal track and the Design item does not leak. The Arrange lane is minted on demand
// when an Arrange item first lands. The derived-visibility trigger still decides WHETHER
// to split; it no longer inflates WHICH lanes are minted.
// //
// 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
+60 -5
View File
@@ -1235,9 +1235,11 @@ static void testLaneMintingThreeModesAndOwnershipKeys() {
// The exact failing case. A folder {F} has descendant leaves in BOTH modes ({LD} Design, // 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 // {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 // 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 // into every mode the folder was derived-visible in. The visibility-aware decision splits
// mints managed lanes on {F} and lanes {own} onto the Design lane — so it hides+silences // {F} and lanes {own} onto the Design lane — so it hides+silences whenever Arrange is
// whenever Arrange is active. This is the load-bearing fix; assert it hard. // active. LAZY-MINT: {F} mints ONLY the Design lane (holding the item), NOT an empty
// reserved Arrange lane — confinement holds via C_LANEPLAYS=0 on the lone Design lane when
// Arrange is active. This is the load-bearing fix; assert it hard.
static void testLaneMintingFolderDerivedVisibleSplitsOwnMedia() { static void testLaneMintingFolderDerivedVisibleSplitsOwnMedia() {
ViewModeModel vm; ViewModeModel vm;
vm.membership().tag("{LD}", kDesignModeId); // a Design leaf under the folder vm.membership().tag("{LD}", kDesignModeId); // a Design leaf under the folder
@@ -1264,17 +1266,69 @@ static void testLaneMintingFolderDerivedVisibleSplitsOwnMedia() {
// {F} MUST split even though its own item is single-mode: it is visible in 2 modes. // {F} MUST split even though its own item is single-mode: it is visible in 2 modes.
CHECK(!plan.empty()); CHECK(!plan.empty());
CHECK(splitLaneCount(plan, "{F}") == 2); // one lane per stance
// LAZY-MINT: ONE lane only — the Design lane that holds the item. No empty reserved
// Arrange lane is minted, even though {F} is derived-visible in Arrange. The Arrange
// lane appears on demand when an Arrange item first lands on {F}.
CHECK(splitLaneCount(plan, "{F}") == 1); // Design lane only — no reserved lane
CHECK(plan.mints.size() == 1);
CHECK(hasMint(plan, "{F}", kDesignModeId)); // Design lane (holds the item) CHECK(hasMint(plan, "{F}", kDesignModeId)); // Design lane (holds the item)
CHECK(hasMint(plan, "{F}", kArrangeModeId)); // Arrange lane (reserves the slot) CHECK(!hasMint(plan, "{F}", kArrangeModeId)); // NO empty reserved Arrange lane
// The own item is confined to its tagged (Design) lane — the exact hide-in-Arrange fix. // The own item is confined to its tagged (Design) lane — the exact hide-in-Arrange fix.
// With only the Design lane present, toggling to Arrange sets its C_LANEPLAYS to 0, so
// the item hides+silences and the track reads as an empty normal track (no leak).
CHECK(plan.assigns.size() == 1); CHECK(plan.assigns.size() == 1);
CHECK(hasAssign(plan, "{own}", "{F}", kDesignModeId)); CHECK(hasAssign(plan, "{own}", "{F}", kDesignModeId));
for (const auto& a : plan.assigns) for (const auto& a : plan.assigns)
CHECK(!(a.itemGuid == "{own}" && a.laneKey == laneNameForMode(kArrangeModeId))); CHECK(!(a.itemGuid == "{own}" && a.laneKey == laneNameForMode(kArrangeModeId)));
} }
// LAZY-MINT confinement proof. Same single-own-mode / dual-visibility folder, but instead
// of asserting the mint COUNT we prove the FUNCTIONAL confinement the lazy split preserves:
// apply the minted lane's ownership to a live model, then drive the toggle planner and show
// the lone Design lane SILENCES when Arrange is active (C_LANEPLAYS = 0). That is the whole
// point — a single managed lane still hides its item in every other mode, so removing the
// empty reserved Arrange lane costs nothing functionally. Fails if the split ever leaves the
// Design item audible in Arrange (the leak the D2 fix closed) or mints a spurious lane.
static void testLaneMintingLazySingleLaneStillConfines() {
ViewModeModel vm;
vm.membership().tag("{LD}", kDesignModeId); // Design leaf ⇒ folder visible in Design
// {LA} untagged ⇒ Arrange member ⇒ folder ALSO derived-visible in Arrange.
vm.membership().tag("{own}", kDesignModeId); // the folder's one own 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});
std::vector<LaneTrack> tracks{
LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}},
};
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
// Exactly one lane minted (the Design lane) — no empty reserved Arrange lane.
CHECK(plan.mints.size() == 1);
CHECK(hasMint(plan, "{F}", kDesignModeId));
// Apply the mint's ownership exactly as the shell does, then drive the toggle planner.
for (const auto& m : plan.mints)
CHECK(vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId));
CHECK(vm.lanes().size() == 1); // one managed lane on {F}, not two
const std::string designLane = laneNameForMode(kDesignModeId);
// Active = Design: the lone Design lane PLAYS (item visible+audible in its own mode).
const auto design = vm.planToggle(tree, kDesignModeId);
CHECK(lanePlaysFor(design, "{F}", designLane) == kLanePlaysExclusive);
// Active = Arrange: the lone Design lane SILENCES — with no lane playing, the track
// reads as an empty normal track and the Design item does NOT leak. This is the
// confinement guarantee that lets us drop the reserved Arrange lane.
const auto arrange = vm.planToggle(tree, kArrangeModeId);
CHECK(lanePlaysFor(arrange, "{F}", designLane) == kLaneSilent);
}
// A folder carrying its OWN items that already span both modes → still split (the two // 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 // triggers OR: own-item span AND derived visibility both point the same way here). Both
// own items separate to their tagged lanes. // own items separate to their tagged lanes.
@@ -1470,6 +1524,7 @@ int main() {
testLaneMintingManualLaneExempt(); testLaneMintingManualLaneExempt();
testLaneMintingThreeModesAndOwnershipKeys(); testLaneMintingThreeModesAndOwnershipKeys();
testLaneMintingFolderDerivedVisibleSplitsOwnMedia(); testLaneMintingFolderDerivedVisibleSplitsOwnMedia();
testLaneMintingLazySingleLaneStillConfines();
testLaneMintingFolderOwnItemsSpanBothModes(); testLaneMintingFolderOwnItemsSpanBothModes();
testLaneMintingShowBothNotForceSplit(); testLaneMintingShowBothNotForceSplit();
testLaneMintingSingleModeLeafVisibleOnceNoSplit(); testLaneMintingSingleModeLeafVisibleOnceNoSplit();