From eec94f3acad4ff74954e335741d8f467381b85f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Mon, 2 Mar 2026 13:10:04 -0500 Subject: [PATCH 1/5] exec: fix looping when playing backwards --- 3rdparty/libossia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/libossia b/3rdparty/libossia index d8ec87f0f9..8304bdf25d 160000 --- a/3rdparty/libossia +++ b/3rdparty/libossia @@ -1 +1 @@ -Subproject commit d8ec87f0f995bc2d4f04a5782f385f20479aed54 +Subproject commit 8304bdf25d12986c8388c2a8d142e543ec2b4004 From 2148f8219d3f6b4232589de3e136e045793a7161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Tue, 3 Mar 2026 12:27:12 -0500 Subject: [PATCH 2/5] clip launcher: init --- .../score-plugin-cliplauncher/CMakeLists.txt | 101 +++++ .../ClipLauncher/CellModel.cpp | 202 +++++++++ .../ClipLauncher/CellModel.hpp | 123 ++++++ .../ClipLauncher/CommandFactory.hpp | 20 + .../ClipLauncher/Commands/AddCell.cpp | 44 ++ .../ClipLauncher/Commands/AddCell.hpp | 35 ++ .../ClipLauncher/Commands/AddLane.cpp | 59 +++ .../ClipLauncher/Commands/AddLane.hpp | 34 ++ .../ClipLauncher/Commands/AddScene.cpp | 59 +++ .../ClipLauncher/Commands/AddScene.hpp | 34 ++ .../Commands/AddTransitionRule.cpp | 39 ++ .../Commands/AddTransitionRule.hpp | 34 ++ .../Commands/CellTriggerCommandFactory.cpp | 36 ++ .../Commands/CellTriggerCommandFactory.hpp | 24 + .../ClipLauncher/Commands/MoveCell.cpp | 48 ++ .../ClipLauncher/Commands/MoveCell.hpp | 35 ++ .../ClipLauncher/Commands/RemoveCell.cpp | 44 ++ .../ClipLauncher/Commands/RemoveCell.hpp | 32 ++ .../ClipLauncher/Commands/RemoveLane.cpp | 69 +++ .../ClipLauncher/Commands/RemoveLane.hpp | 35 ++ .../ClipLauncher/Commands/RemoveScene.cpp | 65 +++ .../ClipLauncher/Commands/RemoveScene.hpp | 35 ++ .../Commands/RemoveTransitionRule.cpp | 39 ++ .../Commands/RemoveTransitionRule.hpp | 34 ++ .../Commands/SetCellProperties.cpp | 104 +++++ .../Commands/SetCellProperties.hpp | 72 +++ .../Commands/SetLaneProperties.cpp | 73 +++ .../Commands/SetLaneProperties.hpp | 54 +++ .../Commands/SetSceneProperties.cpp | 40 ++ .../Commands/SetSceneProperties.hpp | 32 ++ .../Execution/ClipLauncherComponent.cpp | 290 ++++++++++++ .../Execution/ClipLauncherComponent.hpp | 82 ++++ .../Inspector/CellInspectorFactory.cpp | 22 + .../Inspector/CellInspectorFactory.hpp | 20 + .../Inspector/CellInspectorWidget.cpp | 219 +++++++++ .../Inspector/CellInspectorWidget.hpp | 31 ++ .../Inspector/LaneInspectorFactory.cpp | 22 + .../Inspector/LaneInspectorFactory.hpp | 20 + .../Inspector/LaneInspectorWidget.cpp | 68 +++ .../Inspector/LaneInspectorWidget.hpp | 22 + .../Inspector/SceneInspectorFactory.cpp | 22 + .../Inspector/SceneInspectorFactory.hpp | 20 + .../Inspector/SceneInspectorWidget.cpp | 48 ++ .../Inspector/SceneInspectorWidget.hpp | 22 + .../ClipLauncher/LaneModel.cpp | 102 +++++ .../ClipLauncher/LaneModel.hpp | 65 +++ .../ClipLauncher/Metadata.hpp | 24 + .../ClipLauncher/ProcessModel.cpp | 299 +++++++++++++ .../ClipLauncher/ProcessModel.hpp | 88 ++++ .../ClipLauncher/SceneModel.cpp | 54 +++ .../ClipLauncher/SceneModel.hpp | 39 ++ .../ClipLauncher/TransitionRule.hpp | 132 ++++++ .../ClipLauncher/Types.hpp | 110 +++++ .../View/CellDisplayedElementsProvider.cpp | 60 +++ .../View/CellDisplayedElementsProvider.hpp | 21 + .../View/ClipLauncherPresenter.cpp | 294 ++++++++++++ .../View/ClipLauncherPresenter.hpp | 61 +++ .../ClipLauncher/View/ClipLauncherView.cpp | 418 ++++++++++++++++++ .../ClipLauncher/View/ClipLauncherView.hpp | 77 ++++ .../ClipLauncher/View/LayerFactory.cpp | 41 ++ .../ClipLauncher/View/LayerFactory.hpp | 24 + .../score_plugin_cliplauncher.cpp | 66 +++ .../score_plugin_cliplauncher.hpp | 35 ++ 63 files changed, 4573 insertions(+) create mode 100644 src/plugins/score-plugin-cliplauncher/CMakeLists.txt create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/CommandFactory.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Metadata.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/TransitionRule.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.hpp create mode 100644 src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.hpp diff --git a/src/plugins/score-plugin-cliplauncher/CMakeLists.txt b/src/plugins/score-plugin-cliplauncher/CMakeLists.txt new file mode 100644 index 0000000000..ada5129673 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/CMakeLists.txt @@ -0,0 +1,101 @@ +project(score_plugin_cliplauncher LANGUAGES CXX) + +# General initialization +score_common_setup() + +# Source files +set(HDRS + ClipLauncher/Types.hpp + ClipLauncher/TransitionRule.hpp + ClipLauncher/CommandFactory.hpp + ClipLauncher/Metadata.hpp + + ClipLauncher/LaneModel.hpp + ClipLauncher/SceneModel.hpp + ClipLauncher/CellModel.hpp + ClipLauncher/ProcessModel.hpp + + ClipLauncher/Commands/AddLane.hpp + ClipLauncher/Commands/RemoveLane.hpp + ClipLauncher/Commands/AddScene.hpp + ClipLauncher/Commands/RemoveScene.hpp + ClipLauncher/Commands/AddCell.hpp + ClipLauncher/Commands/RemoveCell.hpp + ClipLauncher/Commands/MoveCell.hpp + ClipLauncher/Commands/CellTriggerCommandFactory.hpp + ClipLauncher/Commands/SetCellProperties.hpp + ClipLauncher/Commands/SetLaneProperties.hpp + ClipLauncher/Commands/SetSceneProperties.hpp + ClipLauncher/Commands/AddTransitionRule.hpp + ClipLauncher/Commands/RemoveTransitionRule.hpp + + ClipLauncher/Execution/ClipLauncherComponent.hpp + + ClipLauncher/Inspector/CellInspectorWidget.hpp + ClipLauncher/Inspector/CellInspectorFactory.hpp + ClipLauncher/Inspector/LaneInspectorWidget.hpp + ClipLauncher/Inspector/LaneInspectorFactory.hpp + ClipLauncher/Inspector/SceneInspectorWidget.hpp + ClipLauncher/Inspector/SceneInspectorFactory.hpp + + ClipLauncher/View/ClipLauncherView.hpp + ClipLauncher/View/ClipLauncherPresenter.hpp + ClipLauncher/View/CellDisplayedElementsProvider.hpp + ClipLauncher/View/LayerFactory.hpp + + score_plugin_cliplauncher.hpp +) + +set(SRCS + ClipLauncher/LaneModel.cpp + ClipLauncher/SceneModel.cpp + ClipLauncher/CellModel.cpp + ClipLauncher/ProcessModel.cpp + + ClipLauncher/Commands/AddLane.cpp + ClipLauncher/Commands/RemoveLane.cpp + ClipLauncher/Commands/AddScene.cpp + ClipLauncher/Commands/RemoveScene.cpp + ClipLauncher/Commands/AddCell.cpp + ClipLauncher/Commands/RemoveCell.cpp + ClipLauncher/Commands/MoveCell.cpp + ClipLauncher/Commands/CellTriggerCommandFactory.cpp + ClipLauncher/Commands/SetCellProperties.cpp + ClipLauncher/Commands/SetLaneProperties.cpp + ClipLauncher/Commands/SetSceneProperties.cpp + ClipLauncher/Commands/AddTransitionRule.cpp + ClipLauncher/Commands/RemoveTransitionRule.cpp + + ClipLauncher/Execution/ClipLauncherComponent.cpp + + ClipLauncher/Inspector/CellInspectorWidget.cpp + ClipLauncher/Inspector/CellInspectorFactory.cpp + ClipLauncher/Inspector/LaneInspectorWidget.cpp + ClipLauncher/Inspector/LaneInspectorFactory.cpp + ClipLauncher/Inspector/SceneInspectorWidget.cpp + ClipLauncher/Inspector/SceneInspectorFactory.cpp + + ClipLauncher/View/ClipLauncherView.cpp + ClipLauncher/View/ClipLauncherPresenter.cpp + ClipLauncher/View/CellDisplayedElementsProvider.cpp + ClipLauncher/View/LayerFactory.cpp + + score_plugin_cliplauncher.cpp +) + +# Creation of the library +add_library(${PROJECT_NAME} ${SRCS} ${HDRS}) + +# Code generation +score_generate_command_list_file(${PROJECT_NAME} "${HDRS}") + +# Link +target_link_libraries(${PROJECT_NAME} + PUBLIC + score_plugin_scenario + score_plugin_engine + score_lib_inspector +) + +# Target-specific options +setup_score_plugin(${PROJECT_NAME}) diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.cpp new file mode 100644 index 0000000000..0b83cfc1a4 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.cpp @@ -0,0 +1,202 @@ +#include "CellModel.hpp" + +#include + +#include +#include +#include + +W_OBJECT_IMPL(ClipLauncher::CellModel) + +namespace ClipLauncher +{ + +CellModel::CellModel( + const Id& id, const score::DocumentContext& ctx, QObject* parent) + : score::Entity{id, "Cell", parent} + , Scenario::BaseScenarioContainer{ctx, this} +{ +} + +CellModel::CellModel( + DataStream::Deserializer& vis, const score::DocumentContext& ctx, QObject* parent) + : score::Entity{vis, parent} + , Scenario::BaseScenarioContainer{Scenario::BaseScenarioContainer::no_init{}, ctx, this} +{ + vis.writeTo(*this); +} + +CellModel::CellModel( + JSONObject::Deserializer& vis, const score::DocumentContext& ctx, QObject* parent) + : score::Entity{vis, parent} + , Scenario::BaseScenarioContainer{Scenario::BaseScenarioContainer::no_init{}, ctx, this} +{ + vis.writeTo(*this); +} + +CellModel::~CellModel() { } + +void CellModel::setLane(int l) +{ + if(m_lane != l) + { + m_lane = l; + laneChanged(l); + } +} + +void CellModel::setScene(int s) +{ + if(m_scene != s) + { + m_scene = s; + sceneChanged(s); + } +} + +void CellModel::setLaunchMode(LaunchMode m) +{ + if(m_launchMode != m) + { + m_launchMode = m; + launchModeChanged(m); + } +} + +void CellModel::setTriggerStyle(TriggerStyle s) +{ + if(m_triggerStyle != s) + { + m_triggerStyle = s; + triggerStyleChanged(s); + } +} + +void CellModel::setVelocity(double v) +{ + if(m_velocity != v) + { + m_velocity = v; + velocityChanged(v); + } +} + +void CellModel::addTransitionRule(TransitionRule rule) +{ + m_transitionRules.push_back(std::move(rule)); + transitionRulesChanged(); +} + +void CellModel::removeTransitionRule(int32_t ruleId) +{ + auto it + = std::find_if(m_transitionRules.begin(), m_transitionRules.end(), [ruleId](auto& r) { + return r.id == ruleId; + }); + if(it != m_transitionRules.end()) + { + m_transitionRules.erase(it); + transitionRulesChanged(); + } +} + +void CellModel::setCellState(CellState s) +{ + if(m_cellState != s) + { + m_cellState = s; + cellStateChanged(s); + } +} + +double CellModel::progress() const noexcept +{ + return interval().duration.playPercentage(); +} + +void CellModel::setLoopCount(int c) +{ + if(m_loopCount != c) + { + m_loopCount = c; + loopCountChanged(c); + } +} + +} // namespace ClipLauncher + +// Serialization +template <> +void DataStreamReader::read(const ClipLauncher::CellModel& cell) +{ + // Serialize the BaseScenarioContainer + readFrom(static_cast(cell)); + + // Serialize cell-specific data + m_stream << cell.m_lane << cell.m_scene << cell.m_launchMode << cell.m_triggerStyle + << cell.m_velocity; + + // Transition rules + m_stream << (int32_t)cell.m_transitionRules.size(); + for(const auto& rule : cell.m_transitionRules) + readFrom(rule); + + insertDelimiter(); +} + +template <> +void DataStreamWriter::write(ClipLauncher::CellModel& cell) +{ + // Deserialize the BaseScenarioContainer + writeTo(static_cast(cell)); + + // Deserialize cell-specific data + m_stream >> cell.m_lane >> cell.m_scene >> cell.m_launchMode >> cell.m_triggerStyle + >> cell.m_velocity; + + // Transition rules + int32_t ruleCount; + m_stream >> ruleCount; + cell.m_transitionRules.resize(ruleCount); + for(auto& rule : cell.m_transitionRules) + writeTo(rule); + + checkDelimiter(); +} + +template <> +void JSONReader::read(const ClipLauncher::CellModel& cell) +{ + // Serialize the BaseScenarioContainer + obj["Scenario"] = static_cast(cell); + + // Cell-specific data + obj["Lane"] = cell.m_lane; + obj["Scene"] = cell.m_scene; + obj["LaunchMode"] = cell.m_launchMode; + obj["TriggerStyle"] = cell.m_triggerStyle; + obj["Velocity"] = cell.m_velocity; + + // Transition rules - uses ArraySerializer via TSerializer + obj["TransitionRules"] = cell.m_transitionRules; +} + +template <> +void JSONWriter::write(ClipLauncher::CellModel& cell) +{ + // Deserialize the BaseScenarioContainer + { + JSONObject::Deserializer sub{obj["Scenario"]}; + sub.writeTo(static_cast(cell)); + } + + // Cell-specific data + cell.m_lane = obj["Lane"].toInt(); + cell.m_scene = obj["Scene"].toInt(); + cell.m_launchMode <<= obj["LaunchMode"]; + cell.m_triggerStyle <<= obj["TriggerStyle"]; + cell.m_velocity = obj["Velocity"].toDouble(); + + // Transition rules + cell.m_transitionRules <<= obj["TransitionRules"]; +} diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.hpp new file mode 100644 index 0000000000..1b3f65322a --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/CellModel.hpp @@ -0,0 +1,123 @@ +#pragma once +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +namespace ClipLauncher +{ + +// CellModel inherits from BaseScenarioContainer (like BaseScenario does) +// so that interval.parent() can be dynamic_cast'd to ScenarioInterface*. +class CellModel final + : public score::Entity + , public Scenario::BaseScenarioContainer +{ + W_OBJECT(CellModel) + SCORE_SERIALIZE_FRIENDS + +public: + CellModel( + const Id& id, const score::DocumentContext& ctx, QObject* parent); + CellModel( + DataStream::Deserializer& vis, const score::DocumentContext& ctx, QObject* parent); + CellModel( + JSONObject::Deserializer& vis, const score::DocumentContext& ctx, QObject* parent); + + ~CellModel() override; + + Selectable selection{this}; + + // Access the BaseScenarioContainer part + Scenario::BaseScenarioContainer& scenarioContainer() noexcept { return *this; } + const Scenario::BaseScenarioContainer& scenarioContainer() const noexcept + { + return *this; + } + + // Convenience: the interval holding processes for this cell + using Scenario::BaseScenarioContainer::interval; + + // Grid position + int lane() const noexcept { return m_lane; } + void setLane(int l); + int scene() const noexcept { return m_scene; } + void setScene(int s); + + // Cell-specific properties + LaunchMode launchMode() const noexcept { return m_launchMode; } + void setLaunchMode(LaunchMode m); + + TriggerStyle triggerStyle() const noexcept { return m_triggerStyle; } + void setTriggerStyle(TriggerStyle s); + + double velocity() const noexcept { return m_velocity; } + void setVelocity(double v); + + // Transition rules + const std::vector& transitionRules() const noexcept + { + return m_transitionRules; + } + void addTransitionRule(TransitionRule rule); + void removeTransitionRule(int32_t ruleId); + + // Runtime state (not serialized) + CellState cellState() const noexcept { return m_cellState; } + void setCellState(CellState s); + double progress() const noexcept; + int loopCount() const noexcept { return m_loopCount; } + void setLoopCount(int c); + + // Signals + void laneChanged(int l) E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, laneChanged, l) + void sceneChanged(int s) E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, sceneChanged, s) + void launchModeChanged(ClipLauncher::LaunchMode m) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, launchModeChanged, m) + void triggerStyleChanged(ClipLauncher::TriggerStyle s) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, triggerStyleChanged, s) + void velocityChanged(double v) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, velocityChanged, v) + void cellStateChanged(ClipLauncher::CellState s) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, cellStateChanged, s) + void loopCountChanged(int c) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, loopCountChanged, c) + void transitionRulesChanged() + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, transitionRulesChanged) + +private: + int m_lane{0}; + int m_scene{0}; + LaunchMode m_launchMode{LaunchMode::Immediate}; + TriggerStyle m_triggerStyle{TriggerStyle::Trigger}; + double m_velocity{1.0}; + + std::vector m_transitionRules; + + // Runtime state (not serialized) + CellState m_cellState{CellState::Stopped}; + int m_loopCount{0}; +}; + +// intervalsBeforeTimeSync overload for CellModel (needed by AddTrigger/RemoveTrigger templates) +inline const QVector> +intervalsBeforeTimeSync(const CellModel& cell, const Id& timeSyncId) +{ + if(timeSyncId == cell.endTimeSync().id()) + return {cell.interval().id()}; + return {}; +} + +} // namespace ClipLauncher + +DEFAULT_MODEL_METADATA(ClipLauncher::CellModel, "Cell") +UNDO_NAME_METADATA(EMPTY_MACRO, ClipLauncher::CellModel, "Cell") diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/CommandFactory.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/CommandFactory.hpp new file mode 100644 index 0000000000..739b5bd303 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/CommandFactory.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +namespace ClipLauncher +{ +class CellModel; + +inline const CommandGroupKey& CommandFactoryName() +{ + static const CommandGroupKey key{"ClipLauncher"}; + return key; +} +} + +// Specialization for AddTrigger/RemoveTrigger templates +template <> +inline const CommandGroupKey& CommandFactoryName() +{ + return ClipLauncher::CommandFactoryName(); +} diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.cpp new file mode 100644 index 0000000000..c30324d27c --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.cpp @@ -0,0 +1,44 @@ +#include "AddCell.hpp" + +#include +#include +#include + +#include +#include + +namespace ClipLauncher +{ + +AddCell::AddCell(const ProcessModel& model, int lane, int scene) + : m_path{model} + , m_cellId{getStrongId(model.cells)} + , m_lane{lane} + , m_scene{scene} +{ +} + +void AddCell::undo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + proc.cells.remove(m_cellId); +} + +void AddCell::redo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + proc.cells.add( + ProcessModel::createDefaultCell(m_cellId, m_lane, m_scene, proc.context(), &proc)); +} + +void AddCell::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_lane << m_scene; +} + +void AddCell::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_lane >> m_scene; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.hpp new file mode 100644 index 0000000000..15ce15e591 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddCell.hpp @@ -0,0 +1,35 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +class AddCell final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), AddCell, "Add a cell") +public: + AddCell(const ProcessModel& model, int lane, int scene); + + const Id& cellId() const noexcept { return m_cellId; } + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + int m_lane{}; + int m_scene{}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.cpp new file mode 100644 index 0000000000..409f705d9f --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.cpp @@ -0,0 +1,59 @@ +#include "AddLane.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ + +AddLane::AddLane(const ProcessModel& model, int position) + : m_path{model} + , m_laneId{getStrongId(model.lanes)} + , m_position{position} +{ + // Pre-generate cell IDs for each scene + m_cellIds = getStrongIdRange(model.sceneCount(), model.cells); +} + +void AddLane::undo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + // Remove cells first + for(auto& cellId : m_cellIds) + proc.cells.remove(cellId); + proc.lanes.remove(m_laneId); +} + +void AddLane::redo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + auto lane = new LaneModel{m_laneId, &proc}; + lane->setName(QString("Lane %1").arg(m_position + 1)); + proc.lanes.add(lane); + + // Auto-create cells for every scene + int sceneIdx = 0; + for(auto& cellId : m_cellIds) + { + proc.cells.add(ProcessModel::createDefaultCell( + cellId, m_position, sceneIdx, proc.context(), &proc)); + sceneIdx++; + } +} + +void AddLane::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_laneId << m_cellIds << m_position; +} + +void AddLane::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_laneId >> m_cellIds >> m_position; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.hpp new file mode 100644 index 0000000000..a1b124adee --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddLane.hpp @@ -0,0 +1,34 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; +class LaneModel; + +class AddLane final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), AddLane, "Add a lane") +public: + AddLane(const ProcessModel& model, int position); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_laneId; + std::vector> m_cellIds; + int m_position{}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.cpp new file mode 100644 index 0000000000..80aa8d7599 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.cpp @@ -0,0 +1,59 @@ +#include "AddScene.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ + +AddScene::AddScene(const ProcessModel& model, int position) + : m_path{model} + , m_sceneId{getStrongId(model.scenes)} + , m_position{position} +{ + // Pre-generate cell IDs for each lane + m_cellIds = getStrongIdRange(model.laneCount(), model.cells); +} + +void AddScene::undo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + // Remove cells first + for(auto& cellId : m_cellIds) + proc.cells.remove(cellId); + proc.scenes.remove(m_sceneId); +} + +void AddScene::redo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + auto scene = new SceneModel{m_sceneId, &proc}; + scene->setName(QString("Scene %1").arg(m_position + 1)); + proc.scenes.add(scene); + + // Auto-create cells for every lane + int laneIdx = 0; + for(auto& cellId : m_cellIds) + { + proc.cells.add(ProcessModel::createDefaultCell( + cellId, laneIdx, m_position, proc.context(), &proc)); + laneIdx++; + } +} + +void AddScene::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_sceneId << m_cellIds << m_position; +} + +void AddScene::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_sceneId >> m_cellIds >> m_position; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.hpp new file mode 100644 index 0000000000..8f7567bff8 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddScene.hpp @@ -0,0 +1,34 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; +class SceneModel; + +class AddScene final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), AddScene, "Add a scene") +public: + AddScene(const ProcessModel& model, int position); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_sceneId; + std::vector> m_cellIds; + int m_position{}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.cpp new file mode 100644 index 0000000000..7adc02c614 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.cpp @@ -0,0 +1,39 @@ +#include "AddTransitionRule.hpp" + +#include + +#include +#include + +namespace ClipLauncher +{ + +AddTransitionRule::AddTransitionRule( + const ProcessModel& proc, const CellModel& cell, TransitionRule rule) + : m_path{proc} + , m_cellId{cell.id()} + , m_rule{std::move(rule)} +{ +} + +void AddTransitionRule::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).removeTransitionRule(m_rule.id); +} + +void AddTransitionRule::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).addTransitionRule(m_rule); +} + +void AddTransitionRule::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_rule; +} + +void AddTransitionRule::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_rule; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.hpp new file mode 100644 index 0000000000..6d8447e4b7 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/AddTransitionRule.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +class AddTransitionRule final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), AddTransitionRule, "Add transition rule") +public: + AddTransitionRule( + const ProcessModel& proc, const CellModel& cell, TransitionRule rule); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + TransitionRule m_rule; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.cpp new file mode 100644 index 0000000000..764c895b5d --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.cpp @@ -0,0 +1,36 @@ +#include "CellTriggerCommandFactory.hpp" + +#include +#include +#include + +#include +#include + +#include + +// Instantiate AddTrigger/RemoveTrigger for CellModel +template class Scenario::Command::AddTrigger; +template class Scenario::Command::RemoveTrigger; + +namespace ClipLauncher +{ + +bool CellTriggerCommandFactory::matches(const Scenario::TimeSyncModel& tn) const +{ + return dynamic_cast(tn.parent()) != nullptr; +} + +score::Command* +CellTriggerCommandFactory::make_addTriggerCommand(const Scenario::TimeSyncModel& tn) const +{ + return new Scenario::Command::AddTrigger{tn}; +} + +score::Command* +CellTriggerCommandFactory::make_removeTriggerCommand(const Scenario::TimeSyncModel& tn) const +{ + return new Scenario::Command::RemoveTrigger{Path{tn}}; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.hpp new file mode 100644 index 0000000000..2f2a01922c --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/CellTriggerCommandFactory.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +namespace ClipLauncher +{ + +class CellTriggerCommandFactory final + : public Scenario::Command::TriggerCommandFactory +{ + SCORE_CONCRETE("d4e5f6a7-8b9c-0d1e-2f3a-4b5c6d7e8f90") +public: + bool matches(const Scenario::TimeSyncModel& tn) const override; + score::Command* make_addTriggerCommand(const Scenario::TimeSyncModel& tn) const override; + score::Command* make_removeTriggerCommand(const Scenario::TimeSyncModel& tn) const override; +}; + +} // namespace ClipLauncher + +// Trigger command declarations for the command list generator +#include +#include +#include +SCORE_COMMAND_DECL_T(Scenario::Command::AddTrigger) +SCORE_COMMAND_DECL_T(Scenario::Command::RemoveTrigger) diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.cpp new file mode 100644 index 0000000000..e13b2bfd90 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.cpp @@ -0,0 +1,48 @@ +#include "MoveCell.hpp" + +#include + +#include +#include + +namespace ClipLauncher +{ + +MoveCell::MoveCell( + const ProcessModel& model, const CellModel& cell, int newLane, int newScene) + : m_path{model} + , m_cellId{cell.id()} + , m_oldLane{cell.lane()} + , m_oldScene{cell.scene()} + , m_newLane{newLane} + , m_newScene{newScene} +{ +} + +void MoveCell::undo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + auto& cell = proc.cells.at(m_cellId); + cell.setLane(m_oldLane); + cell.setScene(m_oldScene); +} + +void MoveCell::redo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + auto& cell = proc.cells.at(m_cellId); + cell.setLane(m_newLane); + cell.setScene(m_newScene); +} + +void MoveCell::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_oldLane << m_oldScene << m_newLane << m_newScene; +} + +void MoveCell::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_oldLane >> m_oldScene >> m_newLane >> m_newScene; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.hpp new file mode 100644 index 0000000000..3be6741353 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/MoveCell.hpp @@ -0,0 +1,35 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +class MoveCell final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), MoveCell, "Move a cell") +public: + MoveCell(const ProcessModel& model, const CellModel& cell, int newLane, int newScene); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + int m_oldLane{}; + int m_oldScene{}; + int m_newLane{}; + int m_newScene{}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.cpp new file mode 100644 index 0000000000..a33be84389 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.cpp @@ -0,0 +1,44 @@ +#include "RemoveCell.hpp" + +#include +#include +#include + +#include +#include + +namespace ClipLauncher +{ + +RemoveCell::RemoveCell(const ProcessModel& model, const CellModel& cell) + : m_path{model} + , m_cellId{cell.id()} + , m_cellData{score::marshall(cell)} +{ +} + +void RemoveCell::undo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + DataStream::Deserializer s{m_cellData}; + auto cell = new CellModel{s, proc.context(), &proc}; + proc.cells.add(cell); +} + +void RemoveCell::redo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + proc.cells.remove(m_cellId); +} + +void RemoveCell::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_cellData; +} + +void RemoveCell::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_cellData; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.hpp new file mode 100644 index 0000000000..b8a9f4e636 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveCell.hpp @@ -0,0 +1,32 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +class RemoveCell final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), RemoveCell, "Remove a cell") +public: + RemoveCell(const ProcessModel& model, const CellModel& cell); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + QByteArray m_cellData; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.cpp new file mode 100644 index 0000000000..62c7f7f683 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.cpp @@ -0,0 +1,69 @@ +#include "RemoveLane.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ + +RemoveLane::RemoveLane(const ProcessModel& model, const LaneModel& lane) + : m_path{model} + , m_laneId{lane.id()} + , m_laneData{score::marshall(lane)} +{ + // Also serialize all cells in this lane for undo + for(auto& cell : model.cells) + { + if(cell.lane() == lane.id().val()) + { + m_cellIds.push_back(cell.id()); + m_cellData.push_back(score::marshall(cell)); + } + } +} + +void RemoveLane::undo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + + // Restore lane + DataStream::Deserializer s{m_laneData}; + auto lane = new LaneModel{s, &proc}; + proc.lanes.add(lane); + + // Restore cells + for(const auto& data : m_cellData) + { + DataStream::Deserializer cs{data}; + auto cell = new CellModel{cs, proc.context(), &proc}; + proc.cells.add(cell); + } +} + +void RemoveLane::redo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + + // Remove cells first + for(const auto& cellId : m_cellIds) + proc.cells.remove(cellId); + + proc.lanes.remove(m_laneId); +} + +void RemoveLane::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_laneId << m_laneData << m_cellData << m_cellIds; +} + +void RemoveLane::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_laneId >> m_laneData >> m_cellData >> m_cellIds; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.hpp new file mode 100644 index 0000000000..8c464d1edd --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveLane.hpp @@ -0,0 +1,35 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; +class LaneModel; + +class RemoveLane final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), RemoveLane, "Remove a lane") +public: + RemoveLane(const ProcessModel& model, const LaneModel& lane); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_laneId; + QByteArray m_laneData; + std::vector m_cellData; // Cells in this lane, for undo + std::vector> m_cellIds; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.cpp new file mode 100644 index 0000000000..0543e4ffb5 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.cpp @@ -0,0 +1,65 @@ +#include "RemoveScene.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ + +RemoveScene::RemoveScene(const ProcessModel& model, const SceneModel& scene) + : m_path{model} + , m_sceneId{scene.id()} + , m_sceneData{score::marshall(scene)} +{ + for(auto& cell : model.cells) + { + if(cell.scene() == scene.id().val()) + { + m_cellIds.push_back(cell.id()); + m_cellData.push_back(score::marshall(cell)); + } + } +} + +void RemoveScene::undo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + + DataStream::Deserializer s{m_sceneData}; + auto scene = new SceneModel{s, &proc}; + proc.scenes.add(scene); + + for(const auto& data : m_cellData) + { + DataStream::Deserializer cs{data}; + auto cell = new CellModel{cs, proc.context(), &proc}; + proc.cells.add(cell); + } +} + +void RemoveScene::redo(const score::DocumentContext& ctx) const +{ + auto& proc = m_path.find(ctx); + + for(const auto& cellId : m_cellIds) + proc.cells.remove(cellId); + + proc.scenes.remove(m_sceneId); +} + +void RemoveScene::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_sceneId << m_sceneData << m_cellData << m_cellIds; +} + +void RemoveScene::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_sceneId >> m_sceneData >> m_cellData >> m_cellIds; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.hpp new file mode 100644 index 0000000000..d70c9400a0 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveScene.hpp @@ -0,0 +1,35 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; +class SceneModel; + +class RemoveScene final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), RemoveScene, "Remove a scene") +public: + RemoveScene(const ProcessModel& model, const SceneModel& scene); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_sceneId; + QByteArray m_sceneData; + std::vector m_cellData; + std::vector> m_cellIds; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.cpp new file mode 100644 index 0000000000..4331e199d7 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.cpp @@ -0,0 +1,39 @@ +#include "RemoveTransitionRule.hpp" + +#include + +#include +#include + +namespace ClipLauncher +{ + +RemoveTransitionRule::RemoveTransitionRule( + const ProcessModel& proc, const CellModel& cell, TransitionRule rule) + : m_path{proc} + , m_cellId{cell.id()} + , m_savedRule{std::move(rule)} +{ +} + +void RemoveTransitionRule::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).addTransitionRule(m_savedRule); +} + +void RemoveTransitionRule::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).removeTransitionRule(m_savedRule.id); +} + +void RemoveTransitionRule::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_savedRule; +} + +void RemoveTransitionRule::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_savedRule; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.hpp new file mode 100644 index 0000000000..262529de44 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/RemoveTransitionRule.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +class RemoveTransitionRule final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), RemoveTransitionRule, "Remove transition rule") +public: + RemoveTransitionRule( + const ProcessModel& proc, const CellModel& cell, TransitionRule rule); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + TransitionRule m_savedRule; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.cpp new file mode 100644 index 0000000000..0fb8868283 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.cpp @@ -0,0 +1,104 @@ +#include "SetCellProperties.hpp" + +#include + +#include +#include + +namespace ClipLauncher +{ + +// --- SetCellLaunchMode --- + +SetCellLaunchMode::SetCellLaunchMode( + const ProcessModel& proc, const CellModel& cell, LaunchMode newMode) + : m_path{proc} + , m_cellId{cell.id()} + , m_old{cell.launchMode()} + , m_new{newMode} +{ +} + +void SetCellLaunchMode::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).setLaunchMode(m_old); +} + +void SetCellLaunchMode::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).setLaunchMode(m_new); +} + +void SetCellLaunchMode::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_old << m_new; +} + +void SetCellLaunchMode::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_old >> m_new; +} + +// --- SetCellTriggerStyle --- + +SetCellTriggerStyle::SetCellTriggerStyle( + const ProcessModel& proc, const CellModel& cell, TriggerStyle newStyle) + : m_path{proc} + , m_cellId{cell.id()} + , m_old{cell.triggerStyle()} + , m_new{newStyle} +{ +} + +void SetCellTriggerStyle::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).setTriggerStyle(m_old); +} + +void SetCellTriggerStyle::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).setTriggerStyle(m_new); +} + +void SetCellTriggerStyle::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_old << m_new; +} + +void SetCellTriggerStyle::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_old >> m_new; +} + +// --- SetCellVelocity --- + +SetCellVelocity::SetCellVelocity( + const ProcessModel& proc, const CellModel& cell, double newVel) + : m_path{proc} + , m_cellId{cell.id()} + , m_old{cell.velocity()} + , m_new{newVel} +{ +} + +void SetCellVelocity::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).setVelocity(m_old); +} + +void SetCellVelocity::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).cells.at(m_cellId).setVelocity(m_new); +} + +void SetCellVelocity::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_cellId << m_old << m_new; +} + +void SetCellVelocity::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_cellId >> m_old >> m_new; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.hpp new file mode 100644 index 0000000000..af46bad86c --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetCellProperties.hpp @@ -0,0 +1,72 @@ +#pragma once +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +class SetCellLaunchMode final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetCellLaunchMode, "Set cell launch mode") +public: + SetCellLaunchMode(const ProcessModel& proc, const CellModel& cell, LaunchMode newMode); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + LaunchMode m_old, m_new; +}; + +class SetCellTriggerStyle final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetCellTriggerStyle, "Set cell trigger style") +public: + SetCellTriggerStyle( + const ProcessModel& proc, const CellModel& cell, TriggerStyle newStyle); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + TriggerStyle m_old, m_new; +}; + +class SetCellVelocity final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetCellVelocity, "Set cell velocity") +public: + SetCellVelocity(const ProcessModel& proc, const CellModel& cell, double newVel); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_cellId; + double m_old, m_new; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp new file mode 100644 index 0000000000..9468168981 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp @@ -0,0 +1,73 @@ +#include "SetLaneProperties.hpp" + +#include + +#include +#include + +namespace ClipLauncher +{ + +// --- SetLaneName --- + +SetLaneName::SetLaneName( + const ProcessModel& proc, const LaneModel& lane, QString newName) + : m_path{proc} + , m_laneId{lane.id()} + , m_old{lane.name()} + , m_new{std::move(newName)} +{ +} + +void SetLaneName::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setName(m_old); +} + +void SetLaneName::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setName(m_new); +} + +void SetLaneName::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_laneId << m_old << m_new; +} + +void SetLaneName::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_laneId >> m_old >> m_new; +} + +// --- SetLaneExclusivityMode --- + +SetLaneExclusivityMode::SetLaneExclusivityMode( + const ProcessModel& proc, const LaneModel& lane, ExclusivityMode newMode) + : m_path{proc} + , m_laneId{lane.id()} + , m_old{lane.exclusivityMode()} + , m_new{newMode} +{ +} + +void SetLaneExclusivityMode::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setExclusivityMode(m_old); +} + +void SetLaneExclusivityMode::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setExclusivityMode(m_new); +} + +void SetLaneExclusivityMode::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_laneId << m_old << m_new; +} + +void SetLaneExclusivityMode::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_laneId >> m_old >> m_new; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp new file mode 100644 index 0000000000..6c1ff9e16b --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp @@ -0,0 +1,54 @@ +#pragma once +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class LaneModel; +class ProcessModel; + +class SetLaneName final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetLaneName, "Set lane name") +public: + SetLaneName(const ProcessModel& proc, const LaneModel& lane, QString newName); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_laneId; + QString m_old, m_new; +}; + +class SetLaneExclusivityMode final : public score::Command +{ + SCORE_COMMAND_DECL( + CommandFactoryName(), SetLaneExclusivityMode, "Set lane exclusivity mode") +public: + SetLaneExclusivityMode( + const ProcessModel& proc, const LaneModel& lane, ExclusivityMode newMode); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_laneId; + ExclusivityMode m_old, m_new; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.cpp new file mode 100644 index 0000000000..c9630a1d81 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.cpp @@ -0,0 +1,40 @@ +#include "SetSceneProperties.hpp" + +#include + +#include +#include + +namespace ClipLauncher +{ + +SetSceneName::SetSceneName( + const ProcessModel& proc, const SceneModel& scene, QString newName) + : m_path{proc} + , m_sceneId{scene.id()} + , m_old{scene.name()} + , m_new{std::move(newName)} +{ +} + +void SetSceneName::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).scenes.at(m_sceneId).setName(m_old); +} + +void SetSceneName::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).scenes.at(m_sceneId).setName(m_new); +} + +void SetSceneName::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_sceneId << m_old << m_new; +} + +void SetSceneName::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_sceneId >> m_old >> m_new; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.hpp new file mode 100644 index 0000000000..a719d0b31e --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetSceneProperties.hpp @@ -0,0 +1,32 @@ +#pragma once +#include + +#include +#include +#include + +namespace ClipLauncher +{ +class SceneModel; +class ProcessModel; + +class SetSceneName final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetSceneName, "Set scene name") +public: + SetSceneName(const ProcessModel& proc, const SceneModel& scene, QString newName); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_sceneId; + QString m_old, m_new; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp new file mode 100644 index 0000000000..d133ce01f7 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp @@ -0,0 +1,290 @@ +#include "ClipLauncherComponent.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace ClipLauncher::Execution +{ + +ClipLauncherComponent::ClipLauncherComponent( + ClipLauncher::ProcessModel& element, const ::Execution::Context& ctx, + QObject* parent) + : ProcessComponent_T{element, ctx, "ClipLauncherComponent", parent} +{ + // Create the ossia::scenario that will contain all cell intervals + m_scenario = std::make_shared(); + m_ossia_process = m_scenario; + + // Setup execution structures for each existing cell + for(auto& cell : element.cells) + { + setupCell(cell); + } +} + +ClipLauncherComponent::~ClipLauncherComponent() { } + +void ClipLauncherComponent::cleanup() +{ + // Cleanup all cell components + for(auto& [id, data] : m_cells) + { + if(data.intervalComponent) + data.intervalComponent->cleanup(data.intervalComponent); + if(data.startStateComponent) + data.startStateComponent->cleanup(data.startStateComponent); + if(data.endStateComponent) + data.endStateComponent->cleanup(data.endStateComponent); + if(data.startEventComponent) + data.startEventComponent->cleanup(data.startEventComponent); + if(data.endEventComponent) + data.endEventComponent->cleanup(data.endEventComponent); + if(data.startSyncComponent) + data.startSyncComponent->cleanup(data.startSyncComponent); + if(data.endSyncComponent) + data.endSyncComponent->cleanup(data.endSyncComponent); + } + + m_cells.clear(); + m_activeCellPerLane.clear(); + m_ossia_process.reset(); + m_scenario.reset(); +} + +void ClipLauncherComponent::setupCell(CellModel& cell) +{ + CellExecData data; + auto& scenario = cell.scenarioContainer(); + + // Create ossia time_syncs + data.startSync = std::make_shared(); + data.endSync = std::make_shared(); + + // Create events on the syncs + data.startEvent = *data.startSync->emplace( + data.startSync->get_time_events().begin(), [](auto&&...) {}, + ossia::expressions::make_expression_true()); + data.endEvent = *data.endSync->emplace( + data.endSync->get_time_events().begin(), [](auto&&...) {}, + ossia::expressions::make_expression_true()); + + // Create the time_interval connecting start -> end events + auto& itv = cell.interval(); + auto& ctx = system(); + auto dur = ctx.time(itv.duration.defaultDuration()); + auto minDur = ctx.time(itv.duration.minDuration()); + auto maxDur = ctx.time(itv.duration.maxDuration()); + + // create() both constructs AND links the interval into the events' lists + // (startEvent.next_time_intervals, endEvent.previous_time_intervals) + data.interval = ossia::time_interval::create( + {}, *data.startEvent, *data.endEvent, dur, minDur, maxDur); + + // Prepare the interval's node for execution (required before onSetup) + data.interval->node->prepare(*ctx.execState); + + // Add to scenario + m_scenario->add_time_sync(data.startSync); + m_scenario->add_time_sync(data.endSync); + m_scenario->add_time_interval(data.interval); + + // Create execution components for the score model elements + data.startSyncComponent = std::make_shared<::Execution::TimeSyncComponent>( + scenario.startTimeSync(), ctx, this); + data.endSyncComponent = std::make_shared<::Execution::TimeSyncComponent>( + scenario.endTimeSync(), ctx, this); + + data.startEventComponent = std::make_shared<::Execution::EventComponent>( + scenario.startEvent(), ctx, this); + data.endEventComponent = std::make_shared<::Execution::EventComponent>( + scenario.endEvent(), ctx, this); + + data.startStateComponent = std::make_shared<::Execution::StateComponent>( + scenario.startState(), data.startEvent, ctx, this); + data.endStateComponent = std::make_shared<::Execution::StateComponent>( + scenario.endState(), data.endEvent, ctx, this); + + // Pass m_scenario so child process nodes connect to the audio graph properly + data.intervalComponent = std::make_shared<::Execution::IntervalComponent>( + itv, m_scenario, ctx, this); + + // Wire up the components (use makeTrigger() so active/expression/autotrigger are set) + data.startSyncComponent->onSetup( + data.startSync, data.startSyncComponent->makeTrigger()); + data.endSyncComponent->onSetup( + data.endSync, data.endSyncComponent->makeTrigger()); + + data.startEventComponent->onSetup( + data.startEvent, data.startEventComponent->makeExpression(), + ossia::time_event::offset_behavior::EXPRESSION_TRUE); + data.endEventComponent->onSetup( + data.endEvent, data.endEventComponent->makeExpression(), + ossia::time_event::offset_behavior::EXPRESSION_TRUE); + + data.intervalComponent->onSetup( + data.intervalComponent, data.interval, + data.intervalComponent->makeDurations(), false); + + // Connect interval audio output to scenario audio input for audio routing + { + auto ossia_itv = data.interval; + auto proc = m_scenario; + in_exec([g = ctx.execGraph, proc, ossia_itv] { + if(!ossia_itv->node->root_outputs().empty() + && !proc->node->root_inputs().empty()) + { + auto cable = g->allocate_edge( + ossia::immediate_glutton_connection{}, + ossia_itv->node->root_outputs()[0], proc->node->root_inputs()[0], + ossia_itv->node, proc->node); + g->connect(cable); + } + }); + } + + // Connect interval execution events to cell state + auto cellId = cell.id(); + con(itv, &Scenario::IntervalModel::executionEvent, this, + [this, cellId](Scenario::IntervalExecutionEvent ev) { + auto it = process().cells.find(cellId); + if(it == process().cells.end()) + return; + auto& c = *it; + switch(ev) + { + case Scenario::IntervalExecutionEvent::Playing: + c.setCellState(CellState::Playing); + break; + case Scenario::IntervalExecutionEvent::Stopped: + c.setCellState(CellState::Stopped); + // Remove from active tracking + { + auto laneIt = m_activeCellPerLane.find(c.lane()); + if(laneIt != m_activeCellPerLane.end() && laneIt->second == cellId) + m_activeCellPerLane.erase(laneIt); + } + break; + default: + break; + } + }); + + m_cells.insert({cell.id(), std::move(data)}); +} + +void ClipLauncherComponent::launchCell( + const Id& cellId, double quantizationRate) +{ + auto it = m_cells.find(cellId); + if(it == m_cells.end()) + return; + + auto& cell = process().cells.at(cellId); + int lane = cell.lane(); + + // Stop currently active cell in this lane (exclusive mode by default) + stopAllInLane(lane, quantizationRate); + + auto proc = m_scenario; + auto& interval = it->second.interval; + + // Launch the cell on the audio thread + in_exec([proc, interval, quantizationRate] { + proc->request_start_interval(*interval, quantizationRate); + }); + + // Track active cell + m_activeCellPerLane[lane] = cellId; + + // Update cell state + cell.setCellState(quantizationRate > 0 ? CellState::Queued : CellState::Playing); +} + +void ClipLauncherComponent::stopCell( + const Id& cellId, double quantizationRate) +{ + auto it = m_cells.find(cellId); + if(it == m_cells.end()) + return; + + auto proc = m_scenario; + auto& interval = it->second.interval; + + in_exec([proc, interval, quantizationRate] { + proc->request_stop_interval(*interval, quantizationRate); + }); + + // Remove from active tracking + auto& element = process(); + auto& cell = element.cells.at(cellId); + auto laneIt = m_activeCellPerLane.find(cell.lane()); + if(laneIt != m_activeCellPerLane.end() && laneIt->second == cellId) + m_activeCellPerLane.erase(laneIt); + + cell.setCellState(CellState::Stopping); +} + +void ClipLauncherComponent::launchScene(int sceneIndex) +{ + auto& element = process(); + + // Use global quantization rate (1.0 = bar, 4.0 = beat, 0.0 = immediate) + double rate = element.globalQuantization(); + + // Stop all currently active cells (will be replaced by new scene) + for(auto it = m_activeCellPerLane.begin(); it != m_activeCellPerLane.end();) + { + auto cellIt = m_cells.find(it->second); + if(cellIt != m_cells.end()) + { + auto proc = m_scenario; + auto& interval = cellIt->second.interval; + in_exec([proc, interval, rate] { + proc->request_stop_interval(*interval, rate); + }); + auto& c = element.cells.at(it->second); + c.setCellState(CellState::Stopping); + } + it = m_activeCellPerLane.erase(it); + } + + // Launch all cells in the new scene + for(auto& cell : element.cells) + { + if(cell.scene() == sceneIndex) + { + launchCell(cell.id(), rate); + } + } +} + +void ClipLauncherComponent::stopAllInLane(int lane, double quantizationRate) +{ + auto it = m_activeCellPerLane.find(lane); + if(it != m_activeCellPerLane.end()) + { + stopCell(it->second, quantizationRate); + } +} + +} // namespace ClipLauncher::Execution diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp new file mode 100644 index 0000000000..f3f8669cbd --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp @@ -0,0 +1,82 @@ +#pragma once +#include + +#include + +#include + +namespace ossia +{ +class scenario; +class time_interval; +class time_sync; +class time_event; +} + +namespace Execution +{ +class IntervalComponent; +class TimeSyncComponent; +class EventComponent; +class StateComponent; +} + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +namespace Execution +{ + +// Per-cell execution data +struct CellExecData +{ + std::shared_ptr<::Execution::IntervalComponent> intervalComponent; + std::shared_ptr<::Execution::TimeSyncComponent> startSyncComponent; + std::shared_ptr<::Execution::TimeSyncComponent> endSyncComponent; + std::shared_ptr<::Execution::EventComponent> startEventComponent; + std::shared_ptr<::Execution::EventComponent> endEventComponent; + std::shared_ptr<::Execution::StateComponent> startStateComponent; + std::shared_ptr<::Execution::StateComponent> endStateComponent; + + std::shared_ptr interval; + std::shared_ptr startSync; + std::shared_ptr endSync; + std::shared_ptr startEvent; + std::shared_ptr endEvent; +}; + +class ClipLauncherComponent final + : public ::Execution:: + ProcessComponent_T +{ + COMPONENT_METADATA("b2c7d8e9-3f4a-5b6c-8d9e-0a1b2c3d4e5f") +public: + ClipLauncherComponent( + ClipLauncher::ProcessModel& element, const ::Execution::Context& ctx, + QObject* parent); + + ~ClipLauncherComponent() override; + + void cleanup() override; + + // Launch/stop cells + void launchCell(const Id& cellId, double quantizationRate = 0.); + void stopCell(const Id& cellId, double quantizationRate = 0.); + void launchScene(int sceneIndex); + +private: + void setupCell(CellModel& cell); + void stopAllInLane(int lane, double quantizationRate); + + std::shared_ptr m_scenario; + score::hash_map, CellExecData> m_cells; + score::hash_map> m_activeCellPerLane; // lane -> active cell +}; + +using ClipLauncherComponentFactory + = ::Execution::ProcessComponentFactory_T; + +} // namespace Execution +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.cpp new file mode 100644 index 0000000000..2e70aeee96 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.cpp @@ -0,0 +1,22 @@ +#include "CellInspectorFactory.hpp" + +#include +#include + +namespace ClipLauncher +{ + +QWidget* CellInspectorFactory::make( + const InspectedObjects& sourceElements, const score::DocumentContext& doc, + QWidget* parent) const +{ + return new CellInspectorWidget{ + safe_cast(*sourceElements.first()), doc, parent}; +} + +bool CellInspectorFactory::matches(const InspectedObjects& objects) const +{ + return dynamic_cast(objects.first()); +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.hpp new file mode 100644 index 0000000000..42132348b4 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorFactory.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +namespace ClipLauncher +{ + +class CellInspectorFactory final : public Inspector::InspectorWidgetFactory +{ + SCORE_CONCRETE("a1b2c3d4-e5f6-7890-abcd-ef1234567890") +public: + CellInspectorFactory() : InspectorWidgetFactory{} { } + + QWidget* make( + const InspectedObjects& sourceElements, const score::DocumentContext& doc, + QWidget* parent) const override; + + bool matches(const InspectedObjects& objects) const override; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.cpp new file mode 100644 index 0000000000..f176fa36bb --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.cpp @@ -0,0 +1,219 @@ +#include "CellInspectorWidget.hpp" + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace ClipLauncher +{ + +const ProcessModel& CellInspectorWidget::parentProcess() const +{ + return *safe_cast(m_cell.parent()); +} + +CellInspectorWidget::CellInspectorWidget( + const CellModel& cell, const score::DocumentContext& ctx, QWidget* parent) + : InspectorWidgetBase{cell, ctx, parent, + QString("Cell [%1, %2]").arg(cell.lane() + 1).arg(cell.scene() + 1)} + , m_cell{cell} + , m_ctx{ctx} +{ + std::vector parts; + + // --- Cell Properties Section --- + { + auto section = new Inspector::InspectorSectionWidget{"Cell Properties", false, this}; + auto w = new QWidget; + auto lay = new Inspector::Layout{w}; + + // Launch Mode + m_launchModeCombo = new QComboBox; + m_launchModeCombo->addItems( + {tr("Immediate"), tr("Quantized Beat"), tr("Quantized Bar"), + tr("Quantized End Clip"), tr("Queued"), tr("Fader Start")}); + m_launchModeCombo->setCurrentIndex(static_cast(cell.launchMode())); + connect( + m_launchModeCombo, SignalUtils::QComboBox_currentIndexChanged_int(), this, + [this](int idx) { + auto newMode = static_cast(idx); + if(newMode != m_cell.launchMode()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_cell, newMode); + } + }); + con(cell, &CellModel::launchModeChanged, this, [this](LaunchMode m) { + m_launchModeCombo->setCurrentIndex(static_cast(m)); + }); + lay->addRow(tr("Launch"), m_launchModeCombo); + + // Trigger Style + m_triggerStyleCombo = new QComboBox; + m_triggerStyleCombo->addItems( + {tr("Trigger"), tr("Toggle"), tr("Gate"), tr("Retrigger"), tr("Legato")}); + m_triggerStyleCombo->setCurrentIndex(static_cast(cell.triggerStyle())); + connect( + m_triggerStyleCombo, SignalUtils::QComboBox_currentIndexChanged_int(), this, + [this](int idx) { + auto newStyle = static_cast(idx); + if(newStyle != m_cell.triggerStyle()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_cell, newStyle); + } + }); + con(cell, &CellModel::triggerStyleChanged, this, [this](TriggerStyle s) { + m_triggerStyleCombo->setCurrentIndex(static_cast(s)); + }); + lay->addRow(tr("Trigger"), m_triggerStyleCombo); + + // Velocity + m_velocitySpin = new QDoubleSpinBox; + m_velocitySpin->setRange(0.0, 1.0); + m_velocitySpin->setSingleStep(0.05); + m_velocitySpin->setValue(cell.velocity()); + connect(m_velocitySpin, &QDoubleSpinBox::editingFinished, this, [this] { + double newVal = m_velocitySpin->value(); + if(newVal != m_cell.velocity()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_cell, newVal); + } + }); + con(cell, &CellModel::velocityChanged, m_velocitySpin, &QDoubleSpinBox::setValue); + lay->addRow(tr("Velocity"), m_velocitySpin); + + section->addContent(w); + parts.push_back(section); + } + + // --- Follow Actions (Transition Rules) Section --- + { + auto section = new Inspector::InspectorSectionWidget{"Follow Actions", false, this}; + m_transitionRulesContainer = new QWidget; + m_transitionRulesContainer->setLayout(new QVBoxLayout); + m_transitionRulesContainer->layout()->setContentsMargins(0, 0, 0, 0); + m_transitionRulesContainer->layout()->setSpacing(2); + rebuildTransitionRulesList(); + + auto addBtn = new QPushButton{tr("Add Follow Action")}; + connect(addBtn, &QPushButton::clicked, this, [this] { + TransitionRule rule; + int32_t maxId = 0; + for(const auto& r : m_cell.transitionRules()) + maxId = std::max(maxId, r.id); + rule.id = maxId + 1; + rule.condition = TransitionRule::Condition::OnEnd; + rule.target.scene = -1; // next scene + + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_cell, rule); + }); + + con(cell, &CellModel::transitionRulesChanged, this, + &CellInspectorWidget::rebuildTransitionRulesList); + + section->addContent(m_transitionRulesContainer); + section->addContent(addBtn); + parts.push_back(section); + } + + // --- Interval Info Section --- + { + auto section = new Inspector::InspectorSectionWidget{"Interval", false, this}; + auto w = new QWidget; + auto lay = new Inspector::Layout{w}; + + QString name = cell.interval().metadata().getName(); + if(name.isEmpty()) + name = tr("(unnamed)"); + lay->addRow(tr("Name"), new QLabel{name}); + lay->addRow( + tr("Duration"), + new QLabel{cell.interval().duration.defaultDuration().toString()}); + lay->addRow( + tr("Processes"), + new QLabel{QString::number(cell.interval().processes.size())}); + + section->addContent(w); + parts.push_back(section); + } + + updateAreaLayout(parts); +} + +CellInspectorWidget::~CellInspectorWidget() = default; + +void CellInspectorWidget::rebuildTransitionRulesList() +{ + auto layout = m_transitionRulesContainer->layout(); + QLayoutItem* item; + while((item = layout->takeAt(0)) != nullptr) + { + delete item->widget(); + delete item; + } + + for(const auto& rule : m_cell.transitionRules()) + { + auto rowWidget = new QWidget; + auto rowLay = new QHBoxLayout{rowWidget}; + rowLay->setContentsMargins(0, 0, 0, 0); + rowLay->setSpacing(2); + + // Condition combo + auto condCombo = new QComboBox; + condCombo->addItems( + {tr("On End"), tr("After Loop Count"), tr("On Trigger"), tr("Probability")}); + condCombo->setCurrentIndex(static_cast(rule.condition)); + + // Target combo + auto targetCombo = new QComboBox; + targetCombo->addItems({tr("Next Scene"), tr("Random"), tr("Specific...")}); + if(rule.target.scene == -1) + targetCombo->setCurrentIndex(0); + else if(rule.target.scene == -2) + targetCombo->setCurrentIndex(1); + else + targetCombo->setCurrentIndex(2); + + // Remove button + auto removeBtn = new QPushButton{tr("X")}; + removeBtn->setMaximumWidth(30); + int32_t ruleId = rule.id; + connect(removeBtn, &QPushButton::clicked, this, [this, ruleId] { + for(const auto& r : m_cell.transitionRules()) + { + if(r.id == ruleId) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_cell, r); + return; + } + } + }); + + rowLay->addWidget(condCombo); + rowLay->addWidget(targetCombo); + rowLay->addWidget(removeBtn); + layout->addWidget(rowWidget); + } +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.hpp new file mode 100644 index 0000000000..d921c511b5 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/CellInspectorWidget.hpp @@ -0,0 +1,31 @@ +#pragma once +#include + +class QComboBox; +class QDoubleSpinBox; + +namespace ClipLauncher +{ +class CellModel; +class ProcessModel; + +class CellInspectorWidget final : public Inspector::InspectorWidgetBase +{ +public: + CellInspectorWidget( + const CellModel& cell, const score::DocumentContext& ctx, QWidget* parent); + ~CellInspectorWidget(); + +private: + void rebuildTransitionRulesList(); + const ProcessModel& parentProcess() const; + + const CellModel& m_cell; + const score::DocumentContext& m_ctx; + QComboBox* m_launchModeCombo{}; + QComboBox* m_triggerStyleCombo{}; + QDoubleSpinBox* m_velocitySpin{}; + QWidget* m_transitionRulesContainer{}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.cpp new file mode 100644 index 0000000000..a7566ba4f2 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.cpp @@ -0,0 +1,22 @@ +#include "LaneInspectorFactory.hpp" + +#include +#include + +namespace ClipLauncher +{ + +QWidget* LaneInspectorFactory::make( + const InspectedObjects& sourceElements, const score::DocumentContext& doc, + QWidget* parent) const +{ + return new LaneInspectorWidget{ + safe_cast(*sourceElements.first()), doc, parent}; +} + +bool LaneInspectorFactory::matches(const InspectedObjects& objects) const +{ + return dynamic_cast(objects.first()); +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.hpp new file mode 100644 index 0000000000..08c305ecf1 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorFactory.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +namespace ClipLauncher +{ + +class LaneInspectorFactory final : public Inspector::InspectorWidgetFactory +{ + SCORE_CONCRETE("b2c3d4e5-f6a7-8901-bcde-f23456789012") +public: + LaneInspectorFactory() : InspectorWidgetFactory{} { } + + QWidget* make( + const InspectedObjects& sourceElements, const score::DocumentContext& doc, + QWidget* parent) const override; + + bool matches(const InspectedObjects& objects) const override; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp new file mode 100644 index 0000000000..d39026cab2 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp @@ -0,0 +1,68 @@ +#include "LaneInspectorWidget.hpp" + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace ClipLauncher +{ + +const ProcessModel& LaneInspectorWidget::parentProcess() const +{ + return *safe_cast(m_lane.parent()); +} + +LaneInspectorWidget::LaneInspectorWidget( + const LaneModel& lane, const score::DocumentContext& ctx, QWidget* parent) + : InspectorWidgetBase{lane, ctx, parent, tr("Lane")} + , m_lane{lane} + , m_ctx{ctx} +{ + auto w = new QWidget; + auto lay = new Inspector::Layout{w}; + + // Name + auto nameEdit = new QLineEdit{lane.name()}; + connect(nameEdit, &QLineEdit::editingFinished, this, [this, nameEdit] { + if(nameEdit->text() != m_lane.name()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_lane, nameEdit->text()); + } + }); + con(lane, &LaneModel::nameChanged, nameEdit, &QLineEdit::setText); + lay->addRow(tr("Name"), nameEdit); + + // Exclusivity mode + auto exclCombo = new QComboBox; + exclCombo->addItems({tr("Exclusive"), tr("Polyphonic"), tr("Crossfade")}); + exclCombo->setCurrentIndex(static_cast(lane.exclusivityMode())); + connect( + exclCombo, SignalUtils::QComboBox_currentIndexChanged_int(), this, + [this](int idx) { + auto newMode = static_cast(idx); + if(newMode != m_lane.exclusivityMode()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_lane, newMode); + } + }); + con(lane, &LaneModel::exclusivityModeChanged, this, + [exclCombo](ExclusivityMode m) { exclCombo->setCurrentIndex(static_cast(m)); }); + lay->addRow(tr("Exclusivity"), exclCombo); + + updateAreaLayout({w}); +} + +LaneInspectorWidget::~LaneInspectorWidget() = default; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.hpp new file mode 100644 index 0000000000..435c44b378 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +namespace ClipLauncher +{ +class LaneModel; +class ProcessModel; + +class LaneInspectorWidget final : public Inspector::InspectorWidgetBase +{ +public: + LaneInspectorWidget( + const LaneModel& lane, const score::DocumentContext& ctx, QWidget* parent); + ~LaneInspectorWidget(); + +private: + const ProcessModel& parentProcess() const; + const LaneModel& m_lane; + const score::DocumentContext& m_ctx; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.cpp new file mode 100644 index 0000000000..432af08a1e --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.cpp @@ -0,0 +1,22 @@ +#include "SceneInspectorFactory.hpp" + +#include +#include + +namespace ClipLauncher +{ + +QWidget* SceneInspectorFactory::make( + const InspectedObjects& sourceElements, const score::DocumentContext& doc, + QWidget* parent) const +{ + return new SceneInspectorWidget{ + safe_cast(*sourceElements.first()), doc, parent}; +} + +bool SceneInspectorFactory::matches(const InspectedObjects& objects) const +{ + return dynamic_cast(objects.first()); +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.hpp new file mode 100644 index 0000000000..e41a12c8b2 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorFactory.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +namespace ClipLauncher +{ + +class SceneInspectorFactory final : public Inspector::InspectorWidgetFactory +{ + SCORE_CONCRETE("c3d4e5f6-a7b8-9012-cdef-345678901234") +public: + SceneInspectorFactory() : InspectorWidgetFactory{} { } + + QWidget* make( + const InspectedObjects& sourceElements, const score::DocumentContext& doc, + QWidget* parent) const override; + + bool matches(const InspectedObjects& objects) const override; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.cpp new file mode 100644 index 0000000000..6bfd836cc5 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.cpp @@ -0,0 +1,48 @@ +#include "SceneInspectorWidget.hpp" + +#include + +#include +#include + +#include + +#include +#include +#include + +namespace ClipLauncher +{ + +const ProcessModel& SceneInspectorWidget::parentProcess() const +{ + return *safe_cast(m_scene.parent()); +} + +SceneInspectorWidget::SceneInspectorWidget( + const SceneModel& scene, const score::DocumentContext& ctx, QWidget* parent) + : InspectorWidgetBase{scene, ctx, parent, tr("Scene")} + , m_scene{scene} + , m_ctx{ctx} +{ + auto w = new QWidget; + auto lay = new Inspector::Layout{w}; + + // Name + auto nameEdit = new QLineEdit{scene.name()}; + connect(nameEdit, &QLineEdit::editingFinished, this, [this, nameEdit] { + if(nameEdit->text() != m_scene.name()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_scene, nameEdit->text()); + } + }); + con(scene, &SceneModel::nameChanged, nameEdit, &QLineEdit::setText); + lay->addRow(tr("Name"), nameEdit); + + updateAreaLayout({w}); +} + +SceneInspectorWidget::~SceneInspectorWidget() = default; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.hpp new file mode 100644 index 0000000000..287f7055d9 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/SceneInspectorWidget.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +namespace ClipLauncher +{ +class SceneModel; +class ProcessModel; + +class SceneInspectorWidget final : public Inspector::InspectorWidgetBase +{ +public: + SceneInspectorWidget( + const SceneModel& scene, const score::DocumentContext& ctx, QWidget* parent); + ~SceneInspectorWidget(); + +private: + const ProcessModel& parentProcess() const; + const SceneModel& m_scene; + const score::DocumentContext& m_ctx; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp new file mode 100644 index 0000000000..a149384495 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp @@ -0,0 +1,102 @@ +#include "LaneModel.hpp" + +#include +#include +#include + +W_OBJECT_IMPL(ClipLauncher::LaneModel) + +namespace ClipLauncher +{ + +LaneModel::LaneModel(const Id& id, QObject* parent) + : score::Entity{id, "Lane", parent} +{ +} + +LaneModel::~LaneModel() { } + +void LaneModel::setName(const QString& n) +{ + if(m_name != n) + { + m_name = n; + nameChanged(n); + } +} + +void LaneModel::setExclusivityMode(ExclusivityMode m) +{ + if(m_exclusivityMode != m) + { + m_exclusivityMode = m; + exclusivityModeChanged(m); + } +} + +void LaneModel::setTemporalMode(TemporalMode m) +{ + if(m_temporalMode != m) + { + m_temporalMode = m; + temporalModeChanged(m); + } +} + +void LaneModel::setCrossfadeDuration(double d) +{ + if(m_crossfadeDuration != d) + { + m_crossfadeDuration = d; + crossfadeDurationChanged(d); + } +} + +void LaneModel::setVolume(double v) +{ + if(m_volume != v) + { + m_volume = v; + volumeChanged(v); + } +} + +} // namespace ClipLauncher + +template <> +void DataStreamReader::read(const ClipLauncher::LaneModel& lane) +{ + m_stream << lane.m_name << lane.m_exclusivityMode << lane.m_temporalMode + << lane.m_crossfadeDuration << lane.m_volume; + insertDelimiter(); +} + +template <> +void DataStreamWriter::write(ClipLauncher::LaneModel& lane) +{ + m_stream >> lane.m_name >> lane.m_exclusivityMode >> lane.m_temporalMode + >> lane.m_crossfadeDuration >> lane.m_volume; + checkDelimiter(); +} + +template <> +void JSONReader::read(const ClipLauncher::LaneModel& lane) +{ + obj["Name"] = lane.m_name; + obj["ExclusivityMode"] = static_cast(lane.m_exclusivityMode); + obj["TemporalMode"] = static_cast(lane.m_temporalMode); + obj["CrossfadeDuration"] = lane.m_crossfadeDuration; + obj["Volume"] = lane.m_volume; +} + +template <> +void JSONWriter::write(ClipLauncher::LaneModel& lane) +{ + lane.m_name = obj["Name"].toString(); + lane.m_exclusivityMode + = static_cast(obj["ExclusivityMode"].toInt()); + lane.m_temporalMode + = static_cast(obj["TemporalMode"].toInt()); + lane.m_crossfadeDuration = obj["CrossfadeDuration"].toDouble(); + lane.m_volume = obj["Volume"].toDouble(); +} diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp new file mode 100644 index 0000000000..8b9f95aec8 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include + +#include + +#include + +#include + +namespace ClipLauncher +{ + +class LaneModel final : public score::Entity +{ + W_OBJECT(LaneModel) + SCORE_SERIALIZE_FRIENDS + +public: + LaneModel(const Id& id, QObject* parent); + + template + LaneModel(Impl& vis, QObject* parent) + : score::Entity{vis, parent} + { + vis.writeTo(*this); + } + + ~LaneModel() override; + + QString name() const noexcept { return m_name; } + void setName(const QString& n); + + ExclusivityMode exclusivityMode() const noexcept { return m_exclusivityMode; } + void setExclusivityMode(ExclusivityMode m); + + TemporalMode temporalMode() const noexcept { return m_temporalMode; } + void setTemporalMode(TemporalMode m); + + double crossfadeDuration() const noexcept { return m_crossfadeDuration; } + void setCrossfadeDuration(double d); + + double volume() const noexcept { return m_volume; } + void setVolume(double v); + + void nameChanged(const QString& n) E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, nameChanged, n) + void exclusivityModeChanged(ClipLauncher::ExclusivityMode m) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, exclusivityModeChanged, m) + void temporalModeChanged(ClipLauncher::TemporalMode m) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, temporalModeChanged, m) + void crossfadeDurationChanged(double d) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, crossfadeDurationChanged, d) + void volumeChanged(double v) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, volumeChanged, v) + +private: + QString m_name; + ExclusivityMode m_exclusivityMode{ExclusivityMode::Exclusive}; + TemporalMode m_temporalMode{TemporalMode::BPMSynced}; + double m_crossfadeDuration{0.5}; + double m_volume{1.0}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Metadata.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Metadata.hpp new file mode 100644 index 0000000000..ca334386cf --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Metadata.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +namespace ClipLauncher +{ +class ProcessModel; +} + +PROCESS_METADATA( + , ClipLauncher::ProcessModel, "a8e5e9f0-1b3c-4d7e-9f2a-6c8b4d3e5f7a", + "ClipLauncher", // ObjectKey + "Clip Launcher", // PrettyName + Process::ProcessCategory::Structure, // Category + "Structure", // Category text + "Grid-based clip launcher for live performance with lanes, scenes, " + "and transition rules", // Description + "ossia score", // Author + (QStringList{"live", "performance", "clips", "launcher"}), // Tags + {}, // Inputs + {}, // Outputs + QUrl(""), // Documentation + Process::ProcessFlags::SupportsTemporal // + | Process::ProcessFlags::PutInNewSlot // + | Process::ProcessFlags::FullyCustomItem) diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp new file mode 100644 index 0000000000..11e8efa2bf --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp @@ -0,0 +1,299 @@ +#include "ProcessModel.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +W_OBJECT_IMPL(ClipLauncher::ProcessModel) + +namespace ClipLauncher +{ + +ProcessModel::ProcessModel( + const TimeVal& duration, const Id& id, + const score::DocumentContext& ctx, QObject* parent) + : Process::ProcessModel{duration, id, "ClipLauncher", parent} + , m_context{ctx} +{ + metadata().setInstanceName(*this); + inlet = std::make_unique("Audio In", Id(0), this); + outlet = std::make_unique("Audio Out", Id(0), this); + outlet->setPropagate(true); + + // Create initial grid: 2 lanes x 4 scenes with cells at every position + for(int l = 0; l < 2; l++) + { + auto lane = new LaneModel{getStrongId(lanes), this}; + lane->setName(QString("Lane %1").arg(l + 1)); + lanes.add(lane); + } + for(int s = 0; s < 4; s++) + { + auto scene = new SceneModel{getStrongId(scenes), this}; + scene->setName(QString("Scene %1").arg(s + 1)); + scenes.add(scene); + } + for(int l = 0; l < 2; l++) + for(int s = 0; s < 4; s++) + cells.add(createDefaultCell(getStrongId(cells), l, s, ctx, this)); + + init(); +} + +ProcessModel::~ProcessModel() { } + +void ProcessModel::init() +{ + m_inlets.push_back(inlet.get()); + m_outlets.push_back(outlet.get()); +} + +CellModel* ProcessModel::cellAt(int lane, int scene) const +{ + for(auto& cell : cells) + { + if(cell.lane() == lane && cell.scene() == scene) + return &cell; + } + return nullptr; +} + +std::vector ProcessModel::cellsInLane(int lane) const +{ + std::vector result; + for(auto& cell : cells) + { + if(cell.lane() == lane) + result.push_back(&cell); + } + return result; +} + +std::vector ProcessModel::cellsInScene(int scene) const +{ + std::vector result; + for(auto& cell : cells) + { + if(cell.scene() == scene) + result.push_back(&cell); + } + return result; +} + +void ProcessModel::setGlobalQuantization(double q) +{ + if(m_globalQuantization != q) + { + m_globalQuantization = q; + globalQuantizationChanged(q); + } +} + +CellModel* ProcessModel::createDefaultCell( + const Id& id, int lane, int scene, + const score::DocumentContext& ctx, QObject* parent) +{ + auto cell = new CellModel{id, ctx, parent}; + cell->setLane(lane); + cell->setScene(scene); + + // Clip-launcher defaults: 8s, flexible, end trigger active + auto& dur = cell->interval().duration; + const auto defaultDur = TimeVal::fromMsecs(8000); + dur.setDefaultDuration(defaultDur); + dur.setRigid(false); + dur.setMinNull(true); + dur.setMaxInfinite(true); + cell->endTimeSync().setDate(defaultDur); + cell->endTimeSync().setActive(true); + + return cell; +} + +void ProcessModel::setDurationAndScale(const TimeVal& newDuration) noexcept +{ + setDuration(newDuration); +} + +void ProcessModel::setDurationAndGrow(const TimeVal& newDuration) noexcept +{ + setDuration(newDuration); +} + +void ProcessModel::setDurationAndShrink(const TimeVal& newDuration) noexcept +{ + setDuration(newDuration); +} + +Selection ProcessModel::selectableChildren() const noexcept +{ + Selection s; + for(auto& cell : cells) + s.append(&cell); + return s; +} + +Selection ProcessModel::selectedChildren() const noexcept +{ + Selection s; + for(auto& cell : cells) + { + if(cell.selection.get()) + s.append(&cell); + } + return s; +} + +void ProcessModel::setSelection(const Selection& s) const noexcept +{ + for(auto& cell : cells) + cell.selection.set(s.contains(&cell)); +} + +} // namespace ClipLauncher + +// Serialization +template <> +void DataStreamReader::read(const ClipLauncher::ProcessModel& proc) +{ + readFrom(*proc.inlet); + readFrom(*proc.outlet); + + m_stream << proc.m_globalQuantization; + + // Lanes + m_stream << (int32_t)proc.lanes.size(); + for(const auto& lane : proc.lanes) + readFrom(lane); + + // Scenes + m_stream << (int32_t)proc.scenes.size(); + for(const auto& scene : proc.scenes) + readFrom(scene); + + // Cells + m_stream << (int32_t)proc.cells.size(); + for(const auto& cell : proc.cells) + readFrom(cell); + + insertDelimiter(); +} + +template <> +void DataStreamWriter::write(ClipLauncher::ProcessModel& proc) +{ + proc.inlet = Process::load_audio_inlet(*this, &proc); + proc.outlet = Process::load_audio_outlet(*this, &proc); + + m_stream >> proc.m_globalQuantization; + + // Lanes + { + int32_t sz; + m_stream >> sz; + for(int i = 0; i < sz; i++) + { + auto lane = new ClipLauncher::LaneModel{*this, &proc}; + proc.lanes.add(lane); + } + } + + // Scenes + { + int32_t sz; + m_stream >> sz; + for(int i = 0; i < sz; i++) + { + auto scene = new ClipLauncher::SceneModel{*this, &proc}; + proc.scenes.add(scene); + } + } + + // Cells + { + int32_t cellCount; + m_stream >> cellCount; + for(int i = 0; i < cellCount; i++) + { + auto cell = new ClipLauncher::CellModel{*this, proc.m_context, &proc}; + proc.cells.add(cell); + } + } +} + +template <> +void JSONReader::read(const ClipLauncher::ProcessModel& proc) +{ + obj["Inlet"] = *proc.inlet; + obj["Outlet"] = *proc.outlet; + + obj["GlobalQuantization"] = proc.m_globalQuantization; + + obj["Lanes"] = proc.lanes; + obj["Scenes"] = proc.scenes; + + // Cells: serialize manually since they need DocumentContext + obj["Cells"] = proc.cells; +} + +template <> +void JSONWriter::write(ClipLauncher::ProcessModel& proc) +{ + if(auto inl = obj.tryGet("Inlet")) + { + JSONWriter writer{*inl}; + proc.inlet = Process::load_audio_inlet(writer, &proc); + } + else + { + proc.inlet = std::make_unique( + "Audio In", Id(0), &proc); + } + + if(auto outl = obj.tryGet("Outlet")) + { + JSONWriter writer{*outl}; + proc.outlet = Process::load_audio_outlet(writer, &proc); + } + else + { + proc.outlet = std::make_unique( + "Audio Out", Id(0), &proc); + } + + proc.m_globalQuantization = obj["GlobalQuantization"].toDouble(); + + const auto& lanes_arr = obj["Lanes"].toArray(); + for(const auto& val : lanes_arr) + { + JSONObject::Deserializer des{val}; + auto lane = new ClipLauncher::LaneModel{des, &proc}; + proc.lanes.add(lane); + } + + const auto& scenes_arr = obj["Scenes"].toArray(); + for(const auto& val : scenes_arr) + { + JSONObject::Deserializer des{val}; + auto scene = new ClipLauncher::SceneModel{des, &proc}; + proc.scenes.add(scene); + } + + // Cells + const auto& cells_arr = obj["Cells"].toArray(); + for(const auto& val : cells_arr) + { + JSONObject::Deserializer des{val}; + auto cell = new ClipLauncher::CellModel{des, proc.m_context, &proc}; + proc.cells.add(cell); + } +} diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp new file mode 100644 index 0000000000..ff1acec729 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp @@ -0,0 +1,88 @@ +#pragma once +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +namespace ClipLauncher +{ + +class ProcessModel final : public Process::ProcessModel +{ + W_OBJECT(ProcessModel) + SCORE_SERIALIZE_FRIENDS + PROCESS_METADATA_IMPL(ClipLauncher::ProcessModel) + +public: + std::unique_ptr inlet; + std::unique_ptr outlet; + + score::EntityMap lanes; + score::EntityMap scenes; + score::EntityMap cells; + + ProcessModel( + const TimeVal& duration, const Id& id, + const score::DocumentContext& ctx, QObject* parent); + + template + ProcessModel(Impl& vis, const score::DocumentContext& ctx, QObject* parent) + : Process::ProcessModel{vis, parent} + , m_context{ctx} + { + vis.writeTo(*this); + init(); + } + + ~ProcessModel() override; + + const score::DocumentContext& context() const noexcept { return m_context; } + + // Cell lookup + CellModel* cellAt(int lane, int scene) const; + std::vector cellsInLane(int lane) const; + std::vector cellsInScene(int scene) const; + + // Grid dimensions + int laneCount() const noexcept { return lanes.size(); } + int sceneCount() const noexcept { return scenes.size(); } + + // Global quantization + double globalQuantization() const noexcept { return m_globalQuantization; } + void setGlobalQuantization(double q); + + // Create a cell with proper clip launcher defaults + static CellModel* createDefaultCell( + const Id& id, int lane, int scene, + const score::DocumentContext& ctx, QObject* parent); + + // ProcessModel overrides + void setDurationAndScale(const TimeVal& newDuration) noexcept override; + void setDurationAndGrow(const TimeVal& newDuration) noexcept override; + void setDurationAndShrink(const TimeVal& newDuration) noexcept override; + + Selection selectableChildren() const noexcept override; + Selection selectedChildren() const noexcept override; + void setSelection(const Selection& s) const noexcept override; + + // Signals + void globalQuantizationChanged(double q) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, globalQuantizationChanged, q) + +private: + void init(); + const score::DocumentContext& m_context; + double m_globalQuantization{1.0}; // Default: quantize to bar +}; + +using ProcessFactory = Process::ProcessFactory_T; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.cpp new file mode 100644 index 0000000000..1db0324360 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.cpp @@ -0,0 +1,54 @@ +#include "SceneModel.hpp" + +#include +#include +#include + +W_OBJECT_IMPL(ClipLauncher::SceneModel) + +namespace ClipLauncher +{ + +SceneModel::SceneModel(const Id& id, QObject* parent) + : score::Entity{id, "Scene", parent} +{ +} + +SceneModel::~SceneModel() { } + +void SceneModel::setName(const QString& n) +{ + if(m_name != n) + { + m_name = n; + nameChanged(n); + } +} + +} // namespace ClipLauncher + +template <> +void DataStreamReader::read(const ClipLauncher::SceneModel& scene) +{ + m_stream << scene.m_name; + insertDelimiter(); +} + +template <> +void DataStreamWriter::write(ClipLauncher::SceneModel& scene) +{ + m_stream >> scene.m_name; + checkDelimiter(); +} + +template <> +void JSONReader::read(const ClipLauncher::SceneModel& scene) +{ + obj["Name"] = scene.m_name; +} + +template <> +void JSONWriter::write(ClipLauncher::SceneModel& scene) +{ + scene.m_name = obj["Name"].toString(); +} diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.hpp new file mode 100644 index 0000000000..1891b09df3 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/SceneModel.hpp @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include + +#include + +#include +namespace ClipLauncher +{ + +class SceneModel final : public score::Entity +{ + W_OBJECT(SceneModel) + SCORE_SERIALIZE_FRIENDS + +public: + SceneModel(const Id& id, QObject* parent); + + template + SceneModel(Impl& vis, QObject* parent) + : score::Entity{vis, parent} + { + vis.writeTo(*this); + } + + ~SceneModel() override; + + QString name() const noexcept { return m_name; } + void setName(const QString& n); + + void nameChanged(const QString& n) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, nameChanged, n) + +private: + QString m_name; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/TransitionRule.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/TransitionRule.hpp new file mode 100644 index 0000000000..715d4e89f0 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/TransitionRule.hpp @@ -0,0 +1,132 @@ +#pragma once +#include + +#include +#include + +#include + +namespace ClipLauncher +{ + +struct TransitionTarget +{ + int lane{-1}; // -1 = same lane + int scene{-1}; // -1 = next scene, -2 = random, >= 0 = specific scene +}; + +struct TransitionRule +{ + enum class Condition : uint8_t + { + OnEnd, // When clip reaches its end + AfterLoopCount, // After N loops + OnTrigger, // On external trigger + Probability // Random chance per loop + }; + + int32_t id{}; + Condition condition{Condition::OnEnd}; + int loopCount{1}; + double probability{1.0}; + int priority{0}; + + TransitionTarget target; + LaunchMode launchMode{LaunchMode::Immediate}; +}; + +} // namespace ClipLauncher + +template <> +struct is_custom_serialized : std::true_type +{ +}; +template <> +struct is_custom_serialized : std::true_type +{ +}; + +// DataStream serialization for TransitionTarget +template <> +struct TSerializer +{ + static void readFrom(DataStream::Serializer& s, const ClipLauncher::TransitionTarget& v) + { + s.stream() << v.lane << v.scene; + } + static void writeTo(DataStream::Deserializer& s, ClipLauncher::TransitionTarget& v) + { + s.stream() >> v.lane >> v.scene; + } +}; + +// DataStream serialization for TransitionRule +template <> +struct TSerializer +{ + static void readFrom(DataStream::Serializer& s, const ClipLauncher::TransitionRule& v) + { + s.stream() << v.id << static_cast(v.condition) << v.loopCount + << v.probability << v.priority << v.target + << static_cast(v.launchMode); + } + static void writeTo(DataStream::Deserializer& s, ClipLauncher::TransitionRule& v) + { + uint8_t cond, mode; + s.stream() >> v.id >> cond >> v.loopCount >> v.probability >> v.priority >> v.target + >> mode; + v.condition = static_cast(cond); + v.launchMode = static_cast(mode); + } +}; + +// JSON serialization for TransitionTarget +template <> +struct TSerializer +{ + static void + readFrom(JSONObject::Serializer& s, const ClipLauncher::TransitionTarget& v) + { + s.stream.StartObject(); + s.obj["Lane"] = v.lane; + s.obj["Scene"] = v.scene; + s.stream.EndObject(); + } + static void writeTo(JSONObject::Deserializer& s, ClipLauncher::TransitionTarget& v) + { + v.lane = s.obj["Lane"].toInt(); + v.scene = s.obj["Scene"].toInt(); + } +}; + +// JSON serialization for TransitionRule +template <> +struct TSerializer +{ + static void readFrom(JSONObject::Serializer& s, const ClipLauncher::TransitionRule& v) + { + s.stream.StartObject(); + s.obj["Id"] = v.id; + s.obj["Condition"] = static_cast(v.condition); + s.obj["LoopCount"] = v.loopCount; + s.obj["Probability"] = v.probability; + s.obj["Priority"] = v.priority; + s.obj["Target"] = v.target; + s.obj["LaunchMode"] = static_cast(v.launchMode); + s.stream.EndObject(); + } + static void writeTo(JSONObject::Deserializer& s, ClipLauncher::TransitionRule& v) + { + v.id = s.obj["Id"].toInt(); + v.condition + = static_cast(s.obj["Condition"].toInt()); + v.loopCount = s.obj["LoopCount"].toInt(); + v.probability = s.obj["Probability"].toDouble(); + v.priority = s.obj["Priority"].toInt(); + { + JSONObject::Deserializer sub{s.obj["Target"]}; + sub.writeTo(v.target); + } + v.launchMode = static_cast(s.obj["LaunchMode"].toInt()); + } +}; diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp new file mode 100644 index 0000000000..5a9921e938 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp @@ -0,0 +1,110 @@ +#pragma once +#include +#include + +#include + +#include + +namespace ClipLauncher +{ + +enum class ExclusivityMode : uint8_t +{ + Exclusive, // Starting a clip stops the current one in this lane + Polyphonic, // Multiple clips can run simultaneously + Crossfade // Starting a clip crossfades from current to new +}; + +enum class LaunchMode : uint8_t +{ + Immediate, // Start right now + QuantizedBeat, // Start on next beat boundary + QuantizedBar, // Start on next bar boundary + QuantizedEndClip, // Start when current clip ends + Queued, // Queue after current clip + FaderStart // Start follows fader value +}; + +enum class TriggerStyle : uint8_t +{ + Trigger, // Press to start, press again to stop + Toggle, // Press to start, press to stop + Gate, // Held = playing, release = stop + Retrigger, // Press always restarts from beginning + Legato // Press restarts but continues time position +}; + +enum class TemporalMode : uint8_t +{ + FreeRunning, // Runs at its own pace + BPMSynced, // Follows global tempo + TimecodeLocked, // Locked to timecode + Interactive // Manually driven +}; + +enum class CellState : uint8_t +{ + Empty, + Stopped, + Queued, + Playing, + Stopping +}; + +} // namespace ClipLauncher + +// Serialization +inline QDataStream& operator<<(QDataStream& s, ClipLauncher::ExclusivityMode v) +{ + return s << static_cast(v); +} +inline QDataStream& operator>>(QDataStream& s, ClipLauncher::ExclusivityMode& v) +{ + uint8_t x; + s >> x; + v = static_cast(x); + return s; +} + +inline QDataStream& operator<<(QDataStream& s, ClipLauncher::LaunchMode v) +{ + return s << static_cast(v); +} +inline QDataStream& operator>>(QDataStream& s, ClipLauncher::LaunchMode& v) +{ + uint8_t x; + s >> x; + v = static_cast(x); + return s; +} + +inline QDataStream& operator<<(QDataStream& s, ClipLauncher::TriggerStyle v) +{ + return s << static_cast(v); +} +inline QDataStream& operator>>(QDataStream& s, ClipLauncher::TriggerStyle& v) +{ + uint8_t x; + s >> x; + v = static_cast(x); + return s; +} + +inline QDataStream& operator<<(QDataStream& s, ClipLauncher::TemporalMode v) +{ + return s << static_cast(v); +} +inline QDataStream& operator>>(QDataStream& s, ClipLauncher::TemporalMode& v) +{ + uint8_t x; + s >> x; + v = static_cast(x); + return s; +} + +W_REGISTER_ARGTYPE(ClipLauncher::ExclusivityMode) +W_REGISTER_ARGTYPE(ClipLauncher::LaunchMode) +W_REGISTER_ARGTYPE(ClipLauncher::TriggerStyle) +W_REGISTER_ARGTYPE(ClipLauncher::TemporalMode) +W_REGISTER_ARGTYPE(ClipLauncher::CellState) diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.cpp new file mode 100644 index 0000000000..385d3b76d8 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.cpp @@ -0,0 +1,60 @@ +#include "CellDisplayedElementsProvider.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace ClipLauncher +{ + +bool CellDisplayedElementsProvider::matches( + const Scenario::IntervalModel& cst) const +{ + return dynamic_cast(cst.parent()) != nullptr; +} + +Scenario::DisplayedElementsContainer +CellDisplayedElementsProvider::make(Scenario::IntervalModel& cst) const +{ + if(auto* cell = dynamic_cast(cst.parent())) + { + auto& sc = cell->scenarioContainer(); + return Scenario::DisplayedElementsContainer{ + cst, + sc.startState(), + sc.endState(), + sc.startEvent(), + sc.endEvent(), + sc.startTimeSync(), + sc.endTimeSync()}; + } + return {}; +} + +Scenario::DisplayedElementsPresenterContainer +CellDisplayedElementsProvider::make_presenters( + ZoomRatio zoom, const Scenario::IntervalModel& m, + const Process::Context& ctx, QGraphicsItem* view_parent, + QObject* parent) const +{ + if(auto* cell = dynamic_cast(m.parent())) + { + auto& sc = cell->scenarioContainer(); + return Scenario::DisplayedElementsPresenterContainer{ + new Scenario::FullViewIntervalPresenter{zoom, m, ctx, view_parent, parent}, + new Scenario::StatePresenter{sc.startState(), ctx, view_parent, parent}, + new Scenario::StatePresenter{sc.endState(), ctx, view_parent, parent}, + new Scenario::EventPresenter{sc.startEvent(), view_parent, parent}, + new Scenario::EventPresenter{sc.endEvent(), view_parent, parent}, + new Scenario::TimeSyncPresenter{sc.startTimeSync(), view_parent, parent}, + new Scenario::TimeSyncPresenter{sc.endTimeSync(), view_parent, parent}}; + } + return {}; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.hpp new file mode 100644 index 0000000000..b345bac28b --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/CellDisplayedElementsProvider.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +namespace ClipLauncher +{ + +class CellDisplayedElementsProvider final + : public Scenario::DisplayedElementsProvider +{ + SCORE_CONCRETE("c3d4e5f6-7a8b-9c0d-1e2f-3a4b5c6d7e8f") +public: + bool matches(const Scenario::IntervalModel& cst) const override; + Scenario::DisplayedElementsContainer + make(Scenario::IntervalModel& cst) const override; + Scenario::DisplayedElementsPresenterContainer make_presenters( + ZoomRatio zoom, const Scenario::IntervalModel& m, + const Process::Context& ctx, QGraphicsItem* view_parent, + QObject* parent) const override; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.cpp new file mode 100644 index 0000000000..3d6e362e09 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.cpp @@ -0,0 +1,294 @@ +#include "ClipLauncherPresenter.hpp" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +W_OBJECT_IMPL(ClipLauncher::ClipLauncherPresenter) + +namespace ClipLauncher +{ + +ClipLauncherPresenter::ClipLauncherPresenter( + const ProcessModel& model, ClipLauncherView* view, const Process::Context& ctx, + QObject* parent) + : Process::LayerPresenter{model, view, ctx, parent} + , m_model{model} + , m_view{view} +{ + m_view->setModel(&m_model); + + connect(m_view, &ClipLauncherView::cellClicked, this, &ClipLauncherPresenter::on_cellClicked); + connect( + m_view, &ClipLauncherView::cellDoubleClicked, this, + &ClipLauncherPresenter::on_cellDoubleClicked); + connect( + m_view, &ClipLauncherView::sceneLaunchClicked, this, + &ClipLauncherPresenter::on_sceneLaunchClicked); + connect( + m_view, &ClipLauncherView::dropOnCell, this, + &ClipLauncherPresenter::on_dropOnCell); + connect( + m_view, &ClipLauncherView::laneHeaderClicked, this, + &ClipLauncherPresenter::on_laneHeaderClicked); + connect( + m_view, &ClipLauncherView::sceneHeaderClicked, this, + &ClipLauncherPresenter::on_sceneHeaderClicked); + + // React to model changes (EntityMap uses Nano::Signal, not QObject) + model.cells.added.connect<&ClipLauncherPresenter::on_cellChanged>(this); + model.cells.removed.connect<&ClipLauncherPresenter::on_cellChanged>(this); + model.lanes.added.connect<&ClipLauncherPresenter::on_laneChanged>(this); + model.lanes.removed.connect<&ClipLauncherPresenter::on_laneChanged>(this); + model.scenes.added.connect<&ClipLauncherPresenter::on_sceneChanged>(this); + model.scenes.removed.connect<&ClipLauncherPresenter::on_sceneChanged>(this); + + // Progress update timer for live cell state feedback + m_progressTimer = new QTimer{this}; + m_progressTimer->setInterval(33); // ~30fps + connect(m_progressTimer, &QTimer::timeout, this, &ClipLauncherPresenter::updateView); +} + +ClipLauncherPresenter::~ClipLauncherPresenter() +{ + // Disconnect Nano::Signal connections to prevent dangling callbacks + m_model.cells.added.disconnect<&ClipLauncherPresenter::on_cellChanged>(this); + m_model.cells.removed.disconnect<&ClipLauncherPresenter::on_cellChanged>(this); + m_model.lanes.added.disconnect<&ClipLauncherPresenter::on_laneChanged>(this); + m_model.lanes.removed.disconnect<&ClipLauncherPresenter::on_laneChanged>(this); + m_model.scenes.added.disconnect<&ClipLauncherPresenter::on_sceneChanged>(this); + m_model.scenes.removed.disconnect<&ClipLauncherPresenter::on_sceneChanged>(this); +} + +const ProcessModel& ClipLauncherPresenter::model() const noexcept +{ + return m_model; +} + +void ClipLauncherPresenter::setWidth(qreal width, qreal defaultWidth) +{ + m_view->setWidth(width); +} + +void ClipLauncherPresenter::setHeight(qreal height) +{ + m_view->setHeight(height); +} + +void ClipLauncherPresenter::putToFront() +{ + m_view->setVisible(true); + m_progressTimer->start(); +} + +void ClipLauncherPresenter::putBehind() +{ + m_view->setVisible(false); + m_progressTimer->stop(); +} + +void ClipLauncherPresenter::on_zoomRatioChanged(ZoomRatio val) +{ + updateView(); +} + +void ClipLauncherPresenter::parentGeometryChanged() +{ + updateView(); +} + +Execution::ClipLauncherComponent* ClipLauncherPresenter::executionComponent() const +{ + return score::findComponent(m_model.components()); +} + +void ClipLauncherPresenter::on_cellClicked(int lane, int scene) +{ + auto* cell = m_model.cellAt(lane, scene); + if(!cell) + return; + + // During execution: launch/stop cell + if(auto* exec = executionComponent()) + { + // Determine quantization: per-cell launch mode, or fall back to global + double rate = m_model.globalQuantization(); + switch(cell->launchMode()) + { + case LaunchMode::Immediate: + rate = 0.; + break; + case LaunchMode::QuantizedBeat: + rate = 4.; + break; + case LaunchMode::QuantizedBar: + rate = 1.; + break; + default: + break; // use global quantization + } + + // Toggle based on trigger style + switch(cell->triggerStyle()) + { + case TriggerStyle::Toggle: + if(cell->cellState() == CellState::Playing + || cell->cellState() == CellState::Queued) + exec->stopCell(cell->id(), rate); + else + exec->launchCell(cell->id(), rate); + break; + + case TriggerStyle::Trigger: + case TriggerStyle::Retrigger: + default: + // Always launch (will stop existing if exclusive) + exec->launchCell(cell->id(), rate); + break; + } + return; + } + + // During editing: select the cell itself + m_context.context.selectionStack.pushNewSelection({cell}); +} + +void ClipLauncherPresenter::on_cellDoubleClicked(int lane, int scene) +{ + auto* cell = m_model.cellAt(lane, scene); + if(!cell) + return; + + // Navigate into the cell's interval for editing + auto doc = score::IDocument::documentFromObject(m_model); + if(!doc) + return; + + auto base = score::IDocument::get(*doc); + if(base) + base->setDisplayedInterval(const_cast(&cell->interval())); +} + +void ClipLauncherPresenter::on_sceneLaunchClicked(int scene) +{ + if(auto* exec = executionComponent()) + { + exec->launchScene(scene); + } +} + +void ClipLauncherPresenter::on_dropOnCell(int lane, int scene, const QMimeData& mime) +{ + auto* cell = m_model.cellAt(lane, scene); + if(!cell) + return; + + // Use the existing DropProcessInInterval handler to add the process to the cell's interval + Scenario::DropProcessInInterval handler; + handler.drop(m_context.context, cell->interval(), QPointF{}, mime); +} + +void ClipLauncherPresenter::fillContextMenu( + QMenu& menu, QPoint pos, QPointF scenepos, + const Process::LayerContextMenuManager& cm) +{ + auto viewPos = m_view->mapFromScene(scenepos); + auto cellPos = m_view->cellAtPos(viewPos); + + if(cellPos) + { + int lane = cellPos->first; + int scene = cellPos->second; + auto* cell = m_model.cellAt(lane, scene); + + if(cell) + { + auto* removeAct = menu.addAction(tr("Remove cell")); + connect(removeAct, &QAction::triggered, this, [this, cell] { + CommandDispatcher<> disp{m_context.context.commandStack}; + disp.submit(m_model, *cell); + }); + } + else + { + auto* addAct = menu.addAction(tr("Add cell")); + connect(addAct, &QAction::triggered, this, [this, lane, scene] { + CommandDispatcher<> disp{m_context.context.commandStack}; + disp.submit(m_model, lane, scene); + }); + } + } + + menu.addSeparator(); + + auto* addLaneAct = menu.addAction(tr("Add lane")); + connect(addLaneAct, &QAction::triggered, this, [this] { + CommandDispatcher<> disp{m_context.context.commandStack}; + disp.submit(m_model, m_model.laneCount()); + }); + + auto* addSceneAct = menu.addAction(tr("Add scene")); + connect(addSceneAct, &QAction::triggered, this, [this] { + CommandDispatcher<> disp{m_context.context.commandStack}; + disp.submit(m_model, m_model.sceneCount()); + }); +} + +void ClipLauncherPresenter::on_laneHeaderClicked(int laneIdx) +{ + int idx = 0; + for(auto& lane : m_model.lanes) + { + if(idx == laneIdx) + { + m_context.context.selectionStack.pushNewSelection({&lane}); + return; + } + idx++; + } +} + +void ClipLauncherPresenter::on_sceneHeaderClicked(int sceneIdx) +{ + int idx = 0; + for(auto& scene : m_model.scenes) + { + if(idx == sceneIdx) + { + m_context.context.selectionStack.pushNewSelection({&scene}); + return; + } + idx++; + } +} + +void ClipLauncherPresenter::on_cellChanged(const CellModel&) { updateView(); } +void ClipLauncherPresenter::on_laneChanged(const LaneModel&) { updateView(); } +void ClipLauncherPresenter::on_sceneChanged(const SceneModel&) { updateView(); } + +void ClipLauncherPresenter::updateView() +{ + QMetaObject::invokeMethod(this, [this] { + if(m_view) + m_view->update(); + }, Qt::QueuedConnection); +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.hpp new file mode 100644 index 0000000000..b9bb989ddb --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherPresenter.hpp @@ -0,0 +1,61 @@ +#pragma once +#include + +#include + +class QTimer; + +namespace ClipLauncher +{ +class CellModel; +class LaneModel; +class SceneModel; +class ProcessModel; +class ClipLauncherView; + +namespace Execution +{ +class ClipLauncherComponent; +} + +class ClipLauncherPresenter final : public Process::LayerPresenter +{ + W_OBJECT(ClipLauncherPresenter) +public: + explicit ClipLauncherPresenter( + const ProcessModel& model, ClipLauncherView* view, + const Process::Context& ctx, QObject* parent); + ~ClipLauncherPresenter() override; + + void setWidth(qreal width, qreal defaultWidth) override; + void setHeight(qreal height) override; + void putToFront() override; + void putBehind() override; + void on_zoomRatioChanged(ZoomRatio val) override; + void parentGeometryChanged() override; + void fillContextMenu( + QMenu& menu, QPoint pos, QPointF scenepos, + const Process::LayerContextMenuManager& cm) override; + + const ProcessModel& model() const noexcept; + +private: + Execution::ClipLauncherComponent* executionComponent() const; + + void on_cellClicked(int lane, int scene); + void on_cellDoubleClicked(int lane, int scene); + void on_sceneLaunchClicked(int scene); + void on_dropOnCell(int lane, int scene, const QMimeData& mime); + void on_laneHeaderClicked(int lane); + void on_sceneHeaderClicked(int scene); + void on_cellChanged(const CellModel&); + void on_laneChanged(const LaneModel&); + void on_sceneChanged(const SceneModel&); + void updateView(); + + const ProcessModel& m_model; + ClipLauncherView* m_view; + QTimer* m_progressTimer{}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.cpp new file mode 100644 index 0000000000..be7462321a --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.cpp @@ -0,0 +1,418 @@ +#include "ClipLauncherView.hpp" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include + +W_OBJECT_IMPL(ClipLauncher::ClipLauncherView) + +namespace ClipLauncher +{ + +ClipLauncherView::ClipLauncherView(QGraphicsItem* parent) + : Process::LayerView{parent} +{ + this->setAcceptDrops(true); +} + +ClipLauncherView::~ClipLauncherView() { } + +void ClipLauncherView::setModel(const ProcessModel* model) +{ + m_model = model; + update(); +} + +double ClipLauncherView::cellWidth() const +{ + if(!m_model || m_model->laneCount() == 0) + return MaxCellWidth; + + double available = width() - SceneHeaderWidth - SceneLaunchButtonWidth; + double w = (available - CellPadding * (m_model->laneCount() - 1)) / m_model->laneCount(); + return std::clamp(w, MinCellWidth, MaxCellWidth); +} + +double ClipLauncherView::cellHeight() const +{ + if(!m_model || m_model->sceneCount() == 0) + return MaxCellHeight; + + double available = height() - LaneHeaderHeight; + double h = (available - CellPadding * (m_model->sceneCount() - 1)) / m_model->sceneCount(); + return std::clamp(h, MinCellHeight, MaxCellHeight); +} + +void ClipLauncherView::paint_impl(QPainter* painter) const +{ + if(!m_model) + return; + + painter->setRenderHint(QPainter::Antialiasing, false); + + paintLaneHeaders(painter); + paintSceneHeaders(painter); + paintGrid(painter); +} + +void ClipLauncherView::paintLaneHeaders(QPainter* painter) const +{ + const double cw = cellWidth(); + painter->setPen(Qt::white); + painter->setFont(QFont("Ubuntu", 9)); + + int laneIdx = 0; + for(const auto& lane : m_model->lanes) + { + double x = SceneHeaderWidth + SceneLaunchButtonWidth + laneIdx * (cw + CellPadding); + QRectF headerRect(x, 0, cw, LaneHeaderHeight); + painter->fillRect(headerRect, QColor(60, 60, 70)); + painter->drawText( + headerRect, Qt::AlignCenter, + lane.name().isEmpty() ? QString("Lane %1").arg(laneIdx + 1) : lane.name()); + laneIdx++; + } +} + +void ClipLauncherView::paintSceneHeaders(QPainter* painter) const +{ + const double ch = cellHeight(); + painter->setPen(Qt::white); + painter->setFont(QFont("Ubuntu", 9)); + + int sceneIdx = 0; + for(const auto& scene : m_model->scenes) + { + double y = LaneHeaderHeight + sceneIdx * (ch + CellPadding); + + // Scene name + QRectF headerRect(0, y, SceneHeaderWidth, ch); + painter->fillRect(headerRect, QColor(50, 50, 60)); + painter->drawText( + headerRect, Qt::AlignCenter, + scene.name().isEmpty() ? QString("Scene %1").arg(sceneIdx + 1) : scene.name()); + + // Scene launch button + QRectF launchRect(SceneHeaderWidth, y, SceneLaunchButtonWidth, ch); + paintSceneLaunchButton(painter, sceneIdx, launchRect); + + sceneIdx++; + } +} + +void ClipLauncherView::paintSceneLaunchButton( + QPainter* painter, int scene, const QRectF& rect) const +{ + painter->fillRect(rect, QColor(70, 70, 80)); + painter->setPen(QColor(120, 200, 120)); + + // Draw a play triangle + double cx = rect.center().x(); + double cy = rect.center().y(); + double sz = std::min(8.0, rect.height() * 0.2); + QPolygonF triangle; + triangle << QPointF(cx - sz * 0.5, cy - sz) << QPointF(cx + sz, cy) + << QPointF(cx - sz * 0.5, cy + sz); + painter->setBrush(QColor(120, 200, 120)); + painter->drawPolygon(triangle); + painter->setBrush(Qt::NoBrush); +} + +void ClipLauncherView::paintGrid(QPainter* painter) const +{ + int laneCount = m_model->laneCount(); + int sceneCount = m_model->sceneCount(); + + for(int scene = 0; scene < sceneCount; scene++) + { + for(int lane = 0; lane < laneCount; lane++) + { + QRectF rect = cellRect(lane, scene); + paintCell(painter, lane, scene, rect); + } + } +} + +void ClipLauncherView::paintCell( + QPainter* painter, int lane, int scene, const QRectF& rect) const +{ + auto* cell = m_model->cellAt(lane, scene); + + if(!cell) + { + // Empty cell + painter->fillRect(rect, QColor(40, 40, 45)); + painter->setPen(QColor(60, 60, 65)); + painter->drawRect(rect); + return; + } + + // Cell background based on state + QColor bg; + switch(cell->cellState()) + { + case CellState::Stopped: + bg = QColor(55, 55, 65); + break; + case CellState::Queued: + bg = QColor(120, 100, 30); + break; + case CellState::Playing: + bg = QColor(30, 100, 50); + break; + case CellState::Stopping: + bg = QColor(120, 60, 30); + break; + default: + bg = QColor(40, 40, 45); + break; + } + + painter->fillRect(rect, bg); + + // Cell border + painter->setPen(QColor(80, 80, 90)); + painter->drawRect(rect); + + // Cell name + painter->setPen(Qt::white); + painter->setFont(QFont("Ubuntu", 8)); + QString name = cell->interval().metadata().getName(); + if(name.isEmpty()) + name = QString("Clip %1,%2").arg(lane + 1).arg(scene + 1); + painter->drawText(rect.adjusted(4, 4, -4, -20), Qt::AlignLeft | Qt::AlignTop, name); + + // Process count indicator + int procCount = cell->interval().processes.size(); + if(procCount > 0) + { + painter->setFont(QFont("Ubuntu", 7)); + painter->setPen(QColor(150, 150, 160)); + painter->drawText( + rect.adjusted(4, 0, -4, -4), Qt::AlignLeft | Qt::AlignBottom, + QString("%1 process%2").arg(procCount).arg(procCount > 1 ? "es" : "")); + } + + // Progress bar (use fmod to loop the indicator when progress > 1) + if(cell->cellState() == CellState::Playing && cell->progress() > 0) + { + double p = std::fmod(cell->progress(), 1.0); + if(p <= 0.) + p = 1.; + double pw = rect.width() * p; + QRectF progressRect(rect.left(), rect.bottom() - 3, pw, 3); + painter->fillRect(progressRect, QColor(80, 200, 120)); + } + + // Transition rule indicator + if(!cell->transitionRules().empty()) + { + painter->setPen(QColor(200, 180, 80)); + painter->setFont(QFont("Ubuntu", 7)); + painter->drawText( + rect.adjusted(0, 4, -4, 0), Qt::AlignRight | Qt::AlignTop, + QString::fromUtf8("\xe2\x86\x92")); // → + } + + // Loop count + if(cell->loopCount() > 0) + { + painter->setPen(QColor(150, 150, 160)); + painter->setFont(QFont("Ubuntu", 7)); + painter->drawText( + rect.adjusted(0, 0, -4, -4), Qt::AlignRight | Qt::AlignBottom, + QString("x%1").arg(cell->loopCount())); + } +} + +QRectF ClipLauncherView::cellRect(int lane, int scene) const +{ + const double cw = cellWidth(); + const double ch = cellHeight(); + double x = SceneHeaderWidth + SceneLaunchButtonWidth + lane * (cw + CellPadding); + double y = LaneHeaderHeight + scene * (ch + CellPadding); + return QRectF(x, y, cw, ch); +} + +std::optional> ClipLauncherView::cellAtPos(QPointF pos) const +{ + if(!m_model) + return {}; + + const double cw = cellWidth(); + const double ch = cellHeight(); + + double x = pos.x() - SceneHeaderWidth - SceneLaunchButtonWidth; + double y = pos.y() - LaneHeaderHeight; + + if(x < 0 || y < 0) + return {}; + + int lane = static_cast(x / (cw + CellPadding)); + int scene = static_cast(y / (ch + CellPadding)); + + if(lane >= m_model->laneCount() || scene >= m_model->sceneCount()) + return {}; + + // Check we're within the cell, not in padding + double cellX = x - lane * (cw + CellPadding); + double cellY = y - scene * (ch + CellPadding); + if(cellX > cw || cellY > ch) + return {}; + + return std::make_pair(lane, scene); +} + +std::optional ClipLauncherView::sceneLaunchAtPos(QPointF pos) const +{ + if(!m_model) + return {}; + + const double ch = cellHeight(); + + double x = pos.x(); + double y = pos.y() - LaneHeaderHeight; + + if(x < SceneHeaderWidth || x > SceneHeaderWidth + SceneLaunchButtonWidth || y < 0) + return {}; + + int scene = static_cast(y / (ch + CellPadding)); + if(scene >= m_model->sceneCount()) + return {}; + + return scene; +} + +std::optional ClipLauncherView::laneHeaderAtPos(QPointF pos) const +{ + if(!m_model) + return {}; + if(pos.y() < 0 || pos.y() > LaneHeaderHeight) + return {}; + const double cw = cellWidth(); + double x = pos.x() - SceneHeaderWidth - SceneLaunchButtonWidth; + if(x < 0) + return {}; + int lane = static_cast(x / (cw + CellPadding)); + if(lane >= m_model->laneCount()) + return {}; + double cellX = x - lane * (cw + CellPadding); + if(cellX > cw) + return {}; + return lane; +} + +std::optional ClipLauncherView::sceneHeaderAtPos(QPointF pos) const +{ + if(!m_model) + return {}; + if(pos.x() < 0 || pos.x() > SceneHeaderWidth) + return {}; + const double ch = cellHeight(); + double y = pos.y() - LaneHeaderHeight; + if(y < 0) + return {}; + int scene = static_cast(y / (ch + CellPadding)); + if(scene >= m_model->sceneCount()) + return {}; + double cellY = y - scene * (ch + CellPadding); + if(cellY > ch) + return {}; + return scene; +} + +void ClipLauncherView::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + if(event->button() == Qt::LeftButton) + { + auto pos = event->pos(); + + // Check lane header + if(auto lane = laneHeaderAtPos(pos)) + { + laneHeaderClicked(*lane); + event->accept(); + return; + } + + // Check scene header + if(auto scene = sceneHeaderAtPos(pos)) + { + sceneHeaderClicked(*scene); + event->accept(); + return; + } + + // Check scene launch button + if(auto scene = sceneLaunchAtPos(pos)) + { + sceneLaunchClicked(*scene); + event->accept(); + return; + } + + // Check cell + if(auto cell = cellAtPos(pos)) + { + cellClicked(cell->first, cell->second); + event->accept(); + return; + } + } + event->ignore(); +} + +void ClipLauncherView::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) +{ + if(auto cell = cellAtPos(event->pos())) + { + cellDoubleClicked(cell->first, cell->second); + event->accept(); + return; + } + event->ignore(); +} + +void ClipLauncherView::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) +{ + // Forward to base which triggers askContextMenu -> fillContextMenu on presenter + Process::LayerView::contextMenuEvent(event); +} + +void ClipLauncherView::dragEnterEvent(QGraphicsSceneDragDropEvent* event) +{ + event->accept(); +} + +void ClipLauncherView::dragMoveEvent(QGraphicsSceneDragDropEvent* event) +{ + event->accept(); +} + +void ClipLauncherView::dragLeaveEvent(QGraphicsSceneDragDropEvent* event) +{ + event->accept(); +} + +void ClipLauncherView::dropEvent(QGraphicsSceneDragDropEvent* event) +{ + auto pos = event->pos(); + if(auto cell = cellAtPos(pos)) + { + dropOnCell(cell->first, cell->second, *event->mimeData()); + event->accept(); + return; + } + event->ignore(); +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.hpp new file mode 100644 index 0000000000..23db67cd40 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/ClipLauncherView.hpp @@ -0,0 +1,77 @@ +#pragma once +#include + +#include + +#include + +class QMimeData; +namespace ClipLauncher +{ +class ProcessModel; + +class ClipLauncherView final : public Process::LayerView +{ + W_OBJECT(ClipLauncherView) +public: + static constexpr double LaneHeaderHeight = 25.0; + static constexpr double SceneHeaderWidth = 80.0; + static constexpr double MaxCellWidth = 140.0; + static constexpr double MaxCellHeight = 70.0; + static constexpr double CellPadding = 2.0; + static constexpr double SceneLaunchButtonWidth = 25.0; + static constexpr double MinCellWidth = 40.0; + static constexpr double MinCellHeight = 25.0; + + explicit ClipLauncherView(QGraphicsItem* parent); + ~ClipLauncherView() override; + + void setModel(const ProcessModel* model); + + void paint_impl(QPainter* painter) const override; + + void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; + + void dragEnterEvent(QGraphicsSceneDragDropEvent* event) override; + void dragMoveEvent(QGraphicsSceneDragDropEvent* event) override; + void dragLeaveEvent(QGraphicsSceneDragDropEvent* event) override; + void dropEvent(QGraphicsSceneDragDropEvent* event) override; + + // Signals + void cellClicked(int lane, int scene) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, cellClicked, lane, scene) + void cellDoubleClicked(int lane, int scene) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, cellDoubleClicked, lane, scene) + void sceneLaunchClicked(int scene) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, sceneLaunchClicked, scene) + void dropOnCell(int lane, int scene, const QMimeData& mime) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, dropOnCell, lane, scene, mime) + void laneHeaderClicked(int lane) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, laneHeaderClicked, lane) + void sceneHeaderClicked(int scene) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, sceneHeaderClicked, scene) + + std::optional> cellAtPos(QPointF pos) const; + std::optional sceneLaunchAtPos(QPointF pos) const; + std::optional laneHeaderAtPos(QPointF pos) const; + std::optional sceneHeaderAtPos(QPointF pos) const; + + // Dynamic cell sizing + double cellWidth() const; + double cellHeight() const; + +private: + QRectF cellRect(int lane, int scene) const; + + void paintGrid(QPainter* painter) const; + void paintLaneHeaders(QPainter* painter) const; + void paintSceneHeaders(QPainter* painter) const; + void paintCell(QPainter* painter, int lane, int scene, const QRectF& rect) const; + void paintSceneLaunchButton(QPainter* painter, int scene, const QRectF& rect) const; + + const ProcessModel* m_model{}; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.cpp new file mode 100644 index 0000000000..0207724795 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.cpp @@ -0,0 +1,41 @@ +#include "LayerFactory.hpp" + +#include +#include +#include +#include + +#include + +namespace ClipLauncher +{ + +LayerFactory::~LayerFactory() = default; + +UuidKey LayerFactory::concreteKey() const noexcept +{ + return Metadata::get(); +} + +bool LayerFactory::matches(const UuidKey& p) const +{ + return p == Metadata::get(); +} + +Process::LayerPresenter* LayerFactory::makeLayerPresenter( + const Process::ProcessModel& model, Process::LayerView* view, + const Process::Context& context, QObject* parent) const +{ + return new ClipLauncherPresenter{ + safe_cast(model), + safe_cast(view), context, parent}; +} + +Process::LayerView* LayerFactory::makeLayerView( + const Process::ProcessModel& proc, const Process::Context& context, + QGraphicsItem* parent) const +{ + return new ClipLauncherView{parent}; +} + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.hpp new file mode 100644 index 0000000000..4d92cee7d6 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/View/LayerFactory.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +namespace ClipLauncher +{ + +class LayerFactory final : public Process::LayerFactory +{ +public: + ~LayerFactory() override; + + UuidKey concreteKey() const noexcept override; + bool matches(const UuidKey& p) const override; + + Process::LayerPresenter* makeLayerPresenter( + const Process::ProcessModel& model, Process::LayerView* view, + const Process::Context& context, QObject* parent) const override; + + Process::LayerView* + makeLayerView(const Process::ProcessModel& proc, const Process::Context& context, QGraphicsItem* parent) + const override; +}; + +} // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.cpp b/src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.cpp new file mode 100644 index 0000000000..2b4f4eacb6 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.cpp @@ -0,0 +1,66 @@ +#include "score_plugin_cliplauncher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#include + +score_plugin_cliplauncher::score_plugin_cliplauncher() { } +score_plugin_cliplauncher::~score_plugin_cliplauncher() { } + +std::vector score_plugin_cliplauncher::factories( + const score::ApplicationContext& ctx, const score::InterfaceKey& key) const +{ + return instantiate_factories< + score::ApplicationContext, + FW, + FW>(ctx, key); +} + +std::vector score_plugin_cliplauncher::guiFactories( + const score::GUIApplicationContext& ctx, const score::InterfaceKey& key) const +{ + return instantiate_factories< + score::GUIApplicationContext, + FW, + FW, + FW, + FW>(ctx, key); +} + +std::pair +score_plugin_cliplauncher::make_commands() +{ + using namespace ClipLauncher; + std::pair cmds{ + CommandFactoryName(), CommandGeneratorMap{}}; + + ossia::for_each_type< +#include + >(score::commands::FactoryInserter{cmds.second}); + + return cmds; +} + +#include +SCORE_EXPORT_PLUGIN(score_plugin_cliplauncher) diff --git a/src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.hpp b/src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.hpp new file mode 100644 index 0000000000..ffc544c730 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/score_plugin_cliplauncher.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +class score_plugin_cliplauncher final + : public score::Plugin_QtInterface + , public score::FactoryInterface_QtInterface + , public score::CommandFactory_QtInterface +{ + SCORE_PLUGIN_METADATA(1, "a8e5e9f0-1b3c-4d7e-9f2a-6c8b4d3e5f7a") + +public: + score_plugin_cliplauncher(); + ~score_plugin_cliplauncher() override; + +private: + std::vector factories( + const score::ApplicationContext& ctx, + const score::InterfaceKey& key) const override; + + std::vector guiFactories( + const score::GUIApplicationContext& ctx, + const score::InterfaceKey& key) const override; + + std::pair make_commands() override; +}; From fbcd7cccfd48b08028140b22c08c2901960663c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Fri, 6 Mar 2026 20:19:40 -0500 Subject: [PATCH 3/5] scenario: allow backwards playback --- 3rdparty/libossia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/libossia b/3rdparty/libossia index 8304bdf25d..9c311953ff 160000 --- a/3rdparty/libossia +++ b/3rdparty/libossia @@ -1 +1 @@ -Subproject commit 8304bdf25d12986c8388c2a8d142e543ec2b4004 +Subproject commit 9c311953fff167c784015c96f09233ed44ac4310 From 9bb00ddbfa5a9ec193a72e8c17ce8e7933e8e93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Fri, 6 Mar 2026 20:20:23 -0500 Subject: [PATCH 4/5] drop on cable: fix crash when a node would be changed in a separate slot upon drag and drop --- .../Application/Drops/DropOnCable.hpp | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/plugins/score-plugin-scenario/Scenario/Application/Drops/DropOnCable.hpp b/src/plugins/score-plugin-scenario/Scenario/Application/Drops/DropOnCable.hpp index 55422fde38..b460da147d 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Application/Drops/DropOnCable.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Application/Drops/DropOnCable.hpp @@ -131,7 +131,7 @@ class DropOnNode : public QObject public: const ScenarioDocumentModel& sm; const Process::Context& m_context; - const Process::NodeItem& item; + QPointer item; Scenario::IntervalModel* m_interval{}; @@ -140,13 +140,15 @@ class DropOnNode : public QObject const Process::Context& m_context) : sm{sm} , m_context{m_context} - , item{item} + , item{&item} { } void createPreset(const QByteArray& presetData) { - auto& old = item.model(); + if(!item) + return; + auto& old = item->model(); auto& procs = m_context.app.interfaces(); if(auto preset = Process::Preset::fromJson(procs, presetData)) { @@ -179,7 +181,9 @@ class DropOnNode : public QObject void createProcess(const Process::ProcessDropHandler::ProcessDrop& proc) { - auto& old = item.model(); + if(!item) + return; + auto& old = item->model(); Scenario::Command::Macro m{ new Scenario::Command::DropProcessInIntervalMacro, m_context}; score::Dispatcher_T disp{m}; @@ -200,7 +204,9 @@ class DropOnNode : public QObject void linkNewProcess(Process::ProcessModel* p, Scenario::Command::Macro& m) { - auto& old = item.model(); + if(!item) + return; + auto& old = item->model(); if(p->inlets().size() > 0) { const auto dst = p->inlets()[0]; @@ -264,7 +270,9 @@ class DropOnNode : public QObject void drop(const QMimeData& mime) { // FIXME drop in nodal vs drop in scenario - auto& model = item.model(); + if(!item) + return; + auto& model = item->model(); m_interval = qobject_cast(model.parent()); if(!m_interval) return; From 093b5c34a16d637cce3a92acfaaaf33e3d43ef7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Fri, 6 Mar 2026 20:30:10 -0500 Subject: [PATCH 5/5] clip launcher: one approach for propagation of textures --- cmake/Configurations/all-plugins.cmake | 2 + .../Process/Commands/EditPort.hpp | 2 +- .../Process/Dataflow/Port.cpp | 23 +- .../Process/Dataflow/Port.hpp | 17 +- .../Process/Dataflow/PortItem.cpp | 2 +- .../Process/Execution/ProcessComponent.hpp | 3 + .../score-plugin-cliplauncher/CMakeLists.txt | 3 + .../Commands/SetLaneProperties.cpp | 93 +++++ .../Commands/SetLaneProperties.hpp | 58 +++ .../Execution/ClipLauncherComponent.cpp | 126 ++++++- .../Execution/ClipLauncherComponent.hpp | 11 +- .../Execution/VideoMixerShader.cpp | 335 ++++++++++++++++++ .../Execution/VideoMixerShader.hpp | 22 ++ .../Inspector/LaneInspectorWidget.cpp | 57 +++ .../ClipLauncher/LaneModel.cpp | 30 +- .../ClipLauncher/LaneModel.hpp | 12 + .../ClipLauncher/ProcessModel.cpp | 36 ++ .../ClipLauncher/ProcessModel.hpp | 4 + .../ClipLauncher/Types.hpp | 43 +++ .../Dataflow/AudioOutletItem.cpp | 2 +- .../Execution/BaseScenarioComponent.cpp | 31 ++ src/plugins/score-plugin-gfx/CMakeLists.txt | 4 + .../score-plugin-gfx/Gfx/GfxForwardNode.cpp | 28 ++ .../score-plugin-gfx/Gfx/GfxForwardNode.hpp | 25 ++ .../Gfx/Graph/TextureForwardNode.cpp | 93 +++++ .../Gfx/Graph/TextureForwardNode.hpp | 23 ++ .../score-plugin-gfx/Gfx/TexturePort.cpp | 8 +- .../score-plugin-nodal/Nodal/Executor.cpp | 7 +- .../Document/Interval/IntervalExecution.cpp | 62 +++- .../Document/Interval/IntervalExecution.hpp | 6 + .../Interval/IntervalExecutionHelpers.hpp | 82 +++-- .../Scenario/Process/ScenarioExecution.cpp | 46 ++- .../Scenario/Process/ScenarioExecution.hpp | 6 + 33 files changed, 1236 insertions(+), 66 deletions(-) create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.cpp create mode 100644 src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.hpp create mode 100644 src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.cpp create mode 100644 src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.hpp create mode 100644 src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.cpp create mode 100644 src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.hpp diff --git a/cmake/Configurations/all-plugins.cmake b/cmake/Configurations/all-plugins.cmake index fef720bcf1..796cbdad0c 100644 --- a/cmake/Configurations/all-plugins.cmake +++ b/cmake/Configurations/all-plugins.cmake @@ -33,6 +33,7 @@ if(CMAKE_SYSTEM_NAME MATCHES Emscripten) score-plugin-nodal score-plugin-controlsurface + score-plugin-cliplauncher score-plugin-remotecontrol score-plugin-spline @@ -83,6 +84,7 @@ else() score-plugin-nodal score-plugin-controlsurface + score-plugin-cliplauncher score-plugin-remotecontrol score-plugin-spline diff --git a/src/plugins/score-lib-process/Process/Commands/EditPort.hpp b/src/plugins/score-lib-process/Process/Commands/EditPort.hpp index c28c508e4a..ac2bd05ea2 100644 --- a/src/plugins/score-lib-process/Process/Commands/EditPort.hpp +++ b/src/plugins/score-lib-process/Process/Commands/EditPort.hpp @@ -37,7 +37,7 @@ class SCORE_LIB_PROCESS_EXPORT ChangePortSettings final : public score::Command } PROPERTY_COMMAND_T( - Process, SetPropagate, AudioOutlet::p_propagate, "Set port propagation") + Process, SetPropagate, Outlet::p_propagate, "Set port propagation") SCORE_COMMAND_DECL_T(Process::SetPropagate) PROPERTY_COMMAND_T(Process, ChangePortAddress, Port::p_address, "Set port address") diff --git a/src/plugins/score-lib-process/Process/Dataflow/Port.cpp b/src/plugins/score-lib-process/Process/Dataflow/Port.cpp index b19562c225..99b5ca769b 100644 --- a/src/plugins/score-lib-process/Process/Dataflow/Port.cpp +++ b/src/plugins/score-lib-process/Process/Dataflow/Port.cpp @@ -374,6 +374,15 @@ Outlet::~Outlet() { } void Outlet::setupExecution(ossia::outlet&, QObject* exec_context) const noexcept { } +bool Outlet::propagate() const noexcept { return m_propagate; } + +void Outlet::setPropagate(bool p) { + if(m_propagate != p) { + m_propagate = p; + propagateChanged(m_propagate); + } +} + Outlet::Outlet(const QString& name, Id c, QObject* parent) : Port{std::move(c), QStringLiteral("Outlet"), parent} { @@ -509,20 +518,6 @@ void AudioOutlet::loadData(const QByteArray& arr, PortLoadDataFlags flags) noexc propagateChanged(m_propagate); } -bool AudioOutlet::propagate() const -{ - return m_propagate; -} - -void AudioOutlet::setPropagate(bool propagate) -{ - if(m_propagate == propagate) - return; - - m_propagate = propagate; - propagateChanged(m_propagate); -} - double AudioOutlet::gain() const { return m_gain; diff --git a/src/plugins/score-lib-process/Process/Dataflow/Port.hpp b/src/plugins/score-lib-process/Process/Dataflow/Port.hpp index dd755f0c3a..d2fb85fe73 100644 --- a/src/plugins/score-lib-process/Process/Dataflow/Port.hpp +++ b/src/plugins/score-lib-process/Process/Dataflow/Port.hpp @@ -287,7 +287,16 @@ class SCORE_LIB_PROCESS_EXPORT Outlet : public Port ossia::outlet&, const smallfun::function&) const noexcept; + virtual bool propagate() const noexcept; + virtual void setPropagate(bool p); + void propagateChanged(bool propagate) + E_SIGNAL(SCORE_LIB_PROCESS_EXPORT, propagateChanged, propagate) + + PROPERTY(bool, propagate W_READ propagate W_WRITE setPropagate W_NOTIFY propagateChanged) + protected: + bool m_propagate{false}; + Outlet() = delete; Outlet(const Outlet&) = delete; Outlet(const QString& name, Id c, QObject* parent); @@ -351,11 +360,6 @@ class SCORE_LIB_PROCESS_EXPORT AudioOutlet : public Outlet QByteArray saveData() const noexcept override; void loadData(const QByteArray& arr, PortLoadDataFlags = {}) noexcept override; - bool propagate() const; - void setPropagate(bool propagate); - void propagateChanged(bool propagate) - E_SIGNAL(SCORE_LIB_PROCESS_EXPORT, propagateChanged, propagate) - double gain() const; void setGain(double g); void gainChanged(double g) E_SIGNAL(SCORE_LIB_PROCESS_EXPORT, gainChanged, g) @@ -367,14 +371,11 @@ class SCORE_LIB_PROCESS_EXPORT AudioOutlet : public Outlet std::unique_ptr gainInlet; std::unique_ptr panInlet; - PROPERTY( - bool, propagate W_READ propagate W_WRITE setPropagate W_NOTIFY propagateChanged) PROPERTY(double, gain W_READ gain W_WRITE setGain W_NOTIFY gainChanged) PROPERTY(pan_weight, pan W_READ pan W_WRITE setPan W_NOTIFY panChanged) private: double m_gain{}; pan_weight m_pan; - bool m_propagate{false}; }; class SCORE_LIB_PROCESS_EXPORT MidiInlet : public Inlet diff --git a/src/plugins/score-lib-process/Process/Dataflow/PortItem.cpp b/src/plugins/score-lib-process/Process/Dataflow/PortItem.cpp index 84bc85de09..1f641b45c2 100644 --- a/src/plugins/score-lib-process/Process/Dataflow/PortItem.cpp +++ b/src/plugins/score-lib-process/Process/Dataflow/PortItem.cpp @@ -440,7 +440,7 @@ PortItem::PortItem( auto ap = static_cast(&m_port); m_address->has_propagate = ap->propagate(); connect( - ap, &Process::AudioOutlet::propagateChanged, this, + ap, &Process::Outlet::propagateChanged, this, [a = m_address](bool propagate) { a->has_propagate = propagate; a->update(); diff --git a/src/plugins/score-lib-process/Process/Execution/ProcessComponent.hpp b/src/plugins/score-lib-process/Process/Execution/ProcessComponent.hpp index e39be38f40..9cd6d19f6a 100644 --- a/src/plugins/score-lib-process/Process/Execution/ProcessComponent.hpp +++ b/src/plugins/score-lib-process/Process/Execution/ProcessComponent.hpp @@ -77,6 +77,9 @@ class SCORE_LIB_PROCESS_EXPORT ProcessComponent std::shared_ptr node; + //! Override to expose a gfx_forward_node for texture propagation + virtual std::shared_ptr gfxForwardNode() const { return {}; } + public: void nodeChanged( const ossia::node_ptr& old_node, const ossia::node_ptr& new_node, diff --git a/src/plugins/score-plugin-cliplauncher/CMakeLists.txt b/src/plugins/score-plugin-cliplauncher/CMakeLists.txt index ada5129673..046b6b2b7c 100644 --- a/src/plugins/score-plugin-cliplauncher/CMakeLists.txt +++ b/src/plugins/score-plugin-cliplauncher/CMakeLists.txt @@ -30,6 +30,7 @@ set(HDRS ClipLauncher/Commands/RemoveTransitionRule.hpp ClipLauncher/Execution/ClipLauncherComponent.hpp + ClipLauncher/Execution/VideoMixerShader.hpp ClipLauncher/Inspector/CellInspectorWidget.hpp ClipLauncher/Inspector/CellInspectorFactory.hpp @@ -67,6 +68,7 @@ set(SRCS ClipLauncher/Commands/RemoveTransitionRule.cpp ClipLauncher/Execution/ClipLauncherComponent.cpp + ClipLauncher/Execution/VideoMixerShader.cpp ClipLauncher/Inspector/CellInspectorWidget.cpp ClipLauncher/Inspector/CellInspectorFactory.cpp @@ -95,6 +97,7 @@ target_link_libraries(${PROJECT_NAME} score_plugin_scenario score_plugin_engine score_lib_inspector + score_plugin_gfx ) # Target-specific options diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp index 9468168981..f19842fa4b 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.cpp @@ -70,4 +70,97 @@ void SetLaneExclusivityMode::deserializeImpl(DataStreamOutput& s) s >> m_path >> m_laneId >> m_old >> m_new; } +// --- SetLaneVolume --- + +SetLaneVolume::SetLaneVolume( + const ProcessModel& proc, const LaneModel& lane, double newVolume) + : m_path{proc} + , m_laneId{lane.id()} + , m_old{lane.volume()} + , m_new{newVolume} +{ +} + +void SetLaneVolume::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setVolume(m_old); +} + +void SetLaneVolume::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setVolume(m_new); +} + +void SetLaneVolume::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_laneId << m_old << m_new; +} + +void SetLaneVolume::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_laneId >> m_old >> m_new; +} + +// --- SetLaneBlendMode --- + +SetLaneBlendMode::SetLaneBlendMode( + const ProcessModel& proc, const LaneModel& lane, VideoBlendMode newMode) + : m_path{proc} + , m_laneId{lane.id()} + , m_old{lane.blendMode()} + , m_new{newMode} +{ +} + +void SetLaneBlendMode::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setBlendMode(m_old); +} + +void SetLaneBlendMode::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setBlendMode(m_new); +} + +void SetLaneBlendMode::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_laneId << m_old << m_new; +} + +void SetLaneBlendMode::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_laneId >> m_old >> m_new; +} + +// --- SetLaneVideoOpacity --- + +SetLaneVideoOpacity::SetLaneVideoOpacity( + const ProcessModel& proc, const LaneModel& lane, double newOpacity) + : m_path{proc} + , m_laneId{lane.id()} + , m_old{lane.videoOpacity()} + , m_new{newOpacity} +{ +} + +void SetLaneVideoOpacity::undo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setVideoOpacity(m_old); +} + +void SetLaneVideoOpacity::redo(const score::DocumentContext& ctx) const +{ + m_path.find(ctx).lanes.at(m_laneId).setVideoOpacity(m_new); +} + +void SetLaneVideoOpacity::serializeImpl(DataStreamInput& s) const +{ + s << m_path << m_laneId << m_old << m_new; +} + +void SetLaneVideoOpacity::deserializeImpl(DataStreamOutput& s) +{ + s >> m_path >> m_laneId >> m_old >> m_new; +} + } // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp index 6c1ff9e16b..ac06c61fac 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Commands/SetLaneProperties.hpp @@ -51,4 +51,62 @@ class SetLaneExclusivityMode final : public score::Command ExclusivityMode m_old, m_new; }; +class SetLaneVolume final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetLaneVolume, "Set lane volume") +public: + SetLaneVolume(const ProcessModel& proc, const LaneModel& lane, double newVolume); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_laneId; + double m_old, m_new; +}; + +class SetLaneBlendMode final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetLaneBlendMode, "Set lane blend mode") +public: + SetLaneBlendMode( + const ProcessModel& proc, const LaneModel& lane, VideoBlendMode newMode); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_laneId; + VideoBlendMode m_old, m_new; +}; + +class SetLaneVideoOpacity final : public score::Command +{ + SCORE_COMMAND_DECL(CommandFactoryName(), SetLaneVideoOpacity, "Set lane video opacity") +public: + SetLaneVideoOpacity(const ProcessModel& proc, const LaneModel& lane, double newOpacity); + + void undo(const score::DocumentContext& ctx) const override; + void redo(const score::DocumentContext& ctx) const override; + +protected: + void serializeImpl(DataStreamInput& s) const override; + void deserializeImpl(DataStreamOutput& s) override; + +private: + Path m_path; + Id m_laneId; + double m_old, m_new; +}; + } // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp index d133ce01f7..11899fcc47 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.cpp @@ -1,9 +1,15 @@ #include "ClipLauncherComponent.hpp" #include +#include #include +#include + +#include +#include + #include #include #include @@ -21,6 +27,8 @@ #include #include #include +#include +#include #include namespace ClipLauncher::Execution @@ -35,11 +43,46 @@ ClipLauncherComponent::ClipLauncherComponent( m_scenario = std::make_shared(); m_ossia_process = m_scenario; + // Create a gfx_forward_node for texture propagation + if(auto* gfxPlug = ctx.doc.findPlugin()) + { + m_gfxForwardNode = std::make_shared(gfxPlug->exec); + m_gfxForwardNode->prepare(*ctx.execState); + std::weak_ptr g_weak = ctx.execGraph; + in_exec([g_weak, node = m_gfxForwardNode] { + if(auto graph = g_weak.lock()) + graph->add_node(node); + }); + } + // Setup execution structures for each existing cell for(auto& cell : element.cells) { setupCell(cell); } + + // Connect lane property change signals for volume + for(auto& lane : element.lanes) + { + int idx = laneIndex(lane); + con(lane, &LaneModel::volumeChanged, this, [this, idx](double v) { + // Update gain on all active cells in this lane + for(auto& [cellId, data] : m_cells) + { + auto cellIt = process().cells.find(cellId); + if(cellIt == process().cells.end()) + continue; + if(cellIt->lane() == idx && data.interval) + { + auto itv = data.interval; + in_exec([itv, v] { + auto& ao = static_cast(itv->node.get())->audio_out; + ao.gain = v; + }); + } + } + }); + } } ClipLauncherComponent::~ClipLauncherComponent() { } @@ -67,6 +110,17 @@ void ClipLauncherComponent::cleanup() m_cells.clear(); m_activeCellPerLane.clear(); + + if(m_gfxForwardNode) + { + std::weak_ptr g_weak = system().execGraph; + in_exec([g_weak, node = m_gfxForwardNode] { + if(auto graph = g_weak.lock()) + graph->remove_node(node); + }); + m_gfxForwardNode.reset(); + } + m_ossia_process.reset(); m_scenario.reset(); } @@ -95,10 +149,9 @@ void ClipLauncherComponent::setupCell(CellModel& cell) auto minDur = ctx.time(itv.duration.minDuration()); auto maxDur = ctx.time(itv.duration.maxDuration()); - // create() both constructs AND links the interval into the events' lists - // (startEvent.next_time_intervals, endEvent.previous_time_intervals) data.interval = ossia::time_interval::create( - {}, *data.startEvent, *data.endEvent, dur, minDur, maxDur); + ossia::time_interval::exec_callback{}, + *data.startEvent, *data.endEvent, dur, minDur, maxDur); // Prepare the interval's node for execution (required before onSetup) data.interval->node->prepare(*ctx.execState); @@ -108,6 +161,19 @@ void ClipLauncherComponent::setupCell(CellModel& cell) m_scenario->add_time_sync(data.endSync); m_scenario->add_time_interval(data.interval); + // Ensure all texture outlets in this cell have propagate=true + // so they automatically route through gfx_forward_node chain + for(auto& proc : itv.processes) + { + for(auto* outlet : proc.outlets()) + { + if(outlet->type() == Process::PortType::Texture) + { + outlet->setPropagate(true); + } + } + } + // Create execution components for the score model elements data.startSyncComponent = std::make_shared<::Execution::TimeSyncComponent>( scenario.startTimeSync(), ctx, this); @@ -149,6 +215,26 @@ void ClipLauncherComponent::setupCell(CellModel& cell) { auto ossia_itv = data.interval; auto proc = m_scenario; + + // Apply per-lane volume via the interval node's gain + int laneIdx = cell.lane(); + double vol = 1.0; + { + int li = 0; + for(auto& l : process().lanes) + { + if(li == laneIdx) + { + vol = l.volume(); + break; + } + li++; + } + } + auto* fwd = static_cast(ossia_itv->node.get()); + fwd->audio_out.has_gain = true; + fwd->audio_out.gain = vol; + in_exec([g = ctx.execGraph, proc, ossia_itv] { if(!ossia_itv->node->root_outputs().empty() && !proc->node->root_inputs().empty()) @@ -162,6 +248,24 @@ void ClipLauncherComponent::setupCell(CellModel& cell) }); } + // Connect interval's gfx_forward_node to clip launcher's gfx_forward_node + { + auto itv_gfx_fw = data.intervalComponent->gfxForwardNode(); + auto cl_gfx_fw = m_gfxForwardNode; + if(itv_gfx_fw && cl_gfx_fw + && !itv_gfx_fw->root_outputs().empty() + && !cl_gfx_fw->root_inputs().empty()) + { + in_exec([g = ctx.execGraph, itv_gfx_fw, cl_gfx_fw] { + auto cable = g->allocate_edge( + ossia::immediate_glutton_connection{}, + itv_gfx_fw->root_outputs()[0], cl_gfx_fw->root_inputs()[0], + itv_gfx_fw, cl_gfx_fw); + g->connect(cable); + }); + } + } + // Connect interval execution events to cell state auto cellId = cell.id(); con(itv, &Scenario::IntervalModel::executionEvent, this, @@ -237,7 +341,9 @@ void ClipLauncherComponent::stopCell( // Remove from active tracking auto& element = process(); auto& cell = element.cells.at(cellId); - auto laneIt = m_activeCellPerLane.find(cell.lane()); + int lane = cell.lane(); + + auto laneIt = m_activeCellPerLane.find(lane); if(laneIt != m_activeCellPerLane.end() && laneIt->second == cellId) m_activeCellPerLane.erase(laneIt); @@ -287,4 +393,16 @@ void ClipLauncherComponent::stopAllInLane(int lane, double quantizationRate) } } +int ClipLauncherComponent::laneIndex(const LaneModel& lane) const +{ + int idx = 0; + for(auto& l : process().lanes) + { + if(&l == &lane) + return idx; + idx++; + } + return -1; +} + } // namespace ClipLauncher::Execution diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp index f3f8669cbd..9a9049fde1 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/ClipLauncherComponent.hpp @@ -11,6 +11,7 @@ class scenario; class time_interval; class time_sync; class time_event; +class time_process; } namespace Execution @@ -24,11 +25,11 @@ class StateComponent; namespace ClipLauncher { class CellModel; +class LaneModel; class ProcessModel; namespace Execution { - // Per-cell execution data struct CellExecData { @@ -61,6 +62,11 @@ class ClipLauncherComponent final void cleanup() override; + std::shared_ptr gfxForwardNode() const override + { + return m_gfxForwardNode; + } + // Launch/stop cells void launchCell(const Id& cellId, double quantizationRate = 0.); void stopCell(const Id& cellId, double quantizationRate = 0.); @@ -70,7 +76,10 @@ class ClipLauncherComponent final void setupCell(CellModel& cell); void stopAllInLane(int lane, double quantizationRate); + int laneIndex(const LaneModel& lane) const; + std::shared_ptr m_scenario; + std::shared_ptr m_gfxForwardNode; score::hash_map, CellExecData> m_cells; score::hash_map> m_activeCellPerLane; // lane -> active cell }; diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.cpp new file mode 100644 index 0000000000..77fed90cb1 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.cpp @@ -0,0 +1,335 @@ +#include "VideoMixerShader.hpp" + +#include +#include +#include + +namespace ClipLauncher::Execution +{ + +const QString& VideoMixerShader::blendFunctions() +{ + static const QString funcs = QStringLiteral(R"GLSL( +vec3 blendPhoenix(vec3 base, vec3 blend) { + return min(base, blend) - max(base, blend) + vec3(1.0); +} +vec3 blendPhoenix(vec3 base, vec3 blend, float opacity) { + return (blendPhoenix(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendOverlay(float base, float blend) { + return base < 0.5 ? (2.0 * base * blend) + : (1.0 - 2.0 * (1.0 - base) * (1.0 - blend)); +} +vec3 blendOverlay(vec3 base, vec3 blend) { + return vec3(blendOverlay(base.r, blend.r), blendOverlay(base.g, blend.g), + blendOverlay(base.b, blend.b)); +} +vec3 blendOverlay(vec3 base, vec3 blend, float opacity) { + return (blendOverlay(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendNormal(vec3 base, vec3 blend) { return blend; } +vec3 blendNormal(vec3 base, vec3 blend, float opacity) { + return (blendNormal(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendNegation(vec3 base, vec3 blend) { + return vec3(1.0) - abs(vec3(1.0) - base - blend); +} +vec3 blendNegation(vec3 base, vec3 blend, float opacity) { + return (blendNegation(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendMultiply(vec3 base, vec3 blend) { return base * blend; } +vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { + return (blendMultiply(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendReflect(float base, float blend) { + return (blend == 1.0) ? blend : min(base * base / (1.0 - blend), 1.0); +} +vec3 blendReflect(vec3 base, vec3 blend) { + return vec3(blendReflect(base.r, blend.r), blendReflect(base.g, blend.g), + blendReflect(base.b, blend.b)); +} +vec3 blendReflect(vec3 base, vec3 blend, float opacity) { + return (blendReflect(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendAverage(vec3 base, vec3 blend) { return (base + blend) / 2.0; } +vec3 blendAverage(vec3 base, vec3 blend, float opacity) { + return (blendAverage(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendLinearBurn(float base, float blend) { + return max(base + blend - 1.0, 0.0); +} +vec3 blendLinearBurn(vec3 base, vec3 blend) { + return max(base + blend - vec3(1.0), vec3(0.0)); +} +vec3 blendLinearBurn(vec3 base, vec3 blend, float opacity) { + return (blendLinearBurn(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendLighten(float base, float blend) { return max(blend, base); } +vec3 blendLighten(vec3 base, vec3 blend) { + return vec3(blendLighten(base.r, blend.r), blendLighten(base.g, blend.g), + blendLighten(base.b, blend.b)); +} +vec3 blendLighten(vec3 base, vec3 blend, float opacity) { + return (blendLighten(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendScreen(float base, float blend) { + return 1.0 - ((1.0 - base) * (1.0 - blend)); +} +vec3 blendScreen(vec3 base, vec3 blend) { + return vec3(blendScreen(base.r, blend.r), blendScreen(base.g, blend.g), + blendScreen(base.b, blend.b)); +} +vec3 blendScreen(vec3 base, vec3 blend, float opacity) { + return (blendScreen(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendSoftLight(float base, float blend) { + return (blend < 0.5) + ? (2.0 * base * blend + base * base * (1.0 - 2.0 * blend)) + : (sqrt(base) * (2.0 * blend - 1.0) + 2.0 * base * (1.0 - blend)); +} +vec3 blendSoftLight(vec3 base, vec3 blend) { + return vec3(blendSoftLight(base.r, blend.r), blendSoftLight(base.g, blend.g), + blendSoftLight(base.b, blend.b)); +} +vec3 blendSoftLight(vec3 base, vec3 blend, float opacity) { + return (blendSoftLight(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendSubtract(float base, float blend) { + return max(base + blend - 1.0, 0.0); +} +vec3 blendSubtract(vec3 base, vec3 blend) { + return max(base + blend - vec3(1.0), vec3(0.0)); +} +vec3 blendSubtract(vec3 base, vec3 blend, float opacity) { + return (blendSubtract(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendExclusion(vec3 base, vec3 blend) { + return base + blend - 2.0 * base * blend; +} +vec3 blendExclusion(vec3 base, vec3 blend, float opacity) { + return (blendExclusion(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendDifference(vec3 base, vec3 blend) { return abs(base - blend); } +vec3 blendDifference(vec3 base, vec3 blend, float opacity) { + return (blendDifference(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendDarken(float base, float blend) { return min(blend, base); } +vec3 blendDarken(vec3 base, vec3 blend) { + return vec3(blendDarken(base.r, blend.r), blendDarken(base.g, blend.g), + blendDarken(base.b, blend.b)); +} +vec3 blendDarken(vec3 base, vec3 blend, float opacity) { + return (blendDarken(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendColorDodge(float base, float blend) { + return (blend == 1.0) ? blend : min(base / (1.0 - blend), 1.0); +} +vec3 blendColorDodge(vec3 base, vec3 blend) { + return vec3(blendColorDodge(base.r, blend.r), + blendColorDodge(base.g, blend.g), + blendColorDodge(base.b, blend.b)); +} +vec3 blendColorDodge(vec3 base, vec3 blend, float opacity) { + return (blendColorDodge(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendColorBurn(float base, float blend) { + return (blend == 0.0) ? blend : max((1.0 - ((1.0 - base) / blend)), 0.0); +} +vec3 blendColorBurn(vec3 base, vec3 blend) { + return vec3(blendColorBurn(base.r, blend.r), blendColorBurn(base.g, blend.g), + blendColorBurn(base.b, blend.b)); +} +vec3 blendColorBurn(vec3 base, vec3 blend, float opacity) { + return (blendColorBurn(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendAdd(float base, float blend) { return min(base + blend, 1.0); } +vec3 blendAdd(vec3 base, vec3 blend) { return min(base + blend, vec3(1.0)); } +vec3 blendAdd(vec3 base, vec3 blend, float opacity) { + return (blendAdd(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendLinearDodge(float base, float blend) { + return min(base + blend, 1.0); +} +vec3 blendLinearDodge(vec3 base, vec3 blend) { + return min(base + blend, vec3(1.0)); +} +vec3 blendLinearDodge(vec3 base, vec3 blend, float opacity) { + return (blendLinearDodge(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendHardLight(vec3 base, vec3 blend) { return blendOverlay(blend, base); } +vec3 blendHardLight(vec3 base, vec3 blend, float opacity) { + return (blendHardLight(base, blend) * opacity + base * (1.0 - opacity)); +} +vec3 blendGlow(vec3 base, vec3 blend) { return blendReflect(blend, base); } +vec3 blendGlow(vec3 base, vec3 blend, float opacity) { + return (blendGlow(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendVividLight(float base, float blend) { + return (blend < 0.5) ? blendColorBurn(base, (2.0 * blend)) + : blendColorDodge(base, (2.0 * (blend - 0.5))); +} +vec3 blendVividLight(vec3 base, vec3 blend) { + return vec3(blendVividLight(base.r, blend.r), + blendVividLight(base.g, blend.g), + blendVividLight(base.b, blend.b)); +} +vec3 blendVividLight(vec3 base, vec3 blend, float opacity) { + return (blendVividLight(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendHardMix(float base, float blend) { + return (blendVividLight(base, blend) < 0.5) ? 0.0 : 1.0; +} +vec3 blendHardMix(vec3 base, vec3 blend) { + return vec3(blendHardMix(base.r, blend.r), blendHardMix(base.g, blend.g), + blendHardMix(base.b, blend.b)); +} +vec3 blendHardMix(vec3 base, vec3 blend, float opacity) { + return (blendHardMix(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendLinearLight(float base, float blend) { + return blend < 0.5 ? blendLinearBurn(base, (2.0 * blend)) + : blendLinearDodge(base, (2.0 * (blend - 0.5))); +} +vec3 blendLinearLight(vec3 base, vec3 blend) { + return vec3(blendLinearLight(base.r, blend.r), + blendLinearLight(base.g, blend.g), + blendLinearLight(base.b, blend.b)); +} +vec3 blendLinearLight(vec3 base, vec3 blend, float opacity) { + return (blendLinearLight(base, blend) * opacity + base * (1.0 - opacity)); +} +float blendPinLight(float base, float blend) { + return (blend < 0.5) ? blendDarken(base, (2.0 * blend)) + : blendLighten(base, (2.0 * (blend - 0.5))); +} +vec3 blendPinLight(vec3 base, vec3 blend) { + return vec3(blendPinLight(base.r, blend.r), blendPinLight(base.g, blend.g), + blendPinLight(base.b, blend.b)); +} +vec3 blendPinLight(vec3 base, vec3 blend, float opacity) { + return (blendPinLight(base, blend) * opacity + base * (1.0 - opacity)); +} + +vec3 blendMode(int mode, vec3 base, vec3 blend, float opacity) { + if (mode == 1) return blendAdd(base, blend, opacity); + else if (mode == 2) return blendAverage(base, blend, opacity); + else if (mode == 3) return blendColorBurn(base, blend, opacity); + else if (mode == 4) return blendColorDodge(base, blend, opacity); + else if (mode == 5) return blendDarken(base, blend, opacity); + else if (mode == 6) return blendDifference(base, blend, opacity); + else if (mode == 7) return blendExclusion(base, blend, opacity); + else if (mode == 8) return blendGlow(base, blend, opacity); + else if (mode == 9) return blendHardLight(base, blend, opacity); + else if (mode == 10) return blendHardMix(base, blend, opacity); + else if (mode == 11) return blendLighten(base, blend, opacity); + else if (mode == 12) return blendLinearBurn(base, blend, opacity); + else if (mode == 13) return blendLinearDodge(base, blend, opacity); + else if (mode == 14) return blendLinearLight(base, blend, opacity); + else if (mode == 15) return blendMultiply(base, blend, opacity); + else if (mode == 16) return blendNegation(base, blend, opacity); + else if (mode == 17) return blendNormal(base, blend, opacity); + else if (mode == 18) return blendOverlay(base, blend, opacity); + else if (mode == 19) return blendPhoenix(base, blend, opacity); + else if (mode == 20) return blendPinLight(base, blend, opacity); + else if (mode == 21) return blendReflect(base, blend, opacity); + else if (mode == 22) return blendScreen(base, blend, opacity); + else if (mode == 23) return blendSoftLight(base, blend, opacity); + else if (mode == 24) return blendSubtract(base, blend, opacity); + else if (mode == 25) return blendVividLight(base, blend, opacity); + else return vec3(0.,0.,0.); +} +)GLSL"); + return funcs; +} + +static QJsonObject blendModeComboInput(const QString& name, const QString& label) +{ + return QJsonObject{ + {"NAME", name}, + {"LABEL", label}, + {"TYPE", "long"}, + {"DEFAULT", 17}, // Normal + {"IDENTITY", 1}, + {"VALUES", QJsonArray{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}}, + {"LABELS", + QJsonArray{"Add", "Average", "Color Burn", "Color Dodge", + "Darken", "Difference", "Exclusion", "Glow", + "Hard Light", "Hard Mix", "Lighten", "Linear Burn", + "Linear Dodge","Linear Light", "Multiply", "Negation", + "Normal", "Overlay", "Phoenix", "Pin Light", + "Reflect", "Screen", "Soft Light", "Subtract", + "Vivid Light"}}}; +} + +QString VideoMixerShader::generate(int numInputs) +{ + if(numInputs < 1) + numInputs = 1; + + // Build ISF JSON header + QJsonArray inputs; + for(int i = 1; i <= numInputs; i++) + { + inputs.push_back(QJsonObject{ + {"NAME", QStringLiteral("t%1").arg(i)}, + {"LABEL", QStringLiteral("Texture %1").arg(i)}, + {"TYPE", "image"}}); + } + for(int i = 1; i <= numInputs; i++) + { + inputs.push_back(QJsonObject{ + {"NAME", QStringLiteral("alpha%1").arg(i)}, + {"LABEL", QStringLiteral("Alpha %1").arg(i)}, + {"TYPE", "float"}, + {"DEFAULT", 0}, + {"MIN", 0}, + {"MAX", 1}}); + } + for(int i = 1; i < numInputs; i++) + { + inputs.push_back( + blendModeComboInput( + QStringLiteral("mode%1").arg(i), + QStringLiteral("Mode %1").arg(i))); + } + + QJsonObject header{ + {"ISFVSN", "2"}, + {"DESCRIPTION", "Auto-generated video mixer"}, + {"INPUTS", inputs}}; + + QString isfHeader = QStringLiteral("/*") + QJsonDocument{header}.toJson(QJsonDocument::Compact) + + QStringLiteral("*/\n"); + + // Build main() function + QString main = QStringLiteral("void main() {\n"); + for(int i = 1; i <= numInputs; i++) + main += QStringLiteral(" vec4 s%1 = IMG_THIS_PIXEL(t%1);\n").arg(i); + + main += QStringLiteral("\n float a = s1.a * alpha1;\n"); + main += QStringLiteral(" vec3 rgb = s1.rgb * a;\n"); + main += QStringLiteral(" float ea;\n\n"); + + for(int i = 2; i <= numInputs; i++) + { + main += QStringLiteral(" ea = s%1.a * alpha%1;\n").arg(i); + main += QStringLiteral(" rgb = blendMode(mode%1, rgb, s%2.rgb, ea);\n") + .arg(i - 1) + .arg(i); + main += QStringLiteral(" a = a + ea * (1.0 - a);\n\n"); + } + + main += QStringLiteral(" gl_FragColor = vec4(rgb, a);\n}\n"); + + return isfHeader + blendFunctions() + main; +} + +isf::descriptor VideoMixerShader::parseDescriptor(int numInputs) +{ + auto shader = generate(numInputs); + auto [_, desc] = isf::parser::parse_isf_header(shader.toStdString()); + return desc; +} + +} // namespace ClipLauncher::Execution diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.hpp new file mode 100644 index 0000000000..2203f55b89 --- /dev/null +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Execution/VideoMixerShader.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +#include + +namespace ClipLauncher::Execution +{ + +// Generates a Video Mixer ISF shader at runtime with exactly N inputs +struct VideoMixerShader +{ + // The constant blend mode GLSL function implementations + static const QString& blendFunctions(); + + // Generate a complete ISF fragment shader for N inputs + static QString generate(int numInputs); + + // Parse the generated shader to get the ISF descriptor + static isf::descriptor parseDescriptor(int numInputs); +}; + +} // namespace ClipLauncher::Execution diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp index d39026cab2..bab2533399 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Inspector/LaneInspectorWidget.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -60,6 +61,62 @@ LaneInspectorWidget::LaneInspectorWidget( [exclCombo](ExclusivityMode m) { exclCombo->setCurrentIndex(static_cast(m)); }); lay->addRow(tr("Exclusivity"), exclCombo); + // Blend mode + auto blendCombo = new QComboBox; + blendCombo->addItems( + {tr("Add"), tr("Average"), tr("Color Burn"), tr("Color Dodge"), tr("Darken"), + tr("Difference"), tr("Exclusion"), tr("Glow"), tr("Hard Light"), tr("Hard Mix"), + tr("Lighten"), tr("Linear Burn"), tr("Linear Dodge"), tr("Linear Light"), + tr("Multiply"), tr("Negation"), tr("Normal"), tr("Overlay"), tr("Phoenix"), + tr("Pin Light"), tr("Reflect"), tr("Screen"), tr("Soft Light"), tr("Subtract"), + tr("Vivid Light")}); + // Enum values start at 1, combo index starts at 0 + blendCombo->setCurrentIndex(static_cast(lane.blendMode()) - 1); + connect( + blendCombo, SignalUtils::QComboBox_currentIndexChanged_int(), this, + [this](int idx) { + auto newMode = static_cast(idx + 1); + if(newMode != m_lane.blendMode()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_lane, newMode); + } + }); + con(lane, &LaneModel::blendModeChanged, this, [blendCombo](VideoBlendMode m) { + blendCombo->setCurrentIndex(static_cast(m) - 1); + }); + lay->addRow(tr("Blend Mode"), blendCombo); + + // Video opacity + auto opacitySpin = new QDoubleSpinBox; + opacitySpin->setRange(0.0, 1.0); + opacitySpin->setSingleStep(0.05); + opacitySpin->setValue(lane.videoOpacity()); + connect(opacitySpin, &QDoubleSpinBox::valueChanged, this, [this](double v) { + if(v != m_lane.videoOpacity()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_lane, v); + } + }); + con(lane, &LaneModel::videoOpacityChanged, opacitySpin, &QDoubleSpinBox::setValue); + lay->addRow(tr("Video Opacity"), opacitySpin); + + // Volume + auto volumeSpin = new QDoubleSpinBox; + volumeSpin->setRange(0.0, 2.0); + volumeSpin->setSingleStep(0.05); + volumeSpin->setValue(lane.volume()); + connect(volumeSpin, &QDoubleSpinBox::valueChanged, this, [this](double v) { + if(v != m_lane.volume()) + { + CommandDispatcher<> disp{m_ctx.commandStack}; + disp.submit(parentProcess(), m_lane, v); + } + }); + con(lane, &LaneModel::volumeChanged, volumeSpin, &QDoubleSpinBox::setValue); + lay->addRow(tr("Volume"), volumeSpin); + updateAreaLayout({w}); } diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp index a149384495..46fbde1e94 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.cpp @@ -61,13 +61,32 @@ void LaneModel::setVolume(double v) } } +void LaneModel::setBlendMode(VideoBlendMode m) +{ + if(m_blendMode != m) + { + m_blendMode = m; + blendModeChanged(m); + } +} + +void LaneModel::setVideoOpacity(double v) +{ + if(m_videoOpacity != v) + { + m_videoOpacity = v; + videoOpacityChanged(v); + } +} + } // namespace ClipLauncher template <> void DataStreamReader::read(const ClipLauncher::LaneModel& lane) { m_stream << lane.m_name << lane.m_exclusivityMode << lane.m_temporalMode - << lane.m_crossfadeDuration << lane.m_volume; + << lane.m_crossfadeDuration << lane.m_volume << lane.m_blendMode + << lane.m_videoOpacity; insertDelimiter(); } @@ -75,7 +94,8 @@ template <> void DataStreamWriter::write(ClipLauncher::LaneModel& lane) { m_stream >> lane.m_name >> lane.m_exclusivityMode >> lane.m_temporalMode - >> lane.m_crossfadeDuration >> lane.m_volume; + >> lane.m_crossfadeDuration >> lane.m_volume >> lane.m_blendMode + >> lane.m_videoOpacity; checkDelimiter(); } @@ -87,6 +107,8 @@ void JSONReader::read(const ClipLauncher::LaneModel& lane) obj["TemporalMode"] = static_cast(lane.m_temporalMode); obj["CrossfadeDuration"] = lane.m_crossfadeDuration; obj["Volume"] = lane.m_volume; + obj["BlendMode"] = static_cast(lane.m_blendMode); + obj["VideoOpacity"] = lane.m_videoOpacity; } template <> @@ -99,4 +121,8 @@ void JSONWriter::write(ClipLauncher::LaneModel& lane) = static_cast(obj["TemporalMode"].toInt()); lane.m_crossfadeDuration = obj["CrossfadeDuration"].toDouble(); lane.m_volume = obj["Volume"].toDouble(); + if(auto v = obj.tryGet("BlendMode")) + lane.m_blendMode = static_cast(v->toInt()); + if(auto v = obj.tryGet("VideoOpacity")) + lane.m_videoOpacity = v->toDouble(); } diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp index 8b9f95aec8..60aaa85ed8 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/LaneModel.hpp @@ -44,6 +44,12 @@ class LaneModel final : public score::Entity double volume() const noexcept { return m_volume; } void setVolume(double v); + VideoBlendMode blendMode() const noexcept { return m_blendMode; } + void setBlendMode(VideoBlendMode m); + + double videoOpacity() const noexcept { return m_videoOpacity; } + void setVideoOpacity(double v); + void nameChanged(const QString& n) E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, nameChanged, n) void exclusivityModeChanged(ClipLauncher::ExclusivityMode m) E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, exclusivityModeChanged, m) @@ -53,13 +59,19 @@ class LaneModel final : public score::Entity E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, crossfadeDurationChanged, d) void volumeChanged(double v) E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, volumeChanged, v) + void blendModeChanged(ClipLauncher::VideoBlendMode m) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, blendModeChanged, m) + void videoOpacityChanged(double v) + E_SIGNAL(SCORE_PLUGIN_CLIPLAUNCHER_EXPORT, videoOpacityChanged, v) private: QString m_name; ExclusivityMode m_exclusivityMode{ExclusivityMode::Exclusive}; TemporalMode m_temporalMode{TemporalMode::BPMSynced}; + VideoBlendMode m_blendMode{VideoBlendMode::Normal}; double m_crossfadeDuration{0.5}; double m_volume{1.0}; + double m_videoOpacity{1.0}; }; } // namespace ClipLauncher diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp index 11e8efa2bf..f0465b930a 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.cpp @@ -28,6 +28,10 @@ ProcessModel::ProcessModel( inlet = std::make_unique("Audio In", Id(0), this); outlet = std::make_unique("Audio Out", Id(0), this); outlet->setPropagate(true); + textureOutlet + = std::make_unique("Video Out", Id(1), this); + midiOutlet + = std::make_unique("MIDI Out", Id(2), this); // Create initial grid: 2 lanes x 4 scenes with cells at every position for(int l = 0; l < 2; l++) @@ -55,6 +59,10 @@ void ProcessModel::init() { m_inlets.push_back(inlet.get()); m_outlets.push_back(outlet.get()); + if(textureOutlet) + m_outlets.push_back(textureOutlet.get()); + if(midiOutlet) + m_outlets.push_back(midiOutlet.get()); } CellModel* ProcessModel::cellAt(int lane, int scene) const @@ -167,6 +175,8 @@ void DataStreamReader::read(const ClipLauncher::ProcessModel& proc) { readFrom(*proc.inlet); readFrom(*proc.outlet); + readFrom(*proc.textureOutlet); + readFrom(*proc.midiOutlet); m_stream << proc.m_globalQuantization; @@ -193,6 +203,8 @@ void DataStreamWriter::write(ClipLauncher::ProcessModel& proc) { proc.inlet = Process::load_audio_inlet(*this, &proc); proc.outlet = Process::load_audio_outlet(*this, &proc); + proc.textureOutlet = std::make_unique(*this, &proc); + proc.midiOutlet = Process::load_midi_outlet(*this, &proc); m_stream >> proc.m_globalQuantization; @@ -235,6 +247,8 @@ void JSONReader::read(const ClipLauncher::ProcessModel& proc) { obj["Inlet"] = *proc.inlet; obj["Outlet"] = *proc.outlet; + obj["TextureOutlet"] = *proc.textureOutlet; + obj["MidiOutlet"] = *proc.midiOutlet; obj["GlobalQuantization"] = proc.m_globalQuantization; @@ -270,6 +284,28 @@ void JSONWriter::write(ClipLauncher::ProcessModel& proc) "Audio Out", Id(0), &proc); } + if(auto texOutl = obj.tryGet("TextureOutlet")) + { + JSONWriter writer{*texOutl}; + proc.textureOutlet = std::make_unique(writer, &proc); + } + else + { + proc.textureOutlet + = std::make_unique("Video Out", Id(1), &proc); + } + + if(auto midiOutl = obj.tryGet("MidiOutlet")) + { + JSONWriter writer{*midiOutl}; + proc.midiOutlet = Process::load_midi_outlet(writer, &proc); + } + else + { + proc.midiOutlet + = std::make_unique("MIDI Out", Id(2), &proc); + } + proc.m_globalQuantization = obj["GlobalQuantization"].toDouble(); const auto& lanes_arr = obj["Lanes"].toArray(); diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp index ff1acec729..2eaa5c6093 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/ProcessModel.hpp @@ -8,6 +8,8 @@ #include #include +#include + #include #include @@ -24,6 +26,8 @@ class ProcessModel final : public Process::ProcessModel public: std::unique_ptr inlet; std::unique_ptr outlet; + std::unique_ptr textureOutlet; + std::unique_ptr midiOutlet; score::EntityMap lanes; score::EntityMap scenes; diff --git a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp index 5a9921e938..dd6a80d27b 100644 --- a/src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp +++ b/src/plugins/score-plugin-cliplauncher/ClipLauncher/Types.hpp @@ -52,6 +52,36 @@ enum class CellState : uint8_t Stopping }; +// Values match the Video Mixer ISF shader's mode integers +enum class VideoBlendMode : uint8_t +{ + Add = 1, + Average = 2, + ColorBurn = 3, + ColorDodge = 4, + Darken = 5, + Difference = 6, + Exclusion = 7, + Glow = 8, + HardLight = 9, + HardMix = 10, + Lighten = 11, + LinearBurn = 12, + LinearDodge = 13, + LinearLight = 14, + Multiply = 15, + Negation = 16, + Normal = 17, + Overlay = 18, + Phoenix = 19, + PinLight = 20, + Reflect = 21, + Screen = 22, + SoftLight = 23, + Subtract = 24, + VividLight = 25 +}; + } // namespace ClipLauncher // Serialization @@ -103,8 +133,21 @@ inline QDataStream& operator>>(QDataStream& s, ClipLauncher::TemporalMode& v) return s; } +inline QDataStream& operator<<(QDataStream& s, ClipLauncher::VideoBlendMode v) +{ + return s << static_cast(v); +} +inline QDataStream& operator>>(QDataStream& s, ClipLauncher::VideoBlendMode& v) +{ + uint8_t x; + s >> x; + v = static_cast(x); + return s; +} + W_REGISTER_ARGTYPE(ClipLauncher::ExclusivityMode) W_REGISTER_ARGTYPE(ClipLauncher::LaunchMode) W_REGISTER_ARGTYPE(ClipLauncher::TriggerStyle) W_REGISTER_ARGTYPE(ClipLauncher::TemporalMode) W_REGISTER_ARGTYPE(ClipLauncher::CellState) +W_REGISTER_ARGTYPE(ClipLauncher::VideoBlendMode) diff --git a/src/plugins/score-plugin-dataflow/Dataflow/AudioOutletItem.cpp b/src/plugins/score-plugin-dataflow/Dataflow/AudioOutletItem.cpp index 2e75c49136..6002fd6af1 100644 --- a/src/plugins/score-plugin-dataflow/Dataflow/AudioOutletItem.cpp +++ b/src/plugins/score-plugin-dataflow/Dataflow/AudioOutletItem.cpp @@ -76,7 +76,7 @@ void AudioOutletFactory::setupOutletInspector( d.submit(out, ok); } }); - QObject::connect(&outlet, &Process::AudioOutlet::propagateChanged, cb, [=](bool p) { + QObject::connect(&outlet, &Process::Outlet::propagateChanged, cb, [=](bool p) { if(p != cb->isChecked()) { cb->setChecked(p); diff --git a/src/plugins/score-plugin-engine/Execution/BaseScenarioComponent.cpp b/src/plugins/score-plugin-engine/Execution/BaseScenarioComponent.cpp index 4620719b2c..8b08edbc85 100644 --- a/src/plugins/score-plugin-engine/Execution/BaseScenarioComponent.cpp +++ b/src/plugins/score-plugin-engine/Execution/BaseScenarioComponent.cpp @@ -17,6 +17,13 @@ #include #include + +#if __has_include() +#include +#include +#include +#define SCORE_HAS_GFX 1 +#endif #include #include #include @@ -148,6 +155,30 @@ void BaseScenarioElement::init(bool forcePlay, BaseScenarioRefContainer element) m_ossia_interval->onSetup( m_ossia_interval, main_interval, m_ossia_interval->makeDurations(), true); +#if SCORE_HAS_GFX + // Set up root texture propagation: route the root interval's gfx_forward_node + // output to the first GFX window device, mirroring audio's out/main routing. + if(auto gfx_fw = m_ossia_interval->gfxForwardNode()) + { + auto& outs = gfx_fw->root_outputs(); + if(!outs.empty()) + { + for(auto dev : m_ctx.execState->edit_devices()) + { + if(dynamic_cast(&dev->get_protocol())) + { + auto& root = dev->get_root_node(); + if(auto param = root.get_parameter()) + { + outs[0]->address = param; + break; + } + } + } + } + } +#endif + m_ossia_scenario->start(); } diff --git a/src/plugins/score-plugin-gfx/CMakeLists.txt b/src/plugins/score-plugin-gfx/CMakeLists.txt index d67da7bcfa..b0e6730cfa 100644 --- a/src/plugins/score-plugin-gfx/CMakeLists.txt +++ b/src/plugins/score-plugin-gfx/CMakeLists.txt @@ -150,6 +150,7 @@ set(HDRS Gfx/Graph/TextNode.hpp Gfx/Graph/Uniforms.hpp Gfx/Graph/Utils.hpp + Gfx/Graph/TextureForwardNode.hpp Gfx/Graph/VideoNode.hpp Gfx/Graph/VideoNodeRenderer.hpp Gfx/Graph/DirectVideoNodeRenderer.hpp @@ -201,6 +202,7 @@ set(HDRS Gfx/GfxContext.hpp Gfx/GfxExecNode.hpp Gfx/GfxExecContext.hpp + Gfx/GfxForwardNode.hpp Gfx/GfxParameter.hpp Gfx/GfxDevice.hpp Gfx/GfxInputDevice.hpp @@ -289,6 +291,7 @@ set(SRCS Gfx/Graph/SimpleRenderedISFNode.cpp Gfx/Graph/TextNode.cpp Gfx/Graph/Utils.cpp + Gfx/Graph/TextureForwardNode.cpp Gfx/Graph/VideoNode.cpp Gfx/Graph/VideoNodeRenderer.cpp Gfx/Graph/DirectVideoNodeRenderer.cpp @@ -296,6 +299,7 @@ set(SRCS Gfx/GfxApplicationPlugin.cpp Gfx/GfxExecNode.cpp + Gfx/GfxForwardNode.cpp Gfx/GfxExecutionAction.cpp Gfx/GfxContext.cpp Gfx/GfxDevice.cpp diff --git a/src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.cpp b/src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.cpp new file mode 100644 index 0000000000..d7b83c616f --- /dev/null +++ b/src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.cpp @@ -0,0 +1,28 @@ +#include +#include + +namespace Gfx +{ + +gfx_forward_node::gfx_forward_node(GfxExecutionAction& ctx) + : gfx_exec_node{ctx} +{ + add_texture(); // texture input + add_texture_out(); // texture output + + auto n = std::make_unique(); + id = exec_context->ui->register_node(std::move(n)); +} + +gfx_forward_node::~gfx_forward_node() +{ + if(id != score::gfx::invalid_node_index) + exec_context->ui->unregister_node(id); +} + +std::string gfx_forward_node::label() const noexcept +{ + return "Gfx::forward"; +} + +} diff --git a/src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.hpp b/src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.hpp new file mode 100644 index 0000000000..274632051a --- /dev/null +++ b/src/plugins/score-plugin-gfx/Gfx/GfxForwardNode.hpp @@ -0,0 +1,25 @@ +#pragma once +#include + +namespace Gfx +{ + +/** + * @brief Execution node for texture forwarding/propagation. + * + * Created per-interval and per-scenario to forward texture outputs + * from child processes up the hierarchy, mirroring how audio propagation + * uses ossia::nodes::forward_node. + * + * Has 1 texture inlet and 1 texture outlet. The base class gfx_exec_node::run() + * handles link_cable_to_inlet() and push_texture() automatically. + */ +class SCORE_PLUGIN_GFX_EXPORT gfx_forward_node final : public gfx_exec_node +{ +public: + gfx_forward_node(GfxExecutionAction& ctx); + ~gfx_forward_node(); + std::string label() const noexcept override; +}; + +} diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.cpp new file mode 100644 index 0000000000..d5e7687a12 --- /dev/null +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include + +namespace score::gfx +{ + +static const constexpr auto texture_forward_vertex = R"_(#version 450 +layout(location = 0) in vec2 position; +layout(location = 1) in vec2 texcoord; + +layout(location = 0) out vec2 v_texcoord; + +layout(std140, binding = 0) uniform renderer_t { + mat4 clipSpaceCorrMatrix; + vec2 renderSize; +} renderer; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() +{ + v_texcoord = texcoord; + gl_Position = renderer.clipSpaceCorrMatrix * vec4(position.xy, 0.0, 1.); +} +)_"; + +static const constexpr auto texture_forward_fragment = R"_(#version 450 +layout(location = 0) in vec2 v_texcoord; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform renderer_t { + mat4 clipSpaceCorrMatrix; + vec2 renderSize; +} renderer; + +layout(binding=3) uniform sampler2D y_tex; + +void main() +{ + fragColor = texture(y_tex, v_texcoord); +} +)_"; + +class TextureForwardRenderer : public GenericNodeRenderer +{ +public: + using GenericNodeRenderer::GenericNodeRenderer; + ~TextureForwardRenderer() { } + + void init(RenderList& renderer, QRhiResourceUpdateBatch& res) override + { + const auto& mesh = renderer.defaultTriangle(); + defaultMeshInit(renderer, mesh, res); + processUBOInit(renderer); + m_material.init(renderer, node.input, m_samplers); + std::tie(m_vertexS, m_fragmentS) + = score::gfx::makeShaders(renderer.state, texture_forward_vertex, texture_forward_fragment); + defaultPassesInit(renderer, mesh); + } + + void update(RenderList& renderer, QRhiResourceUpdateBatch& res, Edge* edge) override + { + defaultUBOUpdate(renderer, res); + } + + void release(RenderList& r) override + { + defaultRelease(r); + } +}; + +TextureForwardNode::TextureForwardNode() +{ + // 1 texture input, 1 texture output + input.push_back(new Port{this, {}, Types::Image, {}, {}}); + output.push_back(new Port{this, {}, Types::Image, {}, {}}); +} + +TextureForwardNode::~TextureForwardNode() { } + +NodeRenderer* TextureForwardNode::createRenderer(RenderList& r) const noexcept +{ + return new TextureForwardRenderer{*this}; +} + +void TextureForwardNode::process(Message&& msg) +{ + NodeModel::process(std::move(msg)); +} + +} diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.hpp b/src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.hpp new file mode 100644 index 0000000000..8f0e34e5d2 --- /dev/null +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/TextureForwardNode.hpp @@ -0,0 +1,23 @@ +#pragma once +#include + +namespace score::gfx +{ + +/** + * @brief A node that forwards an input texture to its output unchanged. + * + * Used for texture propagation through the interval/scenario hierarchy, + * mirroring how audio uses forward_node. + */ +class SCORE_PLUGIN_GFX_EXPORT TextureForwardNode : public NodeModel +{ +public: + TextureForwardNode(); + ~TextureForwardNode(); + + score::gfx::NodeRenderer* createRenderer(RenderList& r) const noexcept override; + void process(Message&& msg) override; +}; + +} diff --git a/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp b/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp index ead5dcb7fc..72a5357218 100644 --- a/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp @@ -546,21 +546,25 @@ void JSONWriter::write(Gfx::TextureInlet& p) template <> void DataStreamReader::read(const Gfx::TextureOutlet& p) { - // read((Process::Outlet&)p); + m_stream << p.m_propagate; } template <> void DataStreamWriter::write(Gfx::TextureOutlet& p) { + m_stream >> p.m_propagate; } template <> void JSONReader::read(const Gfx::TextureOutlet& p) { - // read((Process::Outlet&)p); + if(p.m_propagate) + obj["Propagate"] = p.m_propagate; } template <> void JSONWriter::write(Gfx::TextureOutlet& p) { + if(auto it = obj.tryGet("Propagate")) + p.m_propagate = it->toBool(); } W_OBJECT_IMPL(Gfx::GeometryInlet) diff --git a/src/plugins/score-plugin-nodal/Nodal/Executor.cpp b/src/plugins/score-plugin-nodal/Nodal/Executor.cpp index 2874246a32..ae84e25595 100644 --- a/src/plugins/score-plugin-nodal/Nodal/Executor.cpp +++ b/src/plugins/score-plugin-nodal/Nodal/Executor.cpp @@ -137,6 +137,7 @@ struct AddNode std::weak_ptr fw_node; std::weak_ptr process_node; std::weak_ptr g_weak; + std::weak_ptr gfx_fw_node; ossia::pod_vector propagated_outlets; void operator()() const noexcept @@ -153,7 +154,7 @@ struct AddNode if(!g) return; - Execution::connectPropagated(oproc, fw, *g, propagated_outlets); + Execution::connectPropagated(oproc, fw, gfx_fw_node.lock(), *g, propagated_outlets); } }; @@ -170,13 +171,13 @@ void NodalExecutorBase::reg(const RegisteredNode& fx, Execution::Transaction& ve system().setup.register_node(proc.inlets(), proc.outlets(), fx.comp->node, vec); auto reconnectOutlets = Execution::ReconnectOutlets{ - *this, this->node, proc, fx.comp->OSSIAProcessPtr(), system().execGraph}; + *this, this->node, {}, proc, fx.comp->OSSIAProcessPtr(), system().execGraph}; connect(&proc, &Process::ProcessModel::outletsChanged, this, reconnectOutlets); reconnectOutlets(); vec.push_back(AddNode{ - this->node, fx.comp->node, system().execGraph, + this->node, fx.comp->node, system().execGraph, {}, Execution::propagatedOutlets(proc.outlets())}); } diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.cpp b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.cpp index 25fb283af9..26f2c9fd09 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.cpp @@ -27,6 +27,12 @@ #include #include #include + +#if __has_include() +#include +#include +#define SCORE_HAS_GFX 1 +#endif #include #include #include @@ -164,7 +170,7 @@ IntervalComponentBase::IntervalComponentBase( if(scenar) { - con(*interval().outlet, &Process::AudioOutlet::propagateChanged, this, + con(*interval().outlet, &Process::Outlet::propagateChanged, this, [&, scenar](bool propag) { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); if(m_ossia_interval) @@ -200,6 +206,20 @@ IntervalComponentBase::IntervalComponentBase( } }); } + +#if SCORE_HAS_GFX + // Create a gfx_forward_node for texture propagation + if(auto* gfxPlug = ctx.doc.findPlugin()) + { + m_gfxForwardNode = std::make_shared(gfxPlug->exec); + m_gfxForwardNode->prepare(*ctx.execState); + std::weak_ptr g_weak = ctx.execGraph; + in_exec([g_weak, node = m_gfxForwardNode] { + if(auto graph = g_weak.lock()) + graph->add_node(node); + }); + } +#endif // TODO tempo, etc } @@ -332,6 +352,16 @@ void IntervalComponent::cleanup(const std::shared_ptr& self) c->cleanup(); } + if(m_gfxForwardNode) + { + std::weak_ptr g_weak = system().execGraph; + in_exec([g_weak, node = m_gfxForwardNode] { + if(auto graph = g_weak.lock()) + graph->remove_node(node); + }); + m_gfxForwardNode.reset(); + } + executionStopped(); clear(); m_processes.clear(); @@ -606,9 +636,9 @@ IntervalComponentBase::make(ProcessComponentFactory& fac, Process::ProcessModel& ctx->executionQueue.enqueue([p, t = ctx->time(t)] { p->set_start_offset(t); }); }); - // Audio propagation + // Audio + texture propagation auto reconnectOutlets = ReconnectOutlets{ - *this, this->OSSIAInterval()->node, proc, oproc, system().execGraph}; + *this, this->OSSIAInterval()->node, m_gfxForwardNode, proc, oproc, system().execGraph}; con(proc, &Process::ProcessModel::outletsChanged, this, reconnectOutlets); reconnectOutlets(); @@ -619,11 +649,33 @@ IntervalComponentBase::make(ProcessComponentFactory& fac, Process::ProcessModel& in_exec(AddProcess{ m_ossia_interval, m_ossia_interval.get(), oproc, system().execGraph, - propagatedOutlets(proc.outlets())}); + m_gfxForwardNode, propagatedOutlets(proc.outlets())}); + + // Connect process's gfx_forward_node to interval's gfx_forward_node + // (for processes like Scenario or ClipLauncher that aggregate textures) + if(auto proc_gfx = plug->gfxForwardNode()) + { + if(m_gfxForwardNode + && !proc_gfx->root_outputs().empty() + && !m_gfxForwardNode->root_inputs().empty()) + { + std::weak_ptr g_weak = system().execGraph; + in_exec([g_weak, proc_gfx, itv_gfx = m_gfxForwardNode] { + if(auto g = g_weak.lock()) + { + auto cable = g->allocate_edge( + ossia::immediate_glutton_connection{}, + proc_gfx->root_outputs()[0], itv_gfx->root_inputs()[0], + proc_gfx, itv_gfx); + g->connect(cable); + } + }); + } + } connect( plug.get(), &ProcessComponent::nodeChanged, this, - HandleNodeChange{m_ossia_interval->node, oproc, system().execGraph, proc}); + HandleNodeChange{m_ossia_interval->node, m_gfxForwardNode, oproc, system().execGraph, proc}); return plug.get(); } } diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.hpp b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.hpp index 360599a127..b7c50e8254 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecution.hpp @@ -120,12 +120,18 @@ class SCORE_PLUGIN_SCENARIO_EXPORT IntervalComponentBase const Context& context() const { return system(); } + const std::shared_ptr& gfxForwardNode() const + { + return m_gfxForwardNode; + } + protected: void on_processAdded(Process::ProcessModel& score_proc); void recomputePropagate(const Process::ProcessModel& process, const Process::Port& port); std::shared_ptr m_ossia_interval; + std::shared_ptr m_gfxForwardNode; score::hash_map, std::shared_ptr> m_processes; }; diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecutionHelpers.hpp b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecutionHelpers.hpp index be79d2d18e..8598fd93f9 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecutionHelpers.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/IntervalExecutionHelpers.hpp @@ -75,15 +75,15 @@ inline auto propagatedOutlets(const Process::Outlets& outlets) noexcept ossia::pod_vector propagated_outlets; for(std::size_t i = 0; i < outlets.size(); i++) { - if(auto o = qobject_cast(outlets[i])) - if(o->propagate()) - propagated_outlets.push_back(i); + if(outlets[i]->propagate()) + propagated_outlets.push_back(i); } return propagated_outlets; } inline void connectPropagated( const ossia::node_ptr& process_node, const ossia::node_ptr& interval_node, + const ossia::node_ptr& gfx_fw_node, ossia::graph_interface& g, const ossia::pod_vector& propagated_outlets) noexcept { @@ -94,18 +94,36 @@ inline void connectPropagated( if(propagated >= outs.size()) continue; - if(outs[propagated]->which() == ossia::audio_port::which) + switch(outs[propagated]->which()) { - auto cable = g.allocate_edge( - ossia::immediate_glutton_connection{}, outs[propagated], - interval_node->root_inputs()[0], process_node, interval_node); - g.connect(cable); + case ossia::audio_port::which: + { + auto cable = g.allocate_edge( + ossia::immediate_glutton_connection{}, outs[propagated], + interval_node->root_inputs()[0], process_node, interval_node); + g.connect(cable); + break; + } + case ossia::texture_port::which: + { + if(gfx_fw_node && !gfx_fw_node->root_inputs().empty()) + { + auto cable = g.allocate_edge( + ossia::immediate_glutton_connection{}, outs[propagated], + gfx_fw_node->root_inputs()[0], process_node, gfx_fw_node); + g.connect(cable); + } + break; + } + default: + break; } } } inline void updatePropagated( const ossia::node_ptr& process_node, const ossia::node_ptr& interval_node, + const ossia::node_ptr& gfx_fw_node, ossia::graph_interface& g, std::size_t port_idx, bool is_propagated) noexcept { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio); @@ -116,7 +134,16 @@ inline void updatePropagated( const ossia::outlet& outlet = *outs[port_idx]; - if(!outlet.target()) + // Determine the target node based on port type + ossia::node_ptr target_node; + if(outlet.which() == ossia::audio_port::which) + target_node = interval_node; + else if(outlet.which() == ossia::texture_port::which) + target_node = gfx_fw_node; + else + return; + + if(!target_node || target_node->root_inputs().empty()) return; // Remove cables if depropagated, add cables if repropagated @@ -124,20 +151,20 @@ inline void updatePropagated( { for(const ossia::graph_edge* edge : outlet.targets) { - if(edge->in_node == interval_node) + if(edge->in_node == target_node) return; } auto cable = g.allocate_edge( ossia::immediate_glutton_connection{}, outs[port_idx], - interval_node->root_inputs()[0], process_node, interval_node); + target_node->root_inputs()[0], process_node, target_node); g.connect(cable); } else { for(ossia::graph_edge* edge : outlet.targets) { - if(edge->in_node == interval_node) + if(edge->in_node == target_node) { g.disconnect(edge); return; @@ -153,6 +180,7 @@ struct AddProcess ossia::time_interval* cst_ptr{}; std::weak_ptr oproc_weak; std::weak_ptr g_weak; + std::weak_ptr gfx_forward_weak; ossia::pod_vector propagated_outlets; void operator()() const noexcept @@ -170,7 +198,7 @@ struct AddProcess if(!oproc->node) return; - connectPropagated(oproc->node, cst_ptr->node, *g, propagated_outlets); + connectPropagated(oproc->node, cst_ptr->node, gfx_forward_weak.lock(), *g, propagated_outlets); } }; @@ -179,6 +207,7 @@ struct RecomputePropagate const Execution::Context& system; Process::ProcessModel& proc; std::weak_ptr cst_node_weak; + std::weak_ptr gfx_forward_weak; std::weak_ptr oproc_weak; std::weak_ptr g_weak; Process::Outlet* outlet{}; @@ -192,8 +221,8 @@ struct RecomputePropagate = std::distance(proc.outlets().begin(), ossia::find(proc.outlets(), outlet)); system.executionQueue.enqueue( - [cst_node_weak = this->cst_node_weak, g_weak = this->g_weak, - oproc_weak = this->oproc_weak, port_index, propagate] { + [cst_node_weak = this->cst_node_weak, gfx_fw_weak = this->gfx_forward_weak, + g_weak = this->g_weak, oproc_weak = this->oproc_weak, port_index, propagate] { const auto g = g_weak.lock(); if(!g) return; @@ -210,7 +239,7 @@ struct RecomputePropagate if(!oproc->node) return; - updatePropagated(proc_node, cst_node, *g, port_index, propagate); + updatePropagated(proc_node, cst_node, gfx_fw_weak.lock(), *g, port_index, propagate); }); } }; @@ -220,6 +249,7 @@ struct ReconnectOutlets { T& component; std::weak_ptr fw_node; + std::weak_ptr gfx_fw_node; Process::ProcessModel& proc; std::weak_ptr oproc_weak; @@ -231,14 +261,15 @@ struct ReconnectOutlets OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); for(Process::Outlet* outlet : proc.outlets()) { - if(auto o = qobject_cast(outlet)) + if(outlet->propagate() || qobject_cast(outlet) + || outlet->type() == Process::PortType::Texture) { QObject::disconnect( - o, &Process::AudioOutlet::propagateChanged, &component, nullptr); + outlet, &Process::Outlet::propagateChanged, &component, nullptr); QObject::connect( - o, &Process::AudioOutlet::propagateChanged, &component, + outlet, &Process::Outlet::propagateChanged, &component, RecomputePropagate{ - component.system(), proc, fw_node, oproc_weak, g_weak, outlet}); + component.system(), proc, fw_node, gfx_fw_node, oproc_weak, g_weak, outlet}); } } } @@ -247,6 +278,7 @@ struct ReconnectOutlets struct HandleNodeChange { std::weak_ptr cst_node_weak; + std::weak_ptr gfx_forward_weak; std::weak_ptr oproc_weak; std::weak_ptr g_weak; Process::ProcessModel& proc; @@ -257,7 +289,9 @@ struct HandleNodeChange { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); - commands->push_back([cst_node_weak = this->cst_node_weak, g_weak = this->g_weak, + commands->push_back([cst_node_weak = this->cst_node_weak, + gfx_fw_weak = this->gfx_forward_weak, + g_weak = this->g_weak, propagated = propagatedOutlets(proc.outlets()), old_node, new_node] { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio); @@ -267,6 +301,7 @@ struct HandleNodeChange auto g = g_weak.lock(); if(!g) return; + auto gfx_fw = gfx_fw_weak.lock(); // Remove propagate edges from the old node if(old_node) @@ -277,7 +312,8 @@ struct HandleNodeChange auto targets = outlet->targets; for(auto e : targets) { - if(e->in_node.get() == cst_node.get()) + if(e->in_node.get() == cst_node.get() + || (gfx_fw && e->in_node.get() == gfx_fw.get())) { g->disconnect(e); } @@ -288,7 +324,7 @@ struct HandleNodeChange // Add edges to the new node if(new_node) { - connectPropagated(new_node, cst_node, *g, propagated); + connectPropagated(new_node, cst_node, gfx_fw, *g, propagated); } }); } diff --git a/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.cpp b/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.cpp index 7e926b2552..5c014ce7e7 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.cpp @@ -35,6 +35,12 @@ #include #include #include + +#if __has_include() +#include +#include +#define SCORE_HAS_GFX 1 +#endif #include #include #include @@ -80,6 +86,20 @@ ScenarioComponentBase::ScenarioComponentBase( connect( this, &ScenarioComponentBase::sig_eventCallback, this, &ScenarioComponentBase::eventCallback, Qt::QueuedConnection); + +#if SCORE_HAS_GFX + // Create a gfx_forward_node for texture propagation at scenario level + if(auto* gfxPlug = ctx.doc.findPlugin()) + { + m_gfxForwardNode = std::make_shared(gfxPlug->exec); + m_gfxForwardNode->prepare(*ctx.execState); + std::weak_ptr g_weak = ctx.execGraph; + in_exec([g_weak, node = m_gfxForwardNode] { + if(auto graph = g_weak.lock()) + graph->add_node(node); + }); + } +#endif } ScenarioComponentBase::~ScenarioComponentBase() @@ -304,6 +324,15 @@ void ScenarioComponent::lazy_init() void ScenarioComponent::cleanup() { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); + if(m_gfxForwardNode) + { + std::weak_ptr g_weak = system().execGraph; + in_exec([g_weak, node = m_gfxForwardNode] { + if(auto graph = g_weak.lock()) + graph->remove_node(node); + }); + m_gfxForwardNode.reset(); + } clear(); ProcessComponent::cleanup(); } @@ -499,7 +528,10 @@ ScenarioComponentBase::make( elt->onSetup(elt, ossia_cst, dur); const bool prop = cst.graphal() ? false : cst.outlet->propagate(); - in_exec([g = system().execGraph, proc, ossia_sev, ossia_eev, ossia_cst, prop] { + auto itv_gfx_fw = elt->gfxForwardNode(); + auto scn_gfx_fw = m_gfxForwardNode; + in_exec([g = system().execGraph, proc, ossia_sev, ossia_eev, ossia_cst, prop, + itv_gfx_fw, scn_gfx_fw] { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio); if(auto sev = ossia_sev->OSSIAEvent()) sev->next_time_intervals().push_back(ossia_cst); @@ -515,6 +547,18 @@ ScenarioComponentBase::make( proc->node->root_inputs()[0], ossia_cst->node, proc->node); g->connect(cable); } + + // Connect interval's gfx_forward_node to scenario's gfx_forward_node + if(itv_gfx_fw && scn_gfx_fw + && !itv_gfx_fw->root_outputs().empty() + && !scn_gfx_fw->root_inputs().empty()) + { + auto cable = g->allocate_edge( + ossia::immediate_glutton_connection{}, + itv_gfx_fw->root_outputs()[0], scn_gfx_fw->root_inputs()[0], + itv_gfx_fw, scn_gfx_fw); + g->connect(cable); + } }); return elt.get(); } diff --git a/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.hpp b/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.hpp index 698498f139..838cd3af1d 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Process/ScenarioExecution.hpp @@ -91,6 +91,11 @@ class SCORE_PLUGIN_SCENARIO_EXPORT ScenarioComponentBase void playInterval(const Scenario::IntervalModel& itv); void stopInterval(const Scenario::IntervalModel& itv); + std::shared_ptr gfxForwardNode() const override + { + return m_gfxForwardNode; + } + void stop() override; template @@ -139,6 +144,7 @@ class SCORE_PLUGIN_SCENARIO_EXPORT ScenarioComponentBase m_executingIntervals; const Context& m_ctx; + std::shared_ptr m_gfxForwardNode; Scenario::CSPCoherencyCheckerInterface* m_checker{}; QVector> m_pastTn{};