Skip to content

Commit a2fa6b4

Browse files
committed
added default speaker layout config functionality - unsure of windows functiolity
1 parent 43e31f7 commit a2fa6b4

9 files changed

Lines changed: 559 additions & 0 deletions

File tree

internalDocs/AGENTS.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,42 @@ spatialroot/
190190

191191
---
192192

193+
---
194+
195+
## App Storage Conventions
196+
197+
Two separate storage areas are used at runtime. **Never mix them.**
198+
199+
### Session temp/cache (auto-deleted on app close)
200+
201+
| Platform | Path |
202+
|----------|------|
203+
| macOS | `~/Library/Caches/CultDSP/SpatialRoot/temp-sessions/` |
204+
| Windows | `%LOCALAPPDATA%/CultDSP/SpatialRoot/Cache/temp-sessions/` |
205+
| Linux | `$XDG_CACHE_HOME/CultDSP/SpatialRoot/temp-sessions/` or `~/.cache/CultDSP/SpatialRoot/temp-sessions/` |
206+
207+
Contents: temporary LUSID scenes, transcoder outputs, diagnostic reports. Each session gets a UUID-stamped subdirectory with a `.spatialroot_temp_session` marker file. Deleted on clean shutdown unless `--keep-temp-sessions` is set.
208+
Override: `SPATIALROOT_TEMP_ROOT` env var.
209+
Implementation: `SpatialRootPaths::cacheRoot()` / `tempSessionsRoot()` / `SpatialRootPaths.cpp`.
210+
211+
### Persistent app settings (never auto-deleted)
212+
213+
| Platform | Path |
214+
|----------|------|
215+
| macOS | `~/Library/Application Support/Spatial Root/` |
216+
| Windows | `%APPDATA%/Spatial Root/` |
217+
| Linux | `$XDG_CONFIG_HOME/spatial-root/` or `~/.config/spatial-root/` |
218+
219+
Contents:
220+
- `default_layout.json` — authoritative copy of the saved default speaker layout
221+
- `default_layout.meta.json` — display metadata (sourcePath, savedAt, layoutName)
222+
223+
These files are **not** deleted on app close. The engine never reads these directly — the GUI reads and validates them on startup, then passes the layout path into `EngineSession::applyLayout()` explicitly.
224+
Override: `SPATIALROOT_SETTINGS_ROOT` env var.
225+
Implementation: `SpatialRootPaths::appSettingsRoot()` / `DefaultLayoutManager`.
226+
227+
---
228+
193229
## Runtime Control Plane
194230

195231
**Primary (in-process):** ImGui GUI calls direct C++ setters on `EngineSession`:

internalDocs/devHistory.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,3 +593,4 @@ processedData/stageForRender/ — cult-transcoder writes scene.lusid.json her
593593
| Phase 6 | March 31, 2026 | C++ refactor complete. Python GUI/entrypoints/build/venv removed. ImGui + GLFW GUI shipped. |
594594
| Phase 7 | April 17, 2026 | Normalized DBAP (`sum(v_k²)=1`). `thirdparty/allolib``internal/cult-allolib`. Auto-compensation removed. |
595595
| Bug 10.1 | May 7, 2026 | Fast-mover continuity anchor fix for normalized DBAP (`mPrevSafePos` written as last sub-step position). |
596+
| Phase 8 | May 10, 2026 | Persistent default speaker layout + cross-platform app settings paths. `DefaultLayoutManager` added to GUI layer. Settings dir (`~/Library/Application Support/Spatial Root/` etc.) is strictly separate from session temp cache. Atomic writes, non-fatal startup fallback, GUI controls: Set as Default / Clear Default / status display. |

