fdb9e38ad6
Add gap-preserving per-bank SlotMap (reorder + Alt-replace mutators, JSON round-trip, insertion-order migration) to bank_book; stamp captureTimeSig on Sample; pure card_meta formatters + card_drag gesture/slot module; card metadata overlay + purple selection border in the panel. Shell drop/cursor wiring deferred. Fixes: removeSample syncs SlotMap; card_drag gap-probe coordinate.
458 lines
26 KiB
C++
458 lines
26 KiB
C++
#pragma once
|
|
// bank_book — the pure core of the multi-bank phase (Phase B), deliberately free
|
|
// of any REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the
|
|
// third instance of the same "pure registry + JSON round-trip, unit-tested outside
|
|
// the DAW" pattern as bank_model and view_mode_model.
|
|
//
|
|
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
|
// vendor/ includes. Standard library only.
|
|
//
|
|
// -- What it is --------------------------------------------------------------
|
|
//
|
|
// An ordered registry of banks. Each bank = { stable id, display name, ordinal,
|
|
// BankIndex }. The book WRAPS N BankIndex instances — bank_model / BankIndex are
|
|
// UNTOUCHED (additive: no bankId on Sample). Movement of samples between banks is
|
|
// index-only (remove from source's BankIndex, add to destination's); files never
|
|
// relocate — banks are logical groupings over one shared file pool.
|
|
//
|
|
// -- The pool (privileged, not special-cased) --------------------------------
|
|
//
|
|
// Structurally the pool is bank-zero — one Bank among many, seeded on construction
|
|
// with a fixed id (kPoolBankId) and fixed display name (kPoolBankName), ordinal 0.
|
|
// Semantically it is privileged, and the privileges are enforced HERE in the pure
|
|
// rules layer (CONTEXT.md §Multi-bank guardrail — not deferred to a shell):
|
|
// * always exists (seeded on construction; the book never reaches zero banks)
|
|
// * un-deletable (deleteBank rejects the pool)
|
|
// * un-renamable (renameBank rejects the pool)
|
|
// * un-evacuable (evacuate rejects the pool — the pool is evacuation's
|
|
// destination, not a source)
|
|
//
|
|
// -- Id minting is the CALLER'S job (design decision) ------------------------
|
|
//
|
|
// createBank takes a caller-supplied stable id, mirroring bank_model's "id
|
|
// assigned by the caller" and view_mode_model's mode ids. The pure core has no
|
|
// REAPER genGuid / RNG and deliberately introduces none: a fake in-model id source
|
|
// would not be a real GUID anyway, and keeping ids caller-supplied lets the B2
|
|
// shell mint a genuine REAPER GUID while the model stays pure and deterministically
|
|
// testable. The model still enforces the invariants: non-empty, unique, not the
|
|
// reserved pool id.
|
|
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "bank_model.h"
|
|
|
|
namespace reasampler {
|
|
|
|
// The pool's fixed identity. The id is reserved: createBank rejects it, and the
|
|
// pool is always bank-zero. The name is fixed: renameBank rejects the pool.
|
|
inline constexpr const char* kPoolBankId = "pool";
|
|
inline constexpr const char* kPoolBankName = "Pool";
|
|
|
|
// SlotMap — the L7 gap-preserving display-position carrier for ONE bank (F2 settled:
|
|
// plain interchangeable slots, NOT M9 fixed/addressable slots). A slot is just a
|
|
// display position a sample id occupies; the map is sample id -> slot (>= 0). Gaps
|
|
// are first-class: a bank may have a sample at slot 1 with slot 0 empty (an empty
|
|
// first row above an occupied second row). At most one id per slot (a slot is never
|
|
// double-occupied) and at most one slot per id (an id sits in exactly one place).
|
|
//
|
|
// Position lives HERE, not on Sample (CLAUDE.md wrapping discipline): a copy of one
|
|
// sample into two banks may sit at different slots, so position is a per-bank display
|
|
// concern owned by the bank's membership. bank_model / Sample stay untouched.
|
|
//
|
|
// PURE: standard library only. Hard-tested to the bar of BankIndex's round-trip.
|
|
class SlotMap {
|
|
public:
|
|
// The slot an id occupies, or -1 if the id is not mapped. O(N).
|
|
int slotOf(const std::string& id) const;
|
|
|
|
// The id occupying `slot`, or "" if the slot is empty. O(N).
|
|
std::string idAt(int slot) const;
|
|
|
|
// The highest occupied slot, or -1 when the map is empty. Defines the append
|
|
// frontier and (with trailing-empty trim) the content extent.
|
|
int maxSlot() const;
|
|
|
|
// Ids in ASCENDING slot order (the deterministic display order). Empty slots
|
|
// produce no entry — the caller iterates occupants; sparse layout is a draw
|
|
// concern that reads slotOf/idAt, not this list.
|
|
std::vector<std::string> orderedIds() const;
|
|
|
|
// Places `id` at the next free slot after the last occupied one (append). If the
|
|
// id is already mapped it is first removed (leaving its old slot empty), then
|
|
// appended — an append never fills an earlier gap. No-op guard: empty id ignored.
|
|
void append(const std::string& id);
|
|
|
|
// Drops `id`'s mapping, LEAVING ITS SLOT EMPTY (no re-pack) so every other id
|
|
// keeps its position. Returns true if the id was mapped.
|
|
bool remove(const std::string& id);
|
|
|
|
// Moves `id` to `targetSlot`, gap-preserving (F3 reorder semantics):
|
|
// * target slot EMPTY -> `id` moves there; its old slot is left empty.
|
|
// * target slot OCCUPIED -> insert-before-and-shift: `id` takes targetSlot and
|
|
// every occupant at slot >= targetSlot (except `id` itself) shifts up by one,
|
|
// preserving their relative order and never colliding. Matches file-manager
|
|
// reorder. Interior gaps between shifted occupants are preserved as-is
|
|
// (shift is +1 on each occupant, so the gap structure above the target is kept).
|
|
// * negative targetSlot is clamped to 0.
|
|
// Returns false (no mutation) if `id` is not mapped. Deterministic.
|
|
bool reorder(const std::string& id, int targetSlot);
|
|
|
|
// Rebuilds the map densely from `ids` in the given order (slot i = ids[i]),
|
|
// dropping any prior state. The migration path: a pre-L7 bank with no persisted
|
|
// slot data is seeded from its BankIndex insertion order, densely packed (no gaps),
|
|
// so it is visually identical on first post-L7 load. Empty/duplicate ids skipped.
|
|
void resetDense(const std::vector<std::string>& ids);
|
|
|
|
// Drops any mapping whose id is NOT in `liveIds` (a stale marker whose sample left
|
|
// the index) and appends any live id that has NO mapping yet (a sample the index
|
|
// gained out-of-band). Slots of surviving ids are untouched (gaps preserved). Keeps
|
|
// the map consistent with the bank's membership without a re-pack. Deterministic:
|
|
// orphan appends follow `liveIds` order.
|
|
void reconcile(const std::vector<std::string>& liveIds);
|
|
|
|
bool empty() const { return entries_.empty(); }
|
|
std::size_t size() const { return entries_.size(); }
|
|
|
|
bool operator==(const SlotMap& o) const;
|
|
|
|
// JSON fragment (an array of {id, slot} objects, ascending slot). Emitted as the
|
|
// bank envelope's "slots" member by BankBook::serialize; parsed back by its parser.
|
|
// Round-trips losslessly with the rest of the bank.
|
|
std::string serialize() const;
|
|
|
|
// Builds a map from explicit (id, slot) pairs parsed from persisted JSON. Enforces
|
|
// the map invariants defensively against a hand-edited blob: a duplicate id keeps
|
|
// its FIRST occurrence; a slot already taken by a kept id drops the later pair
|
|
// (never double-occupies); an empty id or negative slot is dropped. The result is
|
|
// sorted ascending by slot. reconcile() against live membership runs afterward, so
|
|
// a lossy repair here degrades gracefully rather than corrupting lookup.
|
|
static SlotMap fromEntries(const std::vector<std::pair<std::string, int>>& pairs);
|
|
|
|
private:
|
|
struct Entry {
|
|
std::string id;
|
|
int slot = 0;
|
|
bool operator==(const Entry& o) const { return id == o.id && slot == o.slot; }
|
|
};
|
|
std::vector<Entry> entries_; // kept sorted ascending by slot (invariant)
|
|
|
|
void sortBySlot();
|
|
};
|
|
|
|
// One bank: a stable id, a display name, an ordinal (tab/display order), and its
|
|
// own BankIndex. The pool is the bank whose id == kPoolBankId.
|
|
struct Bank {
|
|
std::string id; // stable, persisted; the pool's is kPoolBankId
|
|
std::string displayName; // mutable for named banks; fixed "Pool" for the pool
|
|
int ordinal = 0; // display order; pool is 0, named banks 1..N
|
|
BankIndex index; // this bank's samples
|
|
SlotMap slots; // L7 display positions of this bank's samples (gap-preserving)
|
|
|
|
bool isPool() const { return id == kPoolBankId; }
|
|
|
|
bool operator==(const Bank& o) const {
|
|
return id == o.id && displayName == o.displayName &&
|
|
ordinal == o.ordinal && index == o.index && slots == o.slots;
|
|
}
|
|
};
|
|
|
|
// Outcome of a cross-bank sample move/copy. Mirrors AddResult's honesty: the op
|
|
// reports what happened rather than silently mutating on a bad request.
|
|
// - Moved / Copied: the sample was transferred to the destination as a new entry.
|
|
// - Collapsed: the destination already held the hash; it collapsed onto the
|
|
// existing entry (a no-op add on the destination side). For a
|
|
// MOVE the source entry is STILL removed; for a COPY the source
|
|
// entry is (as always) retained.
|
|
// - RejectedUnknownBank: a source or destination id named no bank.
|
|
// - RejectedSampleAbsent: the sample id was not in the source bank.
|
|
// - RejectedSameBank: source and destination were the same bank (no-op).
|
|
enum class TransferResult {
|
|
Moved,
|
|
Copied,
|
|
Collapsed,
|
|
RejectedUnknownBank,
|
|
RejectedSampleAbsent,
|
|
RejectedSameBank,
|
|
};
|
|
|
|
// Scope of a sample-remove (fork R-A, settled 2026-07-24). ThisBank is the default
|
|
// and the ONLY behavior surfaced in the UI/action layer; AllBanks is a latent seam —
|
|
// live and tested at the model level, promotable later behind this parameter without
|
|
// a rewrite, but never wired to an affordance in B5.
|
|
// - ThisBank: drop the entry from the one named source bank only. A same-hash entry
|
|
// in another bank survives (no cross-bank cascade — dedup is per-bank).
|
|
// - AllBanks: drop the sample's entry from EVERY bank that holds the source id
|
|
// ("purge from the library"). Latent; unsurfaced.
|
|
enum class RemoveScope {
|
|
ThisBank,
|
|
AllBanks,
|
|
};
|
|
|
|
// Outcome of BankBook::removeSample. Mirrors TransferResult's honesty: the op reports
|
|
// what happened rather than silently mutating on a bad request.
|
|
// - Removed: at least one index entry was dropped.
|
|
// - RejectedUnknownBank: the source bank id named no bank (ThisBank scope only).
|
|
// - RejectedSampleAbsent: the sample id was in no bank in scope (nothing removed).
|
|
enum class RemoveResult {
|
|
Removed,
|
|
RejectedUnknownBank,
|
|
RejectedSampleAbsent,
|
|
};
|
|
|
|
// An ordered registry of banks with the pool seeded as bank-zero, per-bank sample
|
|
// indices, an active-bank pointer, and lossless JSON round-trip. The heart of the
|
|
// multi-bank phase — mirror of bank_model / view_mode_model.
|
|
class BankBook {
|
|
public:
|
|
BankBook(); // seeds the pool (id kPoolBankId, name kPoolBankName, ordinal 0);
|
|
// active bank = pool; zero named banks.
|
|
|
|
// -- Bank lifecycle ------------------------------------------------------
|
|
|
|
// Creates a named bank with the caller-supplied stable id and display name,
|
|
// assigning the next ordinal. Rejects (returns false, no mutation) an empty id,
|
|
// a duplicate id, the reserved pool id, or a display name that duplicates an
|
|
// existing bank's name (including the pool's "Pool"). Display-name uniqueness is
|
|
// trimmed + case-insensitive (ASCII): "Drums", "drums", and " Drums " collide.
|
|
bool createBank(const std::string& id, const std::string& displayName);
|
|
|
|
// Renames a named bank. Rejects (false, no mutation) an unknown id, the pool, or a
|
|
// target name already used by a DIFFERENT bank (trimmed + case-insensitive, as
|
|
// createBank). Renaming a bank to its own current name is a no-op success.
|
|
bool renameBank(const std::string& id, const std::string& displayName);
|
|
|
|
// Deletes a NAMED bank, removing it (and its member index entries) from the
|
|
// registry. Files are a shell/prune concern and are NOT touched here. Rejects
|
|
// (false, no mutation) an unknown id or the pool. Remaining banks' ordinals are
|
|
// compacted so the pool stays 0 and named banks stay contiguous 1..N. If the
|
|
// deleted bank was active, the active bank falls back to the pool.
|
|
bool deleteBank(const std::string& id);
|
|
|
|
// Reorders a NAMED bank to `newOrdinal` (clamped into the named-bank range),
|
|
// shifting the others to keep ordinals contiguous. The pool is pinned at 0 and
|
|
// cannot be reordered. Rejects (false, no mutation) an unknown id or the pool.
|
|
bool reorderBank(const std::string& id, int newOrdinal);
|
|
|
|
// Moves EVERY member of a named bank into the pool (index-only, observing the
|
|
// same destination-collapse-by-hash as a move), leaving the bank empty. Rejects
|
|
// (false, no mutation) an unknown id or the pool (the pool is the destination,
|
|
// never a source). Returns true on success even if the bank was already empty.
|
|
bool evacuate(const std::string& id);
|
|
|
|
// -- Active bank ---------------------------------------------------------
|
|
|
|
// The active bank's id (the capture target). Defaults to the pool.
|
|
const std::string& activeBankId() const { return activeBankId_; }
|
|
|
|
// Sets the active bank. Rejects (returns false, no change) an id that names no
|
|
// bank — an invalid set never corrupts state.
|
|
bool setActiveBank(const std::string& id);
|
|
|
|
// The active bank's BankIndex — the index the capture layer adds to. Always
|
|
// valid (the active id always names a live bank; it falls back to the pool).
|
|
BankIndex& activeIndex();
|
|
const BankIndex& activeIndex() const;
|
|
|
|
// -- Sample movement (index-only; files never relocate) ------------------
|
|
|
|
// Moves a sample by id from `fromBankId` to `toBankId`: removes it from the
|
|
// source index and adds it to the destination (observing destination
|
|
// collapse-by-hash). See TransferResult for the full outcome set.
|
|
TransferResult moveSample(const std::string& sampleId,
|
|
const std::string& fromBankId,
|
|
const std::string& toBankId);
|
|
|
|
// Copies a sample by id from `fromBankId` to `toBankId`: the source entry is
|
|
// retained, the destination gains it (observing destination collapse-by-hash).
|
|
// Same hash may then live in both banks — cross-bank dedup is NOT enforced.
|
|
TransferResult copySample(const std::string& sampleId,
|
|
const std::string& fromBankId,
|
|
const std::string& toBankId);
|
|
|
|
// -- Sample removal (index-only; the file is NEVER touched — orphaned until prune) --
|
|
|
|
// Drops a sample's index entry (the sample-level sibling of move/copy/evacuate).
|
|
// Index-only and non-destructive to the file: a last-reference remove leaves the
|
|
// file on disk, orphaned until Phase R prune — remove NEVER deletes bytes.
|
|
//
|
|
// Scope (fork R-A): ThisBank (default, the only surfaced verb) drops the entry from
|
|
// `fromBankId` alone; AllBanks (latent seam) drops the sample id from every bank
|
|
// that holds it. See RemoveResult for the outcome set.
|
|
// * ThisBank: RejectedUnknownBank if `fromBankId` names no bank; RejectedSampleAbsent
|
|
// if that bank does not hold the id; Removed on a drop.
|
|
// * AllBanks: `fromBankId` is ignored (the id is purged book-wide);
|
|
// RejectedSampleAbsent if NO bank held the id; Removed otherwise.
|
|
// No mutation occurs on any Rejected outcome (no-op guardrail for the undo layer).
|
|
RemoveResult removeSample(const std::string& sampleId,
|
|
const std::string& fromBankId,
|
|
RemoveScope scope = RemoveScope::ThisBank);
|
|
|
|
// -- Sample display order (L7; index membership untouched) ---------------
|
|
|
|
// The bank's sample ids in DISPLAY (slot) order — the deterministic order the grid
|
|
// iterates, sourced from the bank's SlotMap. Reconciles the map against live index
|
|
// membership first (drops stale markers, appends unmapped samples densely), so a
|
|
// freshly-migrated or out-of-band-mutated bank always yields a complete order. An
|
|
// unknown bank id yields an empty vector. Const-logical but reconciles lazily, so
|
|
// it is a non-const member.
|
|
std::vector<std::string> orderedSampleIds(const std::string& bankId);
|
|
|
|
// Ensures every bank's SlotMap is consistent with its index membership: seeds a
|
|
// map that has NO overlap with its index from insertion order (the pre-L7 migration
|
|
// default — dense, no gaps), and reconciles a partially-populated map (drop stale,
|
|
// append unmapped). Idempotent. Called after deserialize and after any capture/
|
|
// transfer that added samples out-of-band of the L7 reorder path.
|
|
void reconcileSlots();
|
|
|
|
// Reorders sample `id` within `bankId` to `targetSlot` (gap-preserving; see
|
|
// SlotMap::reorder). INDEX-ONLY of positions — the sample's membership, file, and
|
|
// metadata are untouched (capture != placement holds). Reconciles the bank's slots
|
|
// first so the target space is complete. Returns false (no mutation) on an unknown
|
|
// bank or an id the bank does not hold.
|
|
bool reorderSample(const std::string& id, const std::string& bankId, int targetSlot);
|
|
|
|
// Alt-replace (L7 F3): the dragged sample `newId` (already a member of `bankId`)
|
|
// takes the slot of the occupant `oldId`, and `oldId` is REMOVED from `bankId`'s
|
|
// index (index-only, same semantics as removeSample ThisBank — the file stays on
|
|
// disk; owned-manifest/prune govern bytes; hashReferencedElsewhere handles the
|
|
// last-reference case). Position of the slot is preserved; only its occupant changes.
|
|
//
|
|
// POOL GUARD (settled): the index-removal of `oldId` passes the SAME guard the
|
|
// remove verb applies — removeSample(oldId, bankId, ThisBank) must return Removed.
|
|
// For the pool this is permitted whenever the occupant exists (per-sample removal
|
|
// is not a pool privilege violation — the pool's guards are un-delete/rename/evacuate,
|
|
// never per-sample remove). If the removal would be rejected (occupant absent), the
|
|
// whole replace is rejected: false, NO mutation (neither the index nor the slots
|
|
// change), so the shell can fall back to the default insert-shift or a no-op.
|
|
// Rejects (false, no mutation) an unknown bank, a `newId`/`oldId` the bank does not
|
|
// hold, or `newId == oldId`. NEVER touches disk; introduces no new deletion authority.
|
|
bool replaceSample(const std::string& newId, const std::string& oldId,
|
|
const std::string& bankId);
|
|
|
|
// Refreshes a sample IN PLACE wherever it lives in the book (M10 re-capture):
|
|
// finds the bank holding `sampleId` and replaces its entry with `updated`
|
|
// (order-preserving, no dedup — see BankIndex::updateInPlace). Scans banks in
|
|
// ordinal order and updates the FIRST holder (a sample id is unique within a
|
|
// bank; the same id living in two banks via copy would update the earliest, which
|
|
// is acceptable — re-capture operates on the panel's focused single selection).
|
|
// Returns false (no mutation) if no bank holds the id or the replacement's path
|
|
// is absolute. Index-only and non-destructive to the timeline.
|
|
bool updateSampleInPlace(const std::string& sampleId, const Sample& updated);
|
|
|
|
// Reference-count query backing the confirm-on-last-reference guardrail: does any
|
|
// bank OTHER than `exceptBankId` still hold an entry whose contentHash == `hash`?
|
|
//
|
|
// Identity is the CONTENT HASH, not the file path: hash is the canonical dedup key
|
|
// the whole model already reasons in (findByHash / collapse-by-hash), and two
|
|
// entries that share content share one file — so "some other bank still references
|
|
// this hash" is exactly "removing here does not orphan the file." An EMPTY hash is
|
|
// never matched (it does not participate in dedup, mirroring findByHash), so an
|
|
// empty-hash sample reads as referenced-nowhere-else — the safe, confirm-eliciting
|
|
// direction (we cannot prove another bank shares an unhashed file).
|
|
bool hashReferencedElsewhere(const std::string& hash,
|
|
const std::string& exceptBankId) const;
|
|
|
|
// Every project-relative file path referenced by ANY bank in the book, pool
|
|
// included — the union across the whole book (Phase R, prune). This is the
|
|
// safety-critical referenced-set the prune core subtracts: a file referenced by
|
|
// any bank (INCLUDING via a copy into a second bank) appears here, so prune never
|
|
// reclaims it. Paths are returned VERBATIM (Sample.relativePath, exact strings —
|
|
// no normalization), first-seen order across banks in ordinal order then sample
|
|
// insertion order, and DE-DUPLICATED (one file referenced by N banks appears
|
|
// once). An empty relativePath is skipped (it references no file). Additive
|
|
// read-only query; adds no mutation and no coupling to Phase R.
|
|
std::vector<std::string> referencedPaths() const;
|
|
|
|
// -- Query ---------------------------------------------------------------
|
|
|
|
// The bank with `id`, or nullptr. Pointer invalidated by any mutating call.
|
|
Bank* bank(const std::string& id);
|
|
const Bank* bank(const std::string& id) const;
|
|
|
|
// The bank's BankIndex by id, or nullptr. Convenience over bank()->index.
|
|
BankIndex* index(const std::string& id);
|
|
const BankIndex* index(const std::string& id) const;
|
|
|
|
// The pool (always present). Never null.
|
|
Bank& pool();
|
|
const Bank& pool() const;
|
|
|
|
// All banks in ordinal order (pool first). The pool is always banks()[0].
|
|
const std::vector<Bank>& banks() const { return banks_; }
|
|
|
|
std::size_t size() const { return banks_.size(); } // >= 1 (the pool)
|
|
|
|
bool operator==(const BankBook& o) const {
|
|
return banks_ == o.banks_ && activeBankId_ == o.activeBankId_;
|
|
}
|
|
|
|
// -- Persistence ---------------------------------------------------------
|
|
|
|
// Serializes the whole book to a JSON string (lossless round-trip): the pool
|
|
// folded in as bank-zero + named banks + per-bank indices + ordinals + active
|
|
// id. deserialize(serialize(x)) == x.
|
|
std::string serialize() const;
|
|
|
|
// Parses a book JSON produced by serialize(). std::nullopt on malformed input.
|
|
//
|
|
// LEGACY MIGRATION: a bare legacy bank_index JSON (the pre-multi-bank shape, an
|
|
// object with a "samples" array and no "banks" key) is promoted into the pool's
|
|
// index, yielding a book of { pool } with zero named banks — one-way, lossless.
|
|
// After migration the book blob is authoritative (the caller persists the book
|
|
// shape going forward; the legacy key is retired by the B2 shell).
|
|
static std::optional<BankBook> deserialize(const std::string& json);
|
|
|
|
// Resolve a BankBook from the two persisted ext-state values a project may carry:
|
|
// the authoritative `banks` blob and the retired-but-possibly-present legacy
|
|
// `bank_index` blob. The persist shell (B2) hands both raw strings straight here so
|
|
// the load-source decision stays REAPER-free and unit-tested. Precedence:
|
|
// 1. non-empty `banksJson` present -> deserialize it (authoritative). If it is
|
|
// MALFORMED, do NOT silently fall back to the legacy blob — a corrupt `banks`
|
|
// blob is an error, not an absence; return an empty book so a stale legacy key
|
|
// can never resurrect a superseded single-bank state over a broken book.
|
|
// 2. else non-empty `legacyJson` -> deserialize it (one-way pool migration).
|
|
// 3. else (both absent/empty) -> a fresh empty book (pool only).
|
|
// Never returns nullopt: an unloadable input degrades to the empty book (matching
|
|
// the shell's existing "malformed -> ignore, start empty" behaviour), so the caller
|
|
// has one branchless install path.
|
|
static BankBook loadFromPersisted(const std::string& banksJson,
|
|
const std::string& legacyJson);
|
|
|
|
private:
|
|
std::vector<Bank> banks_; // ordinal order; banks_[0] is always the pool
|
|
std::string activeBankId_; // always names a live bank; defaults to pool
|
|
|
|
// True if a bank OTHER than `exceptId` already carries `name`'s uniqueness key
|
|
// (trimmed + case-insensitive, ASCII). Backs the create/rename uniqueness check;
|
|
// pass exceptId=id to let a bank keep (or re-case/-space) its own name.
|
|
bool displayNameTaken(const std::string& name, const std::string& exceptId) const;
|
|
|
|
// Re-sorts banks_ by ordinal (pool pinned first) and rewrites ordinals to a
|
|
// contiguous 0..N-1 so the pool is 0 and named banks are 1..N. Called after any
|
|
// structural change (create / delete / reorder).
|
|
void normalizeOrdinals();
|
|
|
|
// Replaces the book's banks with a parsed set, normalizes ordinals, and resolves
|
|
// the active bank (falling back to the pool if the id names no bank). Used only
|
|
// by deserialize; kept private so the public surface stays create/rename/etc.
|
|
void adoptBanks(std::vector<Bank>&& banks, const std::string& activeBank);
|
|
};
|
|
|
|
// The next bank id to activate when cycling the active bank forward, in ordinal
|
|
// order (the ids arrive pool-first, named 1..N, matching banks()). Wraps: the id
|
|
// after the last returns the first (pool → named → … → pool). This is the pure
|
|
// decision behind the "cycle active bank" action — the shell reads the book's
|
|
// ordered bank ids + current active id, asks for the next, and activates it.
|
|
// * empty list -> "" (nothing to cycle to)
|
|
// * single id (pool-only) -> that id (a one-bank book stays put)
|
|
// * currentBankId not present -> the first id (a sane home to jump to)
|
|
// Exposed as a free function (not a BankBook member) so it is unit-testable against
|
|
// a bare id vector without a full book. Mirror of view_mode_model's nextModeId.
|
|
std::string nextBankId(const std::vector<std::string>& orderedBankIds,
|
|
const std::string& currentBankId);
|
|
|
|
} // namespace reasampler
|