rename view_model -> view_mode_model; add shell-expands-FX test and parse-site comments

This commit is contained in:
2026-07-22 20:37:17 -04:00
parent 33ffb45a45
commit 25cfb62d46
4 changed files with 93 additions and 49 deletions
+35 -22
View File
@@ -1,4 +1,4 @@
#include "view_model.h"
#include "view_mode_model.h"
#include <algorithm>
#include <cerrno>
@@ -6,7 +6,7 @@
#include <cstdio>
#include <cstdlib>
// view_model implementation.
// view_mode_model implementation.
//
// JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief:
// keep the pure core dependency-free — no third-party JSON lib). A compact writer
@@ -124,31 +124,31 @@ TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
}
// ---------------------------------------------------------------------------
// ViewModel
// ViewModeModel
// ---------------------------------------------------------------------------
ViewModel::ViewModel() : activeModeId_(kArrangeModeId) {}
ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {}
bool ViewModel::setActiveMode(const std::string& modeId) {
bool ViewModeModel::setActiveMode(const std::string& modeId) {
if (!modes_.contains(modeId)) return false;
activeModeId_ = modeId;
return true;
}
void ViewModel::storeSnapshot(const std::string& guid, const TrackSnapshot& snap) {
void ViewModeModel::storeSnapshot(const std::string& guid, const TrackSnapshot& snap) {
snapshots_[guid] = snap;
}
void ViewModel::clearSnapshot(const std::string& guid) {
void ViewModeModel::clearSnapshot(const std::string& guid) {
snapshots_.erase(guid);
}
const TrackSnapshot* ViewModel::snapshot(const std::string& guid) const {
const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
auto it = snapshots_.find(guid);
return it == snapshots_.end() ? nullptr : &it->second;
}
bool ViewModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const {
bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const {
const Membership* m = membership_.query(guid);
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
if (m->showBoth) return true; // show-both ⇒ every mode
@@ -156,7 +156,7 @@ bool ViewModel::leafBelongsToMode(const std::string& guid, const std::string& mo
return m->modeIds.count(modeId) > 0;
}
std::set<std::string> ViewModel::visibleTracks(const FolderTree& tree,
std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
const std::string& modeId) const {
std::set<std::string> visible;
@@ -189,7 +189,7 @@ std::set<std::string> ViewModel::visibleTracks(const FolderTree& tree,
return visible;
}
TogglePlan ViewModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
TogglePlan plan;
// Index the tree so we can classify each tagged GUID (leaf vs parent vs stale).
@@ -209,10 +209,10 @@ TogglePlan ViewModel::planToggle(const FolderTree& tree, const std::string& targ
if (const TrackSnapshot* snap = snapshot(guid))
plan.restore.push_back(makeRestorePlan(guid, *snap));
} else {
// Inactive tagged leaf ⇒ park. FX count is unknown to the pure model;
// the shell expands per-FX offline from TrackFX_GetCount. We emit the
// scalar flags and leave fxOffline to the shell for the count (park
// offlines ALL, so no per-slot value decision is needed here).
// Inactive tagged leaf ⇒ park. fxOffline is intentionally empty here:
// the D2 shell expands per-FX offline writes using TrackFX_GetCount.
// The pure model has no access to REAPER FX counts at plan time;
// makeParkPlan(guid, 0) emits only the scalar flags as a result.
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
}
}
@@ -220,7 +220,7 @@ TogglePlan ViewModel::planToggle(const FolderTree& tree, const std::string& targ
return plan;
}
bool ViewModel::operator==(const ViewModel& o) const {
bool ViewModeModel::operator==(const ViewModeModel& o) const {
return modes_ == o.modes_ && membership_ == o.membership_ &&
activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_;
}
@@ -301,7 +301,7 @@ private:
} // namespace
std::string ViewModel::serialize() const {
std::string ViewModeModel::serialize() const {
std::string out;
{
ObjWriter root(out);
@@ -378,7 +378,7 @@ namespace {
class Parser {
public:
explicit Parser(const std::string& s) : s_(s) {}
bool parseModel(ViewModel& out);
bool parseModel(ViewModeModel& out);
private:
const std::string& s_;
@@ -622,6 +622,15 @@ bool Parser::parseMembership(MembershipIndex& idx) {
// Install the entry verbatim (tag() would clear a multi-mode set and drop
// show-both). A serialized entry is trusted to already satisfy the model's
// invariants.
//
// Deliberate tolerance: we do NOT validate that membership modeIds reference
// registered modes, and we do not validate snapshot GUIDs against the index.
// Stale-GUID and stale-mode tolerance is a stated invariant of this model —
// a deserialized entry is treated as trusted data, not as live cross-checked
// state. Rejecting stale entries here would violate that invariant. The one
// exception is activeMode (validated below in parseModel): a persisted active
// mode that no longer exists has an immediate behavioral consequence, so it
// is caught and the parse is rejected.
if (!idx.restore(guid, mem)) return false;
} while (consume(','));
return consume(']');
@@ -654,7 +663,7 @@ bool Parser::parseSnapshots(std::map<std::string, TrackSnapshot>& snaps) {
return consume(']');
}
bool Parser::parseModel(ViewModel& out) {
bool Parser::parseModel(ViewModeModel& out) {
if (!consume('{')) return false;
skipWs();
if (consume('}')) return true; // lenient empty root ⇒ default-seeded model
@@ -682,7 +691,11 @@ bool Parser::parseModel(ViewModel& out) {
} else if (key == "snapshots") {
if (!parseSnapshots(snaps)) return false;
} else {
if (!skipValue()) return false; // version, unknown keys
// Unknown keys and the "version" field are skipped here.
// "version" is serialized as a forward-compat placeholder — there is no
// active version gate yet; all persisted data is parsed the same way
// regardless of the value. A future gate would add a version branch here.
if (!skipValue()) return false;
}
} while (consume(','));
@@ -701,8 +714,8 @@ bool Parser::parseModel(ViewModel& out) {
} // namespace
std::optional<ViewModel> ViewModel::deserialize(const std::string& json) {
ViewModel vm;
std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string& json) {
ViewModeModel vm;
Parser p(json);
if (!p.parseModel(vm)) return std::nullopt;
return vm;
+10 -6
View File
@@ -1,5 +1,5 @@
#pragma once
// view_model — the pure core of the Design View feature, deliberately free of any
// view_mode_model — the pure core of the Design View feature, deliberately free of any
// REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the mirror of
// bank_model: it owns the mode registry, the GUID-keyed membership index, the
// folder-tree-aware visibility derivation, the parking/restore planner, and the
@@ -239,15 +239,15 @@ struct TogglePlan {
std::vector<TrackPlan> restore; // active leaves returning -> snapshot values
};
// -- The view model ----------------------------------------------------------
// -- The view mode model -----------------------------------------------------
//
// Owns the mode registry, the membership index, the active mode, and the durable
// per-track snapshots (kept for tracks currently parked so a save-while-parked
// project restores correctly). Visibility and the toggle plan are computed against
// a supplied FolderTree — the tree is never stored.
class ViewModel {
class ViewModeModel {
public:
ViewModel(); // Arrange + Design seeded; active mode = Arrange
ViewModeModel(); // Arrange + Design seeded; active mode = Arrange
ModeRegistry& modes() { return modes_; }
const ModeRegistry& modes() const { return modes_; }
@@ -283,15 +283,19 @@ public:
// leaves that become active AND have a stored snapshot are restored from it.
// Parents, show-both leaves, the master, and untagged tracks are never parked.
// Unknown/stale membership GUIDs absent from the tree are ignored (prune-safe).
//
// Note: park plans emitted here have an empty fxOffline vector. The D2 shell
// expands per-FX offline writes using TrackFX_GetCount — the pure model has no
// access to REAPER FX counts at plan time.
TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;
bool operator==(const ViewModel& o) const;
bool operator==(const ViewModeModel& o) const;
std::string serialize() const;
// Parses a JSON string produced by serialize(). std::nullopt on malformed
// input. On success deserialize(serialize(x)) == x.
static std::optional<ViewModel> deserialize(const std::string& json);
static std::optional<ViewModeModel> deserialize(const std::string& json);
private:
ModeRegistry modes_;