source/gui/imgui/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ set(IMGUI_SOURCES
5656
set(APP_SOURCES
5757
src/main.cpp
5858
src/App.cpp
59+
src/DefaultLayoutManager.cpp
5960
src/SpatialRootPaths.cpp
6061
src/SubprocessRunner.cpp
6162
src/FileDialog.cpp

source/gui/imgui/src/App.cpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ App::App(std::string projectRoot, bool keepTempSessions, std::string tempRootOve
6060
: mProjectRoot(std::move(projectRoot))
6161
, mKeepTempSessions(keepTempSessions)
6262
, mTempRootOverride(std::move(tempRootOverride))
63+
, mDefaultLayoutMgr()
6364
, mSession(std::make_unique<EngineSession>()) {
6465
mLayoutPath = resolveProjectPath(kLayoutPaths[0]);
6566

@@ -70,6 +71,9 @@ App::App(std::string projectRoot, bool keepTempSessions, std::string tempRootOve
7071
appendEngineLog("[GUI] Keeping temporary sessions for debugging is enabled.",
7172
{1.f, 0.8f, 0.2f, 1.f});
7273
}
74+
75+
tryLoadDefaultLayoutOnStartup();
76+
7377
appendEngineLog("[GUI] Select a source and layout, then click START.");
7478

7579
int lw = 0, lh = 0, lch = 0;
@@ -305,6 +309,8 @@ void App::renderEngineTab() {
305309
if (!p.empty()) { mLayoutPath = p; mLayoutPreset = IM_ARRAYSIZE(kLayoutNames) - 1; }
306310
}
307311

312+
renderDefaultLayoutControls();
313+
308314
ImGui::SetCursorPosX(120.f);
309315
if (ImGui::Button("Layout Builder##layoutbuilder")) {
310316
#ifdef __APPLE__
@@ -1105,6 +1111,116 @@ void App::doLaunchEngine(const std::string& scenePath,
11051111
appendEngineLog("[Engine] Started successfully. OSC port 9009.", {0.3f, 0.9f, 0.3f, 1.f});
11061112
}
11071113

1114+
void App::tryLoadDefaultLayoutOnStartup() {
1115+
const DefaultLayoutResult r = mDefaultLayoutMgr.loadDefaultLayout();
1116+
mDefaultLayoutStatus = r.status;
1117+
mDefaultLayoutSavedAt = r.savedAt;
1118+
mDefaultLayoutName = r.layoutName;
1119+
mDefaultLayoutSourcePath = r.sourcePath;
1120+
1121+
if (r.status == DefaultLayoutStatus::None) {
1122+
appendEngineLog("[GUI] No default layout saved.");
1123+
return;
1124+
}
1125+
if (!r.success) {
1126+
appendEngineLog("[GUI] WARNING: Saved default layout could not be loaded: " + r.message,
1127+
{1.f, 0.8f, 0.2f, 1.f});
1128+
return;
1129+
}
1130+
// Write the validated JSON to a temp file so we can point mLayoutPath at it.
1131+
// We write it to the same settings dir — it IS the settings dir copy.
1132+
mLayoutPath = pathString(mDefaultLayoutMgr.layoutPath());
1133+
mLayoutPreset = IM_ARRAYSIZE(kLayoutNames) - 1; // "Custom"
1134+
1135+
const std::string displayName = r.layoutName.empty() ? "default layout" : r.layoutName;
1136+
appendEngineLog("[GUI] Default layout loaded: " + displayName, {0.3f, 0.9f, 0.3f, 1.f});
1137+
}
1138+
1139+
void App::onSetAsDefaultLayout() {
1140+
if (mLayoutPath.empty()) {
1141+
appendEngineLog("[GUI] No layout selected — cannot save as default.", {1.f, 0.5f, 0.2f, 1.f});
1142+
return;
1143+
}
1144+
std::ifstream f(mLayoutPath);
1145+
if (!f.is_open()) {
1146+
appendEngineLog("[GUI] Cannot read layout file: " + mLayoutPath, {1.f, 0.4f, 0.4f, 1.f});
1147+
return;
1148+
}
1149+
const std::string jsonText((std::istreambuf_iterator<char>(f)),
1150+
std::istreambuf_iterator<char>());
1151+
1152+
const DefaultLayoutResult r = mDefaultLayoutMgr.saveDefaultLayout(jsonText, mLayoutPath);
1153+
mDefaultLayoutStatus = r.status;
1154+
mDefaultLayoutSavedAt = r.savedAt;
1155+
mDefaultLayoutName = r.layoutName;
1156+
mDefaultLayoutSourcePath = r.sourcePath;
1157+
1158+
if (r.success) {
1159+
appendEngineLog("[GUI] Default layout saved: " +
1160+
(r.layoutName.empty() ? mLayoutPath : r.layoutName),
1161+
{0.3f, 0.9f, 0.3f, 1.f});
1162+
} else {
1163+
appendEngineLog("[GUI] Failed to save default layout: " + r.message, {1.f, 0.4f, 0.4f, 1.f});
1164+
}
1165+
}
1166+
1167+
void App::onClearDefaultLayout() {
1168+
const DefaultLayoutResult r = mDefaultLayoutMgr.clearDefaultLayout();
1169+
mDefaultLayoutStatus = DefaultLayoutStatus::None;
1170+
mDefaultLayoutSavedAt.clear();
1171+
mDefaultLayoutName.clear();
1172+
mDefaultLayoutSourcePath.clear();
1173+
appendEngineLog("[GUI] Default layout cleared.", {0.65f, 0.65f, 0.9f, 1.f});
1174+
(void)r;
1175+
}
1176+
1177+
void App::renderDefaultLayoutControls() {
1178+
const ImVec4 kGreen = {0.20f, 0.62f, 0.25f, 1.f};
1179+
const ImVec4 kAmber = {0.70f, 0.45f, 0.08f, 1.f};
1180+
const ImVec4 kRed = {0.72f, 0.18f, 0.15f, 1.f};
1181+
const ImVec4 kBlue = {0.40f, 0.60f, 0.90f, 1.f};
1182+
1183+
ImGui::TextDisabled("DEFAULT LAYOUT");
1184+
ImGui::SameLine(160.f);
1185+
1186+
// Status indicator
1187+
switch (mDefaultLayoutStatus) {
1188+
case DefaultLayoutStatus::None:
1189+
ImGui::TextDisabled("none saved");
1190+
break;
1191+
case DefaultLayoutStatus::Loaded:
1192+
ImGui::TextColored(kGreen, "loaded");
1193+
if (!mDefaultLayoutName.empty()) {
1194+
ImGui::SameLine(); ImGui::TextDisabled("(%s)", mDefaultLayoutName.c_str());
1195+
}
1196+
if (!mDefaultLayoutSavedAt.empty()) {
1197+
ImGui::SameLine(); ImGui::TextDisabled("saved %s", mDefaultLayoutSavedAt.c_str());
1198+
}
1199+
break;
1200+
case DefaultLayoutStatus::Invalid:
1201+
ImGui::TextColored(kAmber, "saved file invalid — check layout JSON");
1202+
break;
1203+
case DefaultLayoutStatus::Unavailable:
1204+
ImGui::TextColored(kRed, "unavailable (permission/read error)");
1205+
break;
1206+
}
1207+
1208+
ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - 300.f);
1209+
1210+
const bool hasLayout = !mLayoutPath.empty();
1211+
if (!hasLayout) ImGui::BeginDisabled(true);
1212+
if (ImGui::SmallButton("Set as Default")) onSetAsDefaultLayout();
1213+
if (!hasLayout) ImGui::EndDisabled();
1214+
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Save current layout as startup default");
1215+
1216+
ImGui::SameLine();
1217+
const bool hasDefault = (mDefaultLayoutStatus != DefaultLayoutStatus::None);
1218+
if (!hasDefault) ImGui::BeginDisabled(true);
1219+
if (ImGui::SmallButton("Clear Default")) onClearDefaultLayout();
1220+
if (!hasDefault) ImGui::EndDisabled();
1221+
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Remove the saved default layout");
1222+
}
1223+
11081224
void App::resetRuntimeToDefaults() {
11091225
mGainDb = 0.0f;
11101226
mFocus = 1.5f;

source/gui/imgui/src/App.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// DEV NOTE: Evaluate whether to add an OSC enable/disable toggle in a future
1616
// iteration. For V1, always-on is simplest and matches current behaviour.
1717

18+
#include "DefaultLayoutManager.hpp"
1819
#include "EngineSession.hpp"
1920
#include "SpatialRootPaths.hpp"
2021
#include "SubprocessRunner.hpp"
@@ -70,6 +71,13 @@ class App {
7071
bool mKeepTempSessions = false;
7172
std::string mTempRootOverride;
7273

74+
// ── Default layout manager (persistent settings, not session temp) ───
75+
DefaultLayoutManager mDefaultLayoutMgr;
76+
DefaultLayoutStatus mDefaultLayoutStatus = DefaultLayoutStatus::None;
77+
std::string mDefaultLayoutSavedAt;
78+
std::string mDefaultLayoutName;
79+
std::string mDefaultLayoutSourcePath;
80+
7381
// ── Engine ───────────────────────────────────────────────────────────
7482
std::unique_ptr<EngineSession> mSession;
7583
AppState mState = AppState::Idle;
@@ -251,6 +259,12 @@ class App {
251259
void clearStandaloneTranscodeTempState();
252260
static std::string pathString(const std::filesystem::path& path);
253261

262+
// Default layout helpers
263+
void tryLoadDefaultLayoutOnStartup();
264+
void onSetAsDefaultLayout();
265+
void onClearDefaultLayout();
266+
void renderDefaultLayoutControls();
267+
254268
// Log helpers (main thread only for mEngineLog)
255269
void appendEngineLog(const std::string& text,
256270
ImVec4 color = {0.85f, 0.85f, 0.85f, 1.f});

0 commit comments

Comments
 (0)