mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
ROADMAP.md
This commit is contained in:
@@ -309,9 +309,14 @@ set(FRAME_WORKFLOW_SOURCES
|
||||
src/services/impl/workflow_frame_render_step.cpp
|
||||
src/services/impl/workflow_frame_audio_step.cpp
|
||||
src/services/impl/workflow_frame_gui_step.cpp
|
||||
src/services/impl/workflow_soundboard_catalog_scan_step.cpp
|
||||
src/services/impl/workflow_soundboard_gui_step.cpp
|
||||
src/services/impl/workflow_soundboard_audio_step.cpp
|
||||
src/services/impl/workflow_validation_checkpoint_step.cpp
|
||||
src/services/impl/frame_workflow_step_registrar.cpp
|
||||
src/services/impl/frame_workflow_service.cpp
|
||||
src/services/impl/soundboard_state_service.cpp
|
||||
src/services/impl/soundboard_path_resolver.cpp
|
||||
)
|
||||
|
||||
set(MATERIALX_SCRIPT_SOURCES
|
||||
|
||||
@@ -8,20 +8,43 @@
|
||||
"width": 664,
|
||||
"height": 520
|
||||
},
|
||||
"padding": {
|
||||
"x": 20,
|
||||
"y": 20
|
||||
},
|
||||
"colors": {
|
||||
"background": [0.06, 0.07, 0.09, 0.95],
|
||||
"border": [0.35, 0.38, 0.42, 1.0],
|
||||
"text": [0.96, 0.96, 0.97, 1.0]
|
||||
"title": [0.96, 0.96, 0.97, 1.0],
|
||||
"description": [0.7, 0.75, 0.8, 1.0],
|
||||
"category": [0.9, 0.9, 0.95, 1.0],
|
||||
"status": [0.6, 0.8, 1.0, 1.0]
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"width": 300,
|
||||
"height": 36,
|
||||
"spacing": 12
|
||||
"spacing": 12,
|
||||
"colors": {
|
||||
"background": [0.2, 0.24, 0.28, 1.0],
|
||||
"hover": [0.26, 0.3, 0.36, 1.0],
|
||||
"active": [0.16, 0.22, 0.28, 1.0],
|
||||
"border": [0.45, 0.52, 0.6, 1.0],
|
||||
"text": [1.0, 1.0, 1.0, 1.0]
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"columns": 2,
|
||||
"columnSpacing": 24,
|
||||
"initialY": 120
|
||||
"initialY": 80,
|
||||
"statusOffsetY": 34
|
||||
},
|
||||
"typography": {
|
||||
"titleSize": 24,
|
||||
"descriptionSize": 14,
|
||||
"categorySize": 20,
|
||||
"buttonSize": 16,
|
||||
"statusSize": 14,
|
||||
"headerSpacing": 26
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
"plugin": "soundboard.gui",
|
||||
"position": [520, -120],
|
||||
"inputs": {
|
||||
"catalog": "soundboard.catalog",
|
||||
"layout": "soundboard.layout"
|
||||
"catalog": "soundboard.catalog"
|
||||
},
|
||||
"outputs": {
|
||||
"selection": "soundboard.selection"
|
||||
"selection": "soundboard.selection",
|
||||
"gui_commands": "soundboard.gui.commands"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -35,7 +35,6 @@
|
||||
"plugin": "soundboard.audio",
|
||||
"position": [780, -120],
|
||||
"inputs": {
|
||||
"catalog": "soundboard.catalog",
|
||||
"selection": "soundboard.selection"
|
||||
},
|
||||
"outputs": {
|
||||
@@ -47,7 +46,8 @@
|
||||
"plugin": "frame.render",
|
||||
"position": [520, 120],
|
||||
"inputs": {
|
||||
"elapsed": "frame.elapsed"
|
||||
"elapsed": "frame.elapsed",
|
||||
"gui_commands": "soundboard.gui.commands"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "services/impl/logger_service.hpp"
|
||||
#include "services/impl/pipeline_compiler_service.hpp"
|
||||
#include "services/impl/validation_tour_service.hpp"
|
||||
#include "services/impl/soundboard_state_service.hpp"
|
||||
#include "services/impl/workflow_default_step_registrar.hpp"
|
||||
#include "services/impl/workflow_definition_parser.hpp"
|
||||
#include "services/impl/workflow_executor.hpp"
|
||||
@@ -305,6 +306,9 @@ void ServiceBasedApp::RegisterServices() {
|
||||
registry_.RegisterService<services::IAudioService, services::impl::SdlAudioService>(
|
||||
registry_.GetService<services::ILogger>());
|
||||
|
||||
registry_.RegisterService<services::ISoundboardStateService, services::impl::SoundboardStateService>(
|
||||
registry_.GetService<services::ILogger>());
|
||||
|
||||
registry_.RegisterService<services::IFrameWorkflowService, services::impl::FrameWorkflowService>(
|
||||
registry_.GetService<services::ILogger>(),
|
||||
registry_.GetService<services::IConfigService>(),
|
||||
@@ -313,7 +317,8 @@ void ServiceBasedApp::RegisterServices() {
|
||||
registry_.GetService<services::IPhysicsService>(),
|
||||
registry_.GetService<services::ISceneService>(),
|
||||
registry_.GetService<services::IRenderCoordinatorService>(),
|
||||
registry_.GetService<services::IValidationTourService>());
|
||||
registry_.GetService<services::IValidationTourService>(),
|
||||
registry_.GetService<services::ISoundboardStateService>());
|
||||
|
||||
// Script bridge services
|
||||
registry_.RegisterService<services::IMeshService, services::impl::MeshService>(
|
||||
|
||||
@@ -16,6 +16,7 @@ FrameWorkflowService::FrameWorkflowService(std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<ISceneService> sceneService,
|
||||
std::shared_ptr<IRenderCoordinatorService> renderService,
|
||||
std::shared_ptr<IValidationTourService> validationTourService,
|
||||
std::shared_ptr<ISoundboardStateService> soundboardStateService,
|
||||
const std::filesystem::path& templatePath)
|
||||
: registry_(std::make_shared<WorkflowStepRegistry>()),
|
||||
executor_(registry_, logger),
|
||||
@@ -34,7 +35,8 @@ FrameWorkflowService::FrameWorkflowService(std::shared_ptr<ILogger> logger,
|
||||
std::move(physicsService),
|
||||
std::move(sceneService),
|
||||
std::move(renderService),
|
||||
std::move(validationTourService));
|
||||
std::move(validationTourService),
|
||||
std::move(soundboardStateService));
|
||||
registrar.RegisterUsedSteps(workflow_, registry_);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../interfaces/i_physics_service.hpp"
|
||||
#include "../interfaces/i_render_coordinator_service.hpp"
|
||||
#include "../interfaces/i_scene_service.hpp"
|
||||
#include "../interfaces/i_soundboard_state_service.hpp"
|
||||
#include "../interfaces/i_validation_tour_service.hpp"
|
||||
|
||||
#include "workflow_executor.hpp"
|
||||
@@ -29,6 +30,7 @@ public:
|
||||
std::shared_ptr<ISceneService> sceneService,
|
||||
std::shared_ptr<IRenderCoordinatorService> renderService,
|
||||
std::shared_ptr<IValidationTourService> validationTourService,
|
||||
std::shared_ptr<ISoundboardStateService> soundboardStateService,
|
||||
const std::filesystem::path& templatePath = {});
|
||||
|
||||
void ExecuteFrame(float deltaTime, float elapsedTime) override;
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "workflow_frame_physics_step.hpp"
|
||||
#include "workflow_frame_render_step.hpp"
|
||||
#include "workflow_frame_scene_step.hpp"
|
||||
#include "workflow_soundboard_audio_step.hpp"
|
||||
#include "workflow_soundboard_catalog_scan_step.hpp"
|
||||
#include "workflow_soundboard_gui_step.hpp"
|
||||
#include "workflow_step_registry.hpp"
|
||||
#include "workflow_validation_checkpoint_step.hpp"
|
||||
|
||||
@@ -23,7 +26,8 @@ FrameWorkflowStepRegistrar::FrameWorkflowStepRegistrar(std::shared_ptr<ILogger>
|
||||
std::shared_ptr<IPhysicsService> physicsService,
|
||||
std::shared_ptr<ISceneService> sceneService,
|
||||
std::shared_ptr<IRenderCoordinatorService> renderService,
|
||||
std::shared_ptr<IValidationTourService> validationTourService)
|
||||
std::shared_ptr<IValidationTourService> validationTourService,
|
||||
std::shared_ptr<ISoundboardStateService> soundboardStateService)
|
||||
: logger_(std::move(logger)),
|
||||
configService_(std::move(configService)),
|
||||
audioService_(std::move(audioService)),
|
||||
@@ -31,7 +35,8 @@ FrameWorkflowStepRegistrar::FrameWorkflowStepRegistrar(std::shared_ptr<ILogger>
|
||||
physicsService_(std::move(physicsService)),
|
||||
sceneService_(std::move(sceneService)),
|
||||
renderService_(std::move(renderService)),
|
||||
validationTourService_(std::move(validationTourService)) {}
|
||||
validationTourService_(std::move(validationTourService)),
|
||||
soundboardStateService_(std::move(soundboardStateService)) {}
|
||||
|
||||
void FrameWorkflowStepRegistrar::RegisterUsedSteps(
|
||||
const WorkflowDefinition& workflow,
|
||||
@@ -72,6 +77,20 @@ void FrameWorkflowStepRegistrar::RegisterUsedSteps(
|
||||
registry->RegisterStep(std::make_shared<WorkflowValidationCheckpointStep>(
|
||||
validationTourService_, logger_));
|
||||
}
|
||||
if (plugins.contains("soundboard.catalog.scan")) {
|
||||
registry->RegisterStep(std::make_shared<WorkflowSoundboardCatalogScanStep>(configService_, logger_));
|
||||
}
|
||||
if (plugins.contains("soundboard.gui")) {
|
||||
registry->RegisterStep(std::make_shared<WorkflowSoundboardGuiStep>(inputService_,
|
||||
configService_,
|
||||
soundboardStateService_,
|
||||
logger_));
|
||||
}
|
||||
if (plugins.contains("soundboard.audio")) {
|
||||
registry->RegisterStep(std::make_shared<WorkflowSoundboardAudioStep>(audioService_,
|
||||
soundboardStateService_,
|
||||
logger_));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
|
||||
@@ -13,6 +13,7 @@ class IInputService;
|
||||
class IPhysicsService;
|
||||
class IRenderCoordinatorService;
|
||||
class ISceneService;
|
||||
class ISoundboardStateService;
|
||||
class IValidationTourService;
|
||||
}
|
||||
|
||||
@@ -27,7 +28,8 @@ public:
|
||||
std::shared_ptr<IPhysicsService> physicsService,
|
||||
std::shared_ptr<ISceneService> sceneService,
|
||||
std::shared_ptr<IRenderCoordinatorService> renderService,
|
||||
std::shared_ptr<IValidationTourService> validationTourService);
|
||||
std::shared_ptr<IValidationTourService> validationTourService,
|
||||
std::shared_ptr<ISoundboardStateService> soundboardStateService);
|
||||
|
||||
void RegisterUsedSteps(const WorkflowDefinition& workflow,
|
||||
const std::shared_ptr<IWorkflowStepRegistry>& registry) const;
|
||||
@@ -41,6 +43,7 @@ private:
|
||||
std::shared_ptr<ISceneService> sceneService_;
|
||||
std::shared_ptr<IRenderCoordinatorService> renderService_;
|
||||
std::shared_ptr<IValidationTourService> validationTourService_;
|
||||
std::shared_ptr<ISoundboardStateService> soundboardStateService_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
|
||||
@@ -132,14 +132,22 @@ void RenderCoordinatorService::ConfigureRenderGraphPasses() {
|
||||
}
|
||||
|
||||
void RenderCoordinatorService::RenderFrame(float time) {
|
||||
RenderFrameInternal(time, nullptr);
|
||||
RenderFrameInternal(time, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void RenderCoordinatorService::RenderFrameWithViewState(float time, const ViewState& viewState) {
|
||||
RenderFrameInternal(time, &viewState);
|
||||
RenderFrameInternal(time, &viewState, nullptr);
|
||||
}
|
||||
|
||||
void RenderCoordinatorService::RenderFrameInternal(float time, const ViewState* overrideView) {
|
||||
void RenderCoordinatorService::RenderFrameWithOverrides(float time,
|
||||
const ViewState* viewState,
|
||||
const std::vector<GuiCommand>* guiCommands) {
|
||||
RenderFrameInternal(time, viewState, guiCommands);
|
||||
}
|
||||
|
||||
void RenderCoordinatorService::RenderFrameInternal(float time,
|
||||
const ViewState* overrideView,
|
||||
const std::vector<GuiCommand>* guiCommands) {
|
||||
if (logger_) {
|
||||
logger_->Trace("RenderCoordinatorService", "RenderFrame", "time=" + std::to_string(time), "Entering");
|
||||
}
|
||||
@@ -211,10 +219,14 @@ void RenderCoordinatorService::RenderFrameInternal(float time, const ViewState*
|
||||
ConfigureRenderGraphPasses();
|
||||
}
|
||||
|
||||
if (guiService_ && guiScriptService_ && guiScriptService_->HasGuiCommands()) {
|
||||
auto guiCommands = guiScriptService_->LoadGuiCommands();
|
||||
if (guiService_) {
|
||||
auto extent = graphicsService_->GetSwapchainExtent();
|
||||
guiService_->PrepareFrame(guiCommands, extent.first, extent.second);
|
||||
if (guiCommands) {
|
||||
guiService_->PrepareFrame(*guiCommands, extent.first, extent.second);
|
||||
} else if (guiScriptService_ && guiScriptService_->HasGuiCommands()) {
|
||||
auto scriptCommands = guiScriptService_->LoadGuiCommands();
|
||||
guiService_->PrepareFrame(scriptCommands, extent.first, extent.second);
|
||||
}
|
||||
}
|
||||
|
||||
if (useLuaScene && sceneScriptService_ && sceneService_) {
|
||||
|
||||
@@ -31,6 +31,9 @@ public:
|
||||
|
||||
void RenderFrame(float time) override;
|
||||
void RenderFrameWithViewState(float time, const ViewState& viewState) override;
|
||||
void RenderFrameWithOverrides(float time,
|
||||
const ViewState* viewState,
|
||||
const std::vector<GuiCommand>* guiCommands) override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -51,7 +54,9 @@ private:
|
||||
bool configFirstLogged_ = false;
|
||||
|
||||
void ConfigureRenderGraphPasses();
|
||||
void RenderFrameInternal(float time, const ViewState* overrideView);
|
||||
void RenderFrameInternal(float time,
|
||||
const ViewState* overrideView,
|
||||
const std::vector<GuiCommand>* guiCommands);
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
|
||||
34
src/services/impl/soundboard_path_resolver.cpp
Normal file
34
src/services/impl/soundboard_path_resolver.cpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#include "soundboard_path_resolver.hpp"
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindPackageRoot(const std::filesystem::path& seed) {
|
||||
std::filesystem::path current = seed;
|
||||
for (int depth = 0; depth < 6; ++depth) {
|
||||
const auto candidate = current / "packages" / "soundboard";
|
||||
if (std::filesystem::exists(candidate)) {
|
||||
return current;
|
||||
}
|
||||
if (!current.has_parent_path()) {
|
||||
break;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path ResolveSoundboardPackageRoot(const std::shared_ptr<IConfigService>& configService) {
|
||||
std::filesystem::path seed = std::filesystem::current_path();
|
||||
if (configService) {
|
||||
const auto scriptPath = configService->GetScriptPath();
|
||||
if (!scriptPath.empty()) {
|
||||
seed = scriptPath.parent_path();
|
||||
}
|
||||
}
|
||||
return FindPackageRoot(seed) / "packages" / "soundboard";
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
12
src/services/impl/soundboard_path_resolver.hpp
Normal file
12
src/services/impl/soundboard_path_resolver.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_config_service.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
std::filesystem::path ResolveSoundboardPackageRoot(const std::shared_ptr<IConfigService>& configService);
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -32,8 +32,16 @@ void WorkflowFrameRenderStep::Execute(const WorkflowStepDefinition& step, Workfl
|
||||
throw std::runtime_error("frame.render missing view_state input");
|
||||
}
|
||||
}
|
||||
if (viewState) {
|
||||
renderService_->RenderFrameWithViewState(static_cast<float>(*elapsed), *viewState);
|
||||
const std::vector<GuiCommand>* guiCommands = nullptr;
|
||||
auto guiIt = step.inputs.find("gui_commands");
|
||||
if (guiIt != step.inputs.end()) {
|
||||
guiCommands = context.TryGet<std::vector<GuiCommand>>(guiIt->second);
|
||||
if (!guiCommands) {
|
||||
throw std::runtime_error("frame.render missing gui_commands input");
|
||||
}
|
||||
}
|
||||
if (viewState || guiCommands) {
|
||||
renderService_->RenderFrameWithOverrides(static_cast<float>(*elapsed), viewState, guiCommands);
|
||||
} else {
|
||||
renderService_->RenderFrame(static_cast<float>(*elapsed));
|
||||
}
|
||||
|
||||
77
src/services/impl/workflow_soundboard_audio_step.cpp
Normal file
77
src/services/impl/workflow_soundboard_audio_step.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include "workflow_soundboard_audio_step.hpp"
|
||||
|
||||
#include "workflow_step_io_resolver.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
WorkflowSoundboardAudioStep::WorkflowSoundboardAudioStep(std::shared_ptr<IAudioService> audioService,
|
||||
std::shared_ptr<ISoundboardStateService> stateService,
|
||||
std::shared_ptr<ILogger> logger)
|
||||
: audioService_(std::move(audioService)),
|
||||
stateService_(std::move(stateService)),
|
||||
logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowSoundboardAudioStep::GetPluginId() const {
|
||||
return "soundboard.audio";
|
||||
}
|
||||
|
||||
void WorkflowSoundboardAudioStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
if (!audioService_) {
|
||||
throw std::runtime_error("soundboard.audio requires an IAudioService");
|
||||
}
|
||||
|
||||
WorkflowStepIoResolver resolver;
|
||||
const std::string selectionKey = resolver.GetRequiredInputKey(step, "selection");
|
||||
const std::string statusKey = resolver.GetRequiredOutputKey(step, "status");
|
||||
|
||||
const auto* selection = context.TryGet<SoundboardSelection>(selectionKey);
|
||||
if (!selection) {
|
||||
throw std::runtime_error("soundboard.audio missing selection input");
|
||||
}
|
||||
|
||||
std::string status = stateService_
|
||||
? stateService_->GetStatusMessage()
|
||||
: std::string("Select a clip to play");
|
||||
|
||||
if (selection->hasSelection && selection->requestId != lastRequestId_) {
|
||||
lastRequestId_ = selection->requestId;
|
||||
const std::filesystem::path path = selection->path;
|
||||
if (path.empty()) {
|
||||
status = "Audio path missing for selection";
|
||||
if (logger_) {
|
||||
logger_->Error("WorkflowSoundboardAudioStep::Execute: selection path missing");
|
||||
}
|
||||
} else if (!std::filesystem::exists(path)) {
|
||||
status = "Audio file not found: " + path.string();
|
||||
if (logger_) {
|
||||
logger_->Error("WorkflowSoundboardAudioStep::Execute: audio file not found " + path.string());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
audioService_->PlayEffect(path, false);
|
||||
status = "Playing \"" + selection->label + "\"";
|
||||
if (logger_) {
|
||||
logger_->Trace("WorkflowSoundboardAudioStep", "Execute",
|
||||
"clip=" + selection->label,
|
||||
"Audio playback dispatched");
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
status = "Failed to play \"" + selection->label + "\": " + ex.what();
|
||||
if (logger_) {
|
||||
logger_->Error("WorkflowSoundboardAudioStep::Execute: " + status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stateService_) {
|
||||
stateService_->SetStatusMessage(status);
|
||||
}
|
||||
}
|
||||
|
||||
context.Set(statusKey, status);
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
30
src/services/impl/workflow_soundboard_audio_step.hpp
Normal file
30
src/services/impl/workflow_soundboard_audio_step.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_audio_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_soundboard_state_service.hpp"
|
||||
#include "../interfaces/i_workflow_step.hpp"
|
||||
#include "../interfaces/soundboard_types.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class WorkflowSoundboardAudioStep final : public IWorkflowStep {
|
||||
public:
|
||||
WorkflowSoundboardAudioStep(std::shared_ptr<IAudioService> audioService,
|
||||
std::shared_ptr<ISoundboardStateService> stateService,
|
||||
std::shared_ptr<ILogger> logger);
|
||||
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<IAudioService> audioService_;
|
||||
std::shared_ptr<ISoundboardStateService> stateService_;
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::uint64_t lastRequestId_ = 0;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
149
src/services/impl/workflow_soundboard_catalog_scan_step.cpp
Normal file
149
src/services/impl/workflow_soundboard_catalog_scan_step.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "workflow_soundboard_catalog_scan_step.hpp"
|
||||
|
||||
#include "json_config_document_parser.hpp"
|
||||
#include "soundboard_path_resolver.hpp"
|
||||
#include "workflow_step_io_resolver.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
namespace {
|
||||
|
||||
std::string ToLower(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string PrettyClipName(const std::string& fileName) {
|
||||
std::string base = fileName;
|
||||
const auto dot = base.find_last_of('.');
|
||||
if (dot != std::string::npos) {
|
||||
base = base.substr(0, dot);
|
||||
}
|
||||
for (char& ch : base) {
|
||||
if (ch == '_' || ch == '-') {
|
||||
ch = ' ';
|
||||
}
|
||||
}
|
||||
bool capitalize = true;
|
||||
for (char& ch : base) {
|
||||
if (std::isspace(static_cast<unsigned char>(ch))) {
|
||||
capitalize = true;
|
||||
} else if (capitalize) {
|
||||
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||||
capitalize = false;
|
||||
} else {
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
std::vector<SoundboardClip> LoadClips(const std::filesystem::path& directory) {
|
||||
std::vector<SoundboardClip> clips;
|
||||
if (!std::filesystem::exists(directory)) {
|
||||
return clips;
|
||||
}
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
const auto extension = ToLower(entry.path().extension().string());
|
||||
if (extension != ".ogg" && extension != ".wav") {
|
||||
continue;
|
||||
}
|
||||
const std::string fileName = entry.path().filename().string();
|
||||
SoundboardClip clip{};
|
||||
clip.id = fileName;
|
||||
clip.label = PrettyClipName(fileName);
|
||||
clip.path = entry.path();
|
||||
clips.push_back(std::move(clip));
|
||||
}
|
||||
std::sort(clips.begin(), clips.end(),
|
||||
[](const SoundboardClip& a, const SoundboardClip& b) {
|
||||
return ToLower(a.id) < ToLower(b.id);
|
||||
});
|
||||
return clips;
|
||||
}
|
||||
|
||||
SoundboardCatalog BuildCatalog(const rapidjson::Document& document,
|
||||
const std::filesystem::path& packageRoot) {
|
||||
if (!document.HasMember("categories") || !document["categories"].IsArray()) {
|
||||
throw std::runtime_error("soundboard catalog requires a categories array");
|
||||
}
|
||||
SoundboardCatalog catalog{};
|
||||
catalog.packageRoot = packageRoot;
|
||||
const auto& categories = document["categories"];
|
||||
catalog.categories.reserve(categories.Size());
|
||||
for (const auto& categoryValue : categories.GetArray()) {
|
||||
if (!categoryValue.IsObject()) {
|
||||
throw std::runtime_error("soundboard catalog categories must be objects");
|
||||
}
|
||||
if (!categoryValue.HasMember("id") || !categoryValue["id"].IsString()) {
|
||||
throw std::runtime_error("soundboard catalog category requires string id");
|
||||
}
|
||||
if (!categoryValue.HasMember("name") || !categoryValue["name"].IsString()) {
|
||||
throw std::runtime_error("soundboard catalog category requires string name");
|
||||
}
|
||||
if (!categoryValue.HasMember("path") || !categoryValue["path"].IsString()) {
|
||||
throw std::runtime_error("soundboard catalog category requires string path");
|
||||
}
|
||||
SoundboardCategory category{};
|
||||
category.id = categoryValue["id"].GetString();
|
||||
category.name = categoryValue["name"].GetString();
|
||||
category.basePath = packageRoot / categoryValue["path"].GetString();
|
||||
category.clips = LoadClips(category.basePath);
|
||||
catalog.categories.push_back(std::move(category));
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WorkflowSoundboardCatalogScanStep::WorkflowSoundboardCatalogScanStep(
|
||||
std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ILogger> logger)
|
||||
: configService_(std::move(configService)),
|
||||
logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowSoundboardCatalogScanStep::GetPluginId() const {
|
||||
return "soundboard.catalog.scan";
|
||||
}
|
||||
|
||||
void WorkflowSoundboardCatalogScanStep::Execute(const WorkflowStepDefinition& step,
|
||||
WorkflowContext& context) {
|
||||
WorkflowStepIoResolver resolver;
|
||||
const std::string outputKey = resolver.GetRequiredOutputKey(step, "catalog");
|
||||
|
||||
if (!cachedCatalog_) {
|
||||
cachedCatalog_ = LoadCatalog();
|
||||
if (logger_) {
|
||||
std::size_t clipCount = 0;
|
||||
for (const auto& category : cachedCatalog_->categories) {
|
||||
clipCount += category.clips.size();
|
||||
}
|
||||
logger_->Trace("WorkflowSoundboardCatalogScanStep", "Execute",
|
||||
"categories=" + std::to_string(cachedCatalog_->categories.size()) +
|
||||
", clips=" + std::to_string(clipCount),
|
||||
"Catalog scanned");
|
||||
}
|
||||
}
|
||||
|
||||
context.Set(outputKey, *cachedCatalog_);
|
||||
}
|
||||
|
||||
SoundboardCatalog WorkflowSoundboardCatalogScanStep::LoadCatalog() const {
|
||||
const std::filesystem::path packageRoot = ResolveSoundboardPackageRoot(configService_);
|
||||
const std::filesystem::path catalogPath = packageRoot / "assets" / "audio_catalog.json";
|
||||
json_config::JsonConfigDocumentParser parser;
|
||||
auto document = parser.Parse(catalogPath, "soundboard audio catalog");
|
||||
return BuildCatalog(document, packageRoot);
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
29
src/services/impl/workflow_soundboard_catalog_scan_step.hpp
Normal file
29
src/services/impl/workflow_soundboard_catalog_scan_step.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_config_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_workflow_step.hpp"
|
||||
#include "../interfaces/soundboard_types.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class WorkflowSoundboardCatalogScanStep final : public IWorkflowStep {
|
||||
public:
|
||||
WorkflowSoundboardCatalogScanStep(std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ILogger> logger);
|
||||
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
|
||||
private:
|
||||
SoundboardCatalog LoadCatalog() const;
|
||||
|
||||
std::shared_ptr<IConfigService> configService_;
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::optional<SoundboardCatalog> cachedCatalog_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
334
src/services/impl/workflow_soundboard_gui_step.cpp
Normal file
334
src/services/impl/workflow_soundboard_gui_step.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "workflow_soundboard_gui_step.hpp"
|
||||
|
||||
#include "json_config_document_parser.hpp"
|
||||
#include "soundboard_path_resolver.hpp"
|
||||
#include "workflow_step_io_resolver.hpp"
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
namespace {
|
||||
|
||||
GuiColor ReadColor(const rapidjson::Value& value, const GuiColor& fallback) {
|
||||
if (!value.IsArray() || (value.Size() != 3 && value.Size() != 4)) {
|
||||
return fallback;
|
||||
}
|
||||
GuiColor color = fallback;
|
||||
color.r = static_cast<float>(value[0].GetDouble());
|
||||
color.g = static_cast<float>(value[1].GetDouble());
|
||||
color.b = static_cast<float>(value[2].GetDouble());
|
||||
color.a = value.Size() == 4 ? static_cast<float>(value[3].GetDouble()) : fallback.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
void ReadColorField(const rapidjson::Value& object, const char* name, GuiColor& target) {
|
||||
if (!object.HasMember(name)) {
|
||||
return;
|
||||
}
|
||||
const auto& value = object[name];
|
||||
if (!value.IsArray()) {
|
||||
throw std::runtime_error(std::string("soundboard.gui: '") + name + "' must be an array");
|
||||
}
|
||||
target = ReadColor(value, target);
|
||||
}
|
||||
|
||||
bool ReadFloatField(const rapidjson::Value& object, const char* name, float& target) {
|
||||
if (!object.HasMember(name)) {
|
||||
return false;
|
||||
}
|
||||
const auto& value = object[name];
|
||||
if (!value.IsNumber()) {
|
||||
throw std::runtime_error(std::string("soundboard.gui: '") + name + "' must be a number");
|
||||
}
|
||||
target = static_cast<float>(value.GetDouble());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadIntField(const rapidjson::Value& object, const char* name, int& target) {
|
||||
if (!object.HasMember(name)) {
|
||||
return false;
|
||||
}
|
||||
const auto& value = object[name];
|
||||
if (!value.IsInt()) {
|
||||
throw std::runtime_error(std::string("soundboard.gui: '") + name + "' must be an integer");
|
||||
}
|
||||
target = value.GetInt();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadStringField(const rapidjson::Value& object, const char* name, std::string& target) {
|
||||
if (!object.HasMember(name)) {
|
||||
return false;
|
||||
}
|
||||
const auto& value = object[name];
|
||||
if (!value.IsString()) {
|
||||
throw std::runtime_error(std::string("soundboard.gui: '") + name + "' must be a string");
|
||||
}
|
||||
target = value.GetString();
|
||||
return true;
|
||||
}
|
||||
|
||||
GuiCommand BuildRectCommand(const GuiCommand::RectData& rect, const GuiColor& color,
|
||||
const GuiColor& border, float borderWidth) {
|
||||
GuiCommand command{};
|
||||
command.type = GuiCommand::Type::Rect;
|
||||
command.rect = rect;
|
||||
command.color = color;
|
||||
command.borderColor = border;
|
||||
command.borderWidth = borderWidth;
|
||||
return command;
|
||||
}
|
||||
|
||||
GuiCommand BuildTextCommand(const std::string& text, float x, float y, float fontSize,
|
||||
const GuiColor& color, const std::string& alignX,
|
||||
const std::string& alignY) {
|
||||
GuiCommand command{};
|
||||
command.type = GuiCommand::Type::Text;
|
||||
command.text = text;
|
||||
command.rect.x = x;
|
||||
command.rect.y = y;
|
||||
command.fontSize = fontSize;
|
||||
command.color = color;
|
||||
command.alignX = alignX;
|
||||
command.alignY = alignY;
|
||||
return command;
|
||||
}
|
||||
|
||||
bool IsMouseOver(const GuiCommand::RectData& rect, float x, float y) {
|
||||
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WorkflowSoundboardGuiStep::WorkflowSoundboardGuiStep(std::shared_ptr<IInputService> inputService,
|
||||
std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ISoundboardStateService> stateService,
|
||||
std::shared_ptr<ILogger> logger)
|
||||
: inputService_(std::move(inputService)),
|
||||
configService_(std::move(configService)),
|
||||
stateService_(std::move(stateService)),
|
||||
logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowSoundboardGuiStep::GetPluginId() const {
|
||||
return "soundboard.gui";
|
||||
}
|
||||
|
||||
void WorkflowSoundboardGuiStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
if (!inputService_) {
|
||||
throw std::runtime_error("soundboard.gui requires an IInputService");
|
||||
}
|
||||
EnsureConfigLoaded();
|
||||
|
||||
WorkflowStepIoResolver resolver;
|
||||
const std::string catalogKey = resolver.GetRequiredInputKey(step, "catalog");
|
||||
const std::string selectionKey = resolver.GetRequiredOutputKey(step, "selection");
|
||||
const std::string commandsKey = resolver.GetRequiredOutputKey(step, "gui_commands");
|
||||
|
||||
const auto* catalog = context.TryGet<SoundboardCatalog>(catalogKey);
|
||||
if (!catalog) {
|
||||
throw std::runtime_error("soundboard.gui missing catalog input");
|
||||
}
|
||||
|
||||
const std::string status = stateService_
|
||||
? stateService_->GetStatusMessage()
|
||||
: std::string("Select a clip to play");
|
||||
|
||||
std::optional<SoundboardSelection> selection;
|
||||
std::vector<GuiCommand> commands = BuildCommands(*catalog, *cachedConfig_, status, selection);
|
||||
const std::size_t commandCount = commands.size();
|
||||
|
||||
SoundboardSelection output{};
|
||||
if (selection) {
|
||||
output = *selection;
|
||||
if (logger_) {
|
||||
logger_->Trace("WorkflowSoundboardGuiStep", "Execute",
|
||||
"selection=" + output.label +
|
||||
", requestId=" + std::to_string(output.requestId),
|
||||
"Soundboard selection updated");
|
||||
}
|
||||
}
|
||||
context.Set(selectionKey, output);
|
||||
context.Set(commandsKey, std::move(commands));
|
||||
if (logger_) {
|
||||
logger_->Trace("WorkflowSoundboardGuiStep", "Execute",
|
||||
"commands=" + std::to_string(commandCount),
|
||||
"GUI commands prepared");
|
||||
}
|
||||
}
|
||||
|
||||
void WorkflowSoundboardGuiStep::EnsureConfigLoaded() {
|
||||
if (!cachedConfig_) {
|
||||
cachedConfig_ = LoadConfig();
|
||||
}
|
||||
}
|
||||
|
||||
WorkflowSoundboardGuiStep::SoundboardGuiConfig WorkflowSoundboardGuiStep::LoadConfig() const {
|
||||
const std::filesystem::path packageRoot = ResolveSoundboardPackageRoot(configService_);
|
||||
const std::filesystem::path guiPath = packageRoot / "assets" / "soundboard_gui.json";
|
||||
json_config::JsonConfigDocumentParser parser;
|
||||
auto document = parser.Parse(guiPath, "soundboard gui config");
|
||||
|
||||
SoundboardGuiConfig config{};
|
||||
if (document.HasMember("panel") && document["panel"].IsObject()) {
|
||||
const auto& panel = document["panel"];
|
||||
ReadStringField(panel, "title", config.title);
|
||||
ReadStringField(panel, "description", config.description);
|
||||
if (panel.HasMember("rect") && panel["rect"].IsObject()) {
|
||||
const auto& rect = panel["rect"];
|
||||
ReadFloatField(rect, "x", config.panelRect.x);
|
||||
ReadFloatField(rect, "y", config.panelRect.y);
|
||||
ReadFloatField(rect, "width", config.panelRect.width);
|
||||
ReadFloatField(rect, "height", config.panelRect.height);
|
||||
}
|
||||
if (panel.HasMember("padding") && panel["padding"].IsObject()) {
|
||||
const auto& padding = panel["padding"];
|
||||
ReadFloatField(padding, "x", config.panelPaddingX);
|
||||
ReadFloatField(padding, "y", config.panelPaddingY);
|
||||
}
|
||||
if (panel.HasMember("colors") && panel["colors"].IsObject()) {
|
||||
const auto& colors = panel["colors"];
|
||||
ReadColorField(colors, "background", config.panelColor);
|
||||
ReadColorField(colors, "border", config.panelBorder);
|
||||
ReadColorField(colors, "title", config.titleColor);
|
||||
ReadColorField(colors, "description", config.descriptionColor);
|
||||
ReadColorField(colors, "category", config.categoryColor);
|
||||
ReadColorField(colors, "status", config.statusColor);
|
||||
}
|
||||
}
|
||||
if (document.HasMember("buttons") && document["buttons"].IsObject()) {
|
||||
const auto& buttons = document["buttons"];
|
||||
ReadFloatField(buttons, "width", config.buttonWidth);
|
||||
ReadFloatField(buttons, "height", config.buttonHeight);
|
||||
ReadFloatField(buttons, "spacing", config.buttonSpacing);
|
||||
if (buttons.HasMember("colors") && buttons["colors"].IsObject()) {
|
||||
const auto& colors = buttons["colors"];
|
||||
ReadColorField(colors, "text", config.buttonTextColor);
|
||||
ReadColorField(colors, "border", config.buttonBorder);
|
||||
ReadColorField(colors, "background", config.buttonBackground);
|
||||
ReadColorField(colors, "hover", config.buttonHover);
|
||||
ReadColorField(colors, "active", config.buttonActive);
|
||||
}
|
||||
}
|
||||
if (document.HasMember("layout") && document["layout"].IsObject()) {
|
||||
const auto& layout = document["layout"];
|
||||
ReadIntField(layout, "columns", config.columns);
|
||||
ReadFloatField(layout, "columnSpacing", config.columnSpacing);
|
||||
ReadFloatField(layout, "initialY", config.columnStartY);
|
||||
ReadFloatField(layout, "statusOffsetY", config.statusOffsetY);
|
||||
}
|
||||
if (document.HasMember("typography") && document["typography"].IsObject()) {
|
||||
const auto& type = document["typography"];
|
||||
ReadFloatField(type, "titleSize", config.titleFontSize);
|
||||
ReadFloatField(type, "descriptionSize", config.descriptionFontSize);
|
||||
ReadFloatField(type, "categorySize", config.categoryFontSize);
|
||||
ReadFloatField(type, "buttonSize", config.buttonFontSize);
|
||||
ReadFloatField(type, "statusSize", config.statusFontSize);
|
||||
ReadFloatField(type, "headerSpacing", config.headerSpacing);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
std::vector<GuiCommand> WorkflowSoundboardGuiStep::BuildCommands(
|
||||
const SoundboardCatalog& catalog,
|
||||
const SoundboardGuiConfig& config,
|
||||
const std::string& statusMessage,
|
||||
std::optional<SoundboardSelection>& selectionOut) {
|
||||
std::vector<GuiCommand> commands;
|
||||
|
||||
const auto& rect = config.panelRect;
|
||||
commands.push_back(BuildRectCommand(rect, config.panelColor, config.panelBorder, 1.0f));
|
||||
|
||||
const float titleX = rect.x + config.panelPaddingX;
|
||||
const float titleY = rect.y + config.panelPaddingY;
|
||||
commands.push_back(BuildTextCommand(config.title, titleX, titleY, config.titleFontSize,
|
||||
config.titleColor, "left", "top"));
|
||||
commands.push_back(BuildTextCommand(config.description, titleX,
|
||||
titleY + config.headerSpacing,
|
||||
config.descriptionFontSize, config.descriptionColor,
|
||||
"left", "top"));
|
||||
|
||||
const auto inputState = inputService_->GetState();
|
||||
const float mouseX = inputState.mouseX;
|
||||
const float mouseY = inputState.mouseY;
|
||||
const bool mouseDown = inputService_->IsMouseButtonPressed(SDL_BUTTON_LEFT);
|
||||
const bool justPressed = mouseDown && !lastMouseDown_;
|
||||
const bool justReleased = !mouseDown && lastMouseDown_;
|
||||
|
||||
const float columnStartY = rect.y + config.columnStartY;
|
||||
int columnCount = std::max(1, config.columns);
|
||||
for (size_t index = 0; index < catalog.categories.size(); ++index) {
|
||||
const SoundboardCategory& category = catalog.categories[index];
|
||||
int columnIndex = static_cast<int>(index % static_cast<size_t>(columnCount));
|
||||
float columnX = rect.x + config.panelPaddingX +
|
||||
columnIndex * (config.buttonWidth + config.columnSpacing);
|
||||
commands.push_back(BuildTextCommand(category.name, columnX, columnStartY,
|
||||
config.categoryFontSize, config.categoryColor,
|
||||
"left", "top"));
|
||||
|
||||
float buttonY = columnStartY + config.categoryFontSize + config.buttonSpacing;
|
||||
if (category.clips.empty()) {
|
||||
commands.push_back(BuildTextCommand("No clips available", columnX, buttonY,
|
||||
config.buttonFontSize, config.descriptionColor,
|
||||
"left", "top"));
|
||||
continue;
|
||||
}
|
||||
for (const auto& clip : category.clips) {
|
||||
GuiCommand::RectData buttonRect{columnX, buttonY, config.buttonWidth, config.buttonHeight};
|
||||
const std::string widgetId = category.id + "::" + clip.id;
|
||||
const bool hovered = IsMouseOver(buttonRect, mouseX, mouseY);
|
||||
if (justPressed && hovered) {
|
||||
activeWidget_ = widgetId;
|
||||
}
|
||||
bool clicked = false;
|
||||
if (justReleased && hovered && activeWidget_ == widgetId) {
|
||||
clicked = true;
|
||||
}
|
||||
if (justReleased && activeWidget_ == widgetId) {
|
||||
activeWidget_.clear();
|
||||
}
|
||||
|
||||
GuiColor fill = config.buttonBackground;
|
||||
if (activeWidget_ == widgetId && mouseDown) {
|
||||
fill = config.buttonActive;
|
||||
} else if (hovered) {
|
||||
fill = config.buttonHover;
|
||||
}
|
||||
commands.push_back(BuildRectCommand(buttonRect, fill, config.buttonBorder, 1.0f));
|
||||
const float textX = buttonRect.x + (buttonRect.width * 0.5f);
|
||||
const float textY = buttonRect.y + (buttonRect.height * 0.5f);
|
||||
commands.push_back(BuildTextCommand(clip.label, textX, textY, config.buttonFontSize,
|
||||
config.buttonTextColor, "center", "center"));
|
||||
|
||||
if (clicked) {
|
||||
SoundboardSelection selection{};
|
||||
selection.hasSelection = true;
|
||||
selection.requestId = nextRequestId_++;
|
||||
selection.categoryId = category.id;
|
||||
selection.clipId = clip.id;
|
||||
selection.label = clip.label;
|
||||
selection.path = clip.path;
|
||||
selectionOut = selection;
|
||||
if (stateService_) {
|
||||
stateService_->SetStatusMessage("Playing \"" + clip.label + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
buttonY += config.buttonHeight + config.buttonSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
const float statusY = rect.y + rect.height - config.statusOffsetY;
|
||||
commands.push_back(BuildTextCommand(statusMessage, rect.x + config.panelPaddingX,
|
||||
statusY, config.statusFontSize,
|
||||
config.statusColor, "left", "center"));
|
||||
|
||||
lastMouseDown_ = mouseDown;
|
||||
return commands;
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
79
src/services/impl/workflow_soundboard_gui_step.hpp
Normal file
79
src/services/impl/workflow_soundboard_gui_step.hpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/gui_types.hpp"
|
||||
#include "../interfaces/i_config_service.hpp"
|
||||
#include "../interfaces/i_input_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_soundboard_state_service.hpp"
|
||||
#include "../interfaces/i_workflow_step.hpp"
|
||||
#include "../interfaces/soundboard_types.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class WorkflowSoundboardGuiStep final : public IWorkflowStep {
|
||||
public:
|
||||
WorkflowSoundboardGuiStep(std::shared_ptr<IInputService> inputService,
|
||||
std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ISoundboardStateService> stateService,
|
||||
std::shared_ptr<ILogger> logger);
|
||||
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
|
||||
private:
|
||||
struct SoundboardGuiConfig {
|
||||
GuiCommand::RectData panelRect{16.0f, 16.0f, 664.0f, 520.0f};
|
||||
float panelPaddingX = 20.0f;
|
||||
float panelPaddingY = 20.0f;
|
||||
float headerSpacing = 26.0f;
|
||||
float columnStartY = 120.0f;
|
||||
float statusOffsetY = 34.0f;
|
||||
int columns = 2;
|
||||
float columnSpacing = 24.0f;
|
||||
float buttonWidth = 300.0f;
|
||||
float buttonHeight = 36.0f;
|
||||
float buttonSpacing = 12.0f;
|
||||
float titleFontSize = 24.0f;
|
||||
float descriptionFontSize = 14.0f;
|
||||
float categoryFontSize = 20.0f;
|
||||
float buttonFontSize = 16.0f;
|
||||
float statusFontSize = 14.0f;
|
||||
GuiColor panelColor{0.06f, 0.07f, 0.09f, 0.95f};
|
||||
GuiColor panelBorder{0.35f, 0.38f, 0.42f, 1.0f};
|
||||
GuiColor titleColor{0.96f, 0.96f, 0.97f, 1.0f};
|
||||
GuiColor descriptionColor{0.7f, 0.75f, 0.8f, 1.0f};
|
||||
GuiColor categoryColor{0.9f, 0.9f, 0.95f, 1.0f};
|
||||
GuiColor statusColor{0.6f, 0.8f, 1.0f, 1.0f};
|
||||
GuiColor buttonTextColor{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GuiColor buttonBorder{0.45f, 0.52f, 0.6f, 1.0f};
|
||||
GuiColor buttonBackground{0.2f, 0.24f, 0.28f, 1.0f};
|
||||
GuiColor buttonHover{0.26f, 0.3f, 0.36f, 1.0f};
|
||||
GuiColor buttonActive{0.16f, 0.22f, 0.28f, 1.0f};
|
||||
std::string title = "Audio Soundboard";
|
||||
std::string description = "Trigger SFX or speech clips from bundled audio assets.";
|
||||
};
|
||||
|
||||
SoundboardGuiConfig LoadConfig() const;
|
||||
void EnsureConfigLoaded();
|
||||
std::vector<GuiCommand> BuildCommands(const SoundboardCatalog& catalog,
|
||||
const SoundboardGuiConfig& config,
|
||||
const std::string& statusMessage,
|
||||
std::optional<SoundboardSelection>& selectionOut);
|
||||
|
||||
std::shared_ptr<IInputService> inputService_;
|
||||
std::shared_ptr<IConfigService> configService_;
|
||||
std::shared_ptr<ISoundboardStateService> stateService_;
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::optional<SoundboardGuiConfig> cachedConfig_;
|
||||
std::string activeWidget_;
|
||||
bool lastMouseDown_ = false;
|
||||
std::uint64_t nextRequestId_ = 1;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "graphics_types.hpp"
|
||||
#include "gui_types.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
@@ -10,6 +13,9 @@ public:
|
||||
|
||||
virtual void RenderFrame(float time) = 0;
|
||||
virtual void RenderFrameWithViewState(float time, const ViewState& viewState) = 0;
|
||||
virtual void RenderFrameWithOverrides(float time,
|
||||
const ViewState* viewState,
|
||||
const std::vector<GuiCommand>* guiCommands) = 0;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
|
||||
Reference in New Issue
Block a user