From 314cd7bf36f0931227071ad4f4984106993ba210 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Fri, 24 Jul 2026 05:44:19 -0400 Subject: [PATCH] Make tool-managed lane splits visually transparent: collapse display + lazy-mint Drive C_LANESCOLLAPSED=2 and hide lane buttons only on tracks the tool itself splits (gated on the pre-write I_FREEMODE read, so user comp-lane display prefs are never stomped). Lazy-mint managed lanes only for modes with own content, never an empty reserved lane; confinement holds via C_LANEPLAYS on the lone lane. --- src/view.cpp | 48 +++++++++++++++++++++++-- src/view_mode_model.cpp | 20 ++++++----- src/view_mode_model.h | 21 ++++++----- tests/test_view_mode_model.cpp | 65 +++++++++++++++++++++++++++++++--- 4 files changed, 131 insertions(+), 23 deletions(-) diff --git a/src/view.cpp b/src/view.cpp index fe7ca33..501e2d9 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -53,6 +53,39 @@ namespace { // positioning, 2=fixed lanes. 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(kLanesDisplayAsNormal)); + const int settings = static_cast(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS")); + SetMediaTrackInfo_Value(tr, "C_LANESETTINGS", + static_cast(settings | kLaneSettingsHideButtons)); +} + // The parmname for each planner Flag. All four are documented bool*/int* track // info params driven through the double-valued Get/SetMediaTrackInfo_Value API. const char* flagParm(Flag f) { @@ -224,11 +257,16 @@ bool applyLaneOps(const std::vector>& handle // 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 - // 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(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) { SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast(kFreeModeFixedLanes)); + applyTransparentLaneDisplay(tr); // tool-managed track ⇒ read like a normal track touchedFreeMode = true; } @@ -371,10 +409,16 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan, MediaTrack* tr = resolve(handleByGuid, split.trackGuid); 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(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) { SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast(kFreeModeFixedLanes)); + applyTransparentLaneDisplay(tr); // tool-split track ⇒ read like a normal track changed = true; } diff --git a/src/view_mode_model.cpp b/src/view_mode_model.cpp index 7052f57..56c361d 100644 --- a/src/view_mode_model.cpp +++ b/src/view_mode_model.cpp @@ -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. 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 laneModes = ownItemModes; - if (visIt != visibleModesOf.end()) - laneModes.insert(visIt->second.begin(), visIt->second.end()); + // Lazy-mint: lanes to mint = ONLY the modes the track's OWN items actually occupy — + // never an empty reserved lane for a mode the track is merely derived-visible in. + // A folder whose own item is Design-only but which is derived-visible in Arrange too + // mints a Design lane ONLY (holding the item); it mints NO Arrange lane. Confinement + // still holds: with only a Design lane present, toggling to Arrange drives that lane's + // C_LANEPLAYS to 0 (it hides+silences) and no lane plays, so the track reads as an + // 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& 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. plan.splits.push_back(LaneMintPlan::TrackSplit{ track.trackGuid, static_cast(laneModes.size())}); diff --git a/src/view_mode_model.h b/src/view_mode_model.h index deffc33..b1daaaf 100644 --- a/src/view_mode_model.h +++ b/src/view_mode_model.h @@ -641,14 +641,19 @@ struct LaneMintPlan { // * 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." +// mode the track's OWN items occupy, 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). +// * LAZY-MINT: lanes are minted ONLY for modes the track's own items actually occupy — +// never an empty reserved lane for a mode the track is merely derived-visible in. So a +// folder whose own item is Design-only but which is derived-visible in Arrange mints a +// 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 // resolves to a mode). The function mutates nothing — it returns a plan the shell diff --git a/tests/test_view_mode_model.cpp b/tests/test_view_mode_model.cpp index 5e3f5f5..b5e39c3 100644 --- a/tests/test_view_mode_model.cpp +++ b/tests/test_view_mode_model.cpp @@ -1235,9 +1235,11 @@ static void testLaneMintingThreeModesAndOwnershipKeys() { // 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. +// into every mode the folder was derived-visible in. The visibility-aware decision splits +// {F} and lanes {own} onto the Design lane — so it hides+silences whenever Arrange is +// 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() { ViewModeModel vm; 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. 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}", 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. + // 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(hasAssign(plan, "{own}", "{F}", kDesignModeId)); for (const auto& a : plan.assigns) 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 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 // triggers OR: own-item span AND derived visibility both point the same way here). Both // own items separate to their tagged lanes. @@ -1470,6 +1524,7 @@ int main() { testLaneMintingManualLaneExempt(); testLaneMintingThreeModesAndOwnershipKeys(); testLaneMintingFolderDerivedVisibleSplitsOwnMedia(); + testLaneMintingLazySingleLaneStillConfines(); testLaneMintingFolderOwnItemsSpanBothModes(); testLaneMintingShowBothNotForceSplit(); testLaneMintingSingleModeLeafVisibleOnceNoSplit();