diff --git a/CMakeLists.txt b/CMakeLists.txt index d80b1bddacb..2f544ce7bf3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ option(RADIUM_GENERATE_LIB_ENGINE "Include Radium::Engine in CMake project." ON) option(RADIUM_GENERATE_LIB_GUI "Include Radium::Gui in CMake project." ON) option(RADIUM_GENERATE_LIB_PLUGINBASE "Include Radium::PluginBase in CMake project." ON) option(RADIUM_GENERATE_LIB_HEADLESS "Include Radium::Headless in CMake project." ON) +option(RADIUM_GENERATE_LIB_DATAFLOW "Include Radium::Dataflow* in CMake project." ON) option( RADIUM_UPDATE_VERSION "Update version file each time the project is compiled (update compilation time in version.cpp)." @@ -290,6 +291,7 @@ message_setting("RADIUM_GENERATE_LIB_CORE") message_setting("RADIUM_GENERATE_LIB_ENGINE") message_setting("RADIUM_GENERATE_LIB_GUI") message_setting("RADIUM_GENERATE_LIB_HEADLESS") +message_setting("RADIUM_GENERATE_LIB_DATAFLOW") message_setting("RADIUM_GENERATE_LIB_IO") message_setting("RADIUM_GENERATE_LIB_PLUGINBASE") string(REPLACE ";" " " COMPONENTS_LIST "${RADIUM_COMPONENTS}") diff --git a/cmake/RadiumSetupFunctions.cmake b/cmake/RadiumSetupFunctions.cmake index 9c4c861422e..39e807b228d 100644 --- a/cmake/RadiumSetupFunctions.cmake +++ b/cmake/RadiumSetupFunctions.cmake @@ -966,18 +966,27 @@ function(configure_radium_library) # add the cmake command to install target add_custom_install_target(${ARGS_TARGET}) + get_target_property(TargetType ${ARGS_TARGET} TYPE) + if(TargetType STREQUAL INTERFACE_LIBRARY) + set(PropertyQualifier INTERFACE) + else() + set(PropertyQualifier PUBLIC) + target_compile_definitions(${ARGS_TARGET} PRIVATE ${ARGS_TARGET}_EXPORTS) + endif() + if(CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_definitions(${ARGS_TARGET} PUBLIC _DEBUG) - if(MSVC OR MSVC_IDE) + target_compile_definitions(${ARGS_TARGET} ${PropertyQualifier} _DEBUG) + if((MSVC OR MSVC_IDE) AND NOT (TargetType STREQUAL INTERFACE_LIBRARY)) install(FILES $ DESTINATION bin) endif() endif() - target_compile_features(${ARGS_TARGET} PUBLIC cxx_std_17) + + target_compile_features(${ARGS_TARGET} ${PropertyQualifier} cxx_std_17) if(OPENMP_FOUND) - target_link_libraries(${ARGS_TARGET} PUBLIC OpenMP::OpenMP_CXX) + target_link_libraries(${ARGS_TARGET} ${PropertyQualifier} OpenMP::OpenMP_CXX) endif(OPENMP_FOUND) - target_compile_definitions(${ARGS_TARGET} PRIVATE ${ARGS_TARGET}_EXPORTS) - target_include_directories(${ARGS_TARGET} PUBLIC $) + target_include_directories(${ARGS_TARGET} ${PropertyQualifier} $) + add_library(${ARGS_NAMESPACE}::${ARGS_TARGET} ALIAS ${ARGS_TARGET}) install( diff --git a/doc/basics.md b/doc/basics.md index 524e49fc523..860bc6e2ec4 100644 --- a/doc/basics.md +++ b/doc/basics.md @@ -59,6 +59,9 @@ These dependencies are shipped with radium, fetching external git sources. * with options `-DOPTION_BUILD_TESTS=OFF -DOPTION_BUILD_DOCS=OFF -DCMAKE_INSTALL_PREFIX=` * nlohmann_json: https://github.com/nlohmann/json.git, [tags/v3.10.5], * with options `-DJSON_Install=ON -DJSON_BuildTests=OFF` +* [Dataflow] + * RadiumNodeEditor: https://github.com/MathiasPaulin/RadiumQtNodeEditor.git, [main], + * with options `-DCMAKE_INSTALL_PREFIX=` ## TL;DR; command line version diff --git a/doc/basics/dependencies.md b/doc/basics/dependencies.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/doc/basics/details.md b/doc/basics/details.md index db8deaa5dde..c7ebdde63be 100644 --- a/doc/basics/details.md +++ b/doc/basics/details.md @@ -70,6 +70,7 @@ Currently supported (note that these paths must refer to the installation direct * `OpenMesh_DIR` * `cpplocate_DIR` * `nlohmann_json_DIR` +* `RadiumNodeEditor_DIR` diff --git a/doc/concepts.md b/doc/concepts.md index 91f1f94b593..1dcfefea3b1 100644 --- a/doc/concepts.md +++ b/doc/concepts.md @@ -5,3 +5,4 @@ * \subpage eventSystem * \subpage pluginSystem * \subpage forwardRenderer +* \subpage nodeSystem diff --git a/doc/concepts/dataflow/HelloGraph.json b/doc/concepts/dataflow/HelloGraph.json new file mode 100644 index 00000000000..ec1e743560f --- /dev/null +++ b/doc/concepts/dataflow/HelloGraph.json @@ -0,0 +1,71 @@ +{ + "instance": "helloGraph", + "model": { + "graph": { + "connections": [ + { + "in_node": "Filter", + "in_port": "in", + "out_node": "Source", + "out_port": "to" + }, + { + "in_node": "Filter", + "in_port": "f", + "out_node": "Selector", + "out_port": "f" + }, + { + "in_node": "Sink", + "in_port": "from", + "out_node": "Filter", + "out_port": "out" + } + ], + "factories": [], + "nodes": [ + { + "instance": "Source", + "model": { + "name": "Source>" + }, + "position": { + "x": -195.0, + "y": -82.0 + } + }, + { + "instance": "Selector", + "model": { + "name": "Source>" + }, + "position": { + "x": -279.0, + "y": 35.0 + } + }, + { + "instance": "Filter", + "model": { + "name": "Filter>" + }, + "position": { + "x": 44.0, + "y": -45.0 + } + }, + { + "instance": "Sink", + "model": { + "name": "Sink>" + }, + "position": { + "x": 267.0, + "y": 12.0 + } + } + ] + }, + "name": "Core DataflowGraph" + } +} diff --git a/doc/concepts/nodesystem.md b/doc/concepts/nodesystem.md new file mode 100644 index 00000000000..e484a849cf0 --- /dev/null +++ b/doc/concepts/nodesystem.md @@ -0,0 +1,199 @@ +\page nodeSystem Radium node system +[TOC] + +# Radium node system + +Radium-Engine embed a node system allowing to develop computation graph using an adaptation of dataflow programming. +This documentation explain the concepts used in the node system and how to develop computation graph using the Core +node system and how to extend the Core node system to be used in specific Radium-Engine application or library. + +## Structure and usage of the Radium::Dataflow component + +When building the Radium-Engine libraries, the node system is available from the Radium::Dataflow component. +The availability of this component in the set of built/installed libraries is managed using the +`RADIUM_GENERATE_LIB_DATAFLOW` cmake option (set to `ON` by default) of the main Radium-Engine CMakeLists.txt. + +The Radium::Dataflow component is a header-only library with is linked against three sub-components : + +- **DataflowCore** library defining the Core node system and a set Core Nodes allowing to develop several computation + graph. +- **DataflowQtGui** library defining Qt based Gui elements to edit and interact with a computation graph. +- **DataflowRendering** library defining specific nodes to be used by a Graph-based renderer allowing to easily define + custom renderers. + +When defining the CMakeLists.txt configuration file for an application/library that will build upon the node system, +The RadiumDataflow component might be requested in several way : + +- `find_package(Radium REQUIRED COMPONENTS Dataflow)`. This will define the imported target `Radium::Dataflow` that +gives access to all the available sub-components at once but also through imported targets `Radium::DataflowCore`, +`Radium::DataflowQtGui` and `Radium::DataflowRendering`. +- `find_package(Radium REQUIRED COMPONENTS DataflowCore)`. This will define the imported target `Radium::DataflowCore` +only. +- `find_package(Radium REQUIRED COMPONENTS DataflowQtGui)`. This will define the imported target `Radium::DataflowQtGui` +only, with transitive dependencies on `Radium::DataflowCore`. +- `find_package(Radium REQUIRED COMPONENTS DataflowRendering)`. This will define the imported target +`Radium::DataflowRendering` only, with transitive dependencies on `Radium::DataflowCore`. + +The targets that depends on a Dataflow components should then be configured as any target and linked against the +requested dataflow component, by, e.g, adding the line +`target_link_libraries(target_name PUBLIC Radium::Dataflow)` (or the only needed subcomponent). + +## Concepts of the node system + +The node system allow to build computation graph that takes its input from some _data source_ and store their results +in some _data sink_ after applying several _functions_ on the data. + +Computation graphs can be serialized and un-serialized in json format. The serialization process is nevertheless limited +to serializable data stored on the node and it is of the responsibility of the application to manage non serializable +data such as, e.g. anonymous functions (lambdas, functors, ...) dynamically defined by the application. + +The Radium node system relies on the following concepts + +- _Node_ (see Ra::Dataflow::Core::Node for reference manual) : a node represent a function that will be executed on +several strongly typed input data, defining the definition domain of the function, to produce some strongly typed +output data, defining the definition co-domain of the function. + + The input and output data are accessed through _ports_ allowing to connect nodes together to form a graph. + + The node profile is implicitly defined by its domain and co-domain. + + A node can be specialized to be a _data source_ node (empty domain) or a _data sink_ node (empty co-domain). + These specific nodes define the input and output of a complex _computation graph_ + +- _Port_ (see Ra::Dataflow::Core::PortBase for reference manual) : a port represent an element of the node +profile and allow to build the computation graph by linking ports together, implicitly defining _links_. + + A port gives access to a strongly typed data and, while implementing the general Ra::Dataflow::Core::PortBase +interface should be specialized to be either an input port (element of the definition domain of a node) through the +instancing of the template Ra::Dataflow::Core::PortIn or to an output port (element of the definition +co-domain of a node) through the instancing of the template Ra::Dataflow::Core::PortOut. + + When a node executes its function, it takes its parameter from its input ports and set the result on the output port. + + An output port can be connected to an input port of the same _DataType_ to build the computation graph. +- _Graph_ (see Ra::Dataflow::Core::DataflowGraph for reference manual) : a graph is a set of node connected through +their ports so that they define a direct acyclic graph (DAG) representing a complex function. The DAG represents +connections from some _data source_ nodes to some _data sink_ nodes through + + Once built by adding nodes and links, a graph should be _compiled_ so that the system verify its validity +(DAG, types, connections, ...). + + Once compiled, a graph can be _executed_. + The input data of the computation graph should be set on the available _data sources_ of the graph and the results +fetched from the _data sinks_. + + As a graph can be used as a node (a sub graph) in any other graph. When doing this, all _data sources_ and +_data sinks_ nodes are associated with _interface ports_ and these interface ports are added as _input_ or _output_ +ports on the graph so that links can be defined using these ports. + + _input_ and _output_ ports of a graph can also be accessed directly from the application using _data setters_ and +_data getters_ fetched from the graph. These _data setters_ and _data getters_ allows to use any graph without the +need to know explicitly their _data sources_ and _data sinks_ nor defining ports to be linked with the _input_ and +_output_ ports of the graph. + +- _Factories_ (see Ra::Dataflow::Core::NodeFactoriesManager for reference manual) : the serialization of a graph output +a set of json object describing the graph. If serialization is always possible, care must be taken for the system to +manage un-serilization of any nodes. + + When serializing a graph, the json representing a node contains the type (the name of the concrete C++ class) of the +node and several other properties of the node system. When un-serializing a graph, nodes will be automatically +instanced from their type. +The instantiation of a node is made using services offered by node factories and associated to the node type. So, in +order to be un-serializable, each node must register its type to a factory and each graph must refer to the factories used +to instantiate its node. + +## HelloGraph : your first program that uses the node system + +The example application examples/HelloGraph shows how to define a computation graph to apply filtering on a collection. +In this example, whose code is detailed below, the following graph is built and executed using different input data. + +![HelloGraph computation graph](images/HelloGraph.png) + +This graphs has two inputs, corresponding to the two **Source< ... >** nodes. These input will deliver to the +computation graph : + +- from a Ra::Dataflow::Core::Sources::SingleDataSourceNode, a vector of scalars, whose container type is +Ra::Core::VectorArray (abridged here as a RaVector) and value type is _Scalar_ (float in the default Radium-Engine +configuration), +- from a Ra::Dataflow::Core::Sources::FunctionSourceNode a predicate whose type is _std::function_ +which returns _true_ if its parameter is valid according to some decision process. + +These two _sources_ are linked to the input of a Ra::Dataflow::Core::Functionals::FilterNode, here represented by the +**Filter< ... >** node. This node select from its **in** input only the values validated by the predicate **f** and +built its output **out** with these values. + +The result of this filtering is linked to the graph output, corresponding to the Ra::Dataflow::Core::Sinks::SinkNode +**Sink< ... >**. + +Once the graph is built and compile, the HelloGraph application sent different input to the graph and print the result +of the computation. + +To develop such an application, the following should be done + +### 1. Building and inspecting the graph + +First, an object of type Ra::Dataflow::Core::DataflowGraph is instanced : +\snippet examples/DataflowExamples/HelloGraph/main.cpp Creating an empty graph + +Then, the nodes are instanced +\snippet examples/DataflowExamples/HelloGraph/main.cpp Creating Nodes + +and added to the graph +\snippet examples/DataflowExamples/HelloGraph/main.cpp Adding Nodes to the graph + +Links between ports are added to the graph, and if an error is detected, due to e.g. port type incompatiblitiy, it is +reported +\snippet examples/DataflowExamples/HelloGraph/main.cpp Creating links between Nodes + +Once the graph is built, it can be inspected using dedicated methods. +\snippet examples/DataflowExamples/HelloGraph/main.cpp Inspect the graph interface : inputs and outputs port + +For the graph constructed above, this prints on stdout + +```text +Input ports (2) are : + "Selector_f" accepting type function + "Source_to" accepting type RaVector +Output ports (1) are : + "Sink_from" generating type RaVector +``` + +### 2. Compiling the graph and getting input/output accessors + +In order to use the graph as a function acting on its input, it should be first compiled by +\snippet examples/DataflowExamples/HelloGraph/main.cpp Verifying the graph can be compiled + +If the compilation success the accessors for the input data and the output result might be fetched from the graph +\snippet examples/DataflowExamples/HelloGraph/main.cpp Configure the interface ports (input and output of the graph) + +Here, the accessor `input` allows to set the pointer on the `RaVector` to be processed while the accessor `selector` +allows to set the predicate to evaluate when filtering the collection. This predicates select values les than `0.5` + +The accessor `output` will allow, once the graph is executed, to get a reference to or to copy the resulting values. + +### 3. Executing the graph + +Once the input data are available (in this example, the `test` vector is filled with 10 random values between 0 and 1), +the graph can be executed and a reference to the resulting vector can be fetched using +\snippet examples/DataflowExamples/HelloGraph/main.cpp Execute the graph + +### 4. Multiple run of the graph on different input + +As accessors use pointers and references on the data sent to / fetched from the graph, the HelloGraph example shows how +to change the input using different available means so that every run of the graph process different values. +See the file examples/DataflowExamples/HelloGraph/main.cpp for all the details. + +## Examples of graphs and of programming custom nodes + +The unittests developed alongside the Radium::Dataflow component, and located in the directory +`tests/unittest/Dataflow/` of the Radium-Engine source tree, can be used to learn the following : + +- sourcesandsinks.cpp : demonstrate the default supported types for sources and sinks node. +- nodes.cpp : demonstrate the development of a more complex graph implementing transform/reduce +on several collections using different reduction operators. +- graphinspect.cpp : demonstrate the way a graph can be inspected to discover its structure. +- serialization.cpp : demonstrate how to save/load a graph from a json file and use it in an +application. +- customnodes.cpp : demonstrate how it is simple to develop your own node type (in C++) and +use your nodes alongside standard nodes. + \snippet unittest/Dataflow/customnodes.cpp Develop a custom node diff --git a/doc/images/HelloGraph.png b/doc/images/HelloGraph.png new file mode 100644 index 00000000000..f7dfe06218d Binary files /dev/null and b/doc/images/HelloGraph.png differ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c17881c905b..8d69d7d9011 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -15,6 +15,7 @@ foreach( APP CoreExample CustomCameraManipulator + DataflowExamples DrawPrimitives EntityAnimation EnvMap diff --git a/examples/DataflowExamples/CMakeLists.txt b/examples/DataflowExamples/CMakeLists.txt new file mode 100644 index 00000000000..3237b2d77e9 --- /dev/null +++ b/examples/DataflowExamples/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0042 NEW) + +project(DataflowExamples VERSION 1.0.0) + +add_custom_target(${PROJECT_NAME}) +add_custom_target(Install_${PROJECT_NAME}) +foreach(APP FunctionalsGraph GraphAsNode GraphEditor GraphSerialization HelloGraph) + add_subdirectory(${APP}) + add_dependencies(${PROJECT_NAME} ${APP}) + add_dependencies(Install_${PROJECT_NAME} Install_${APP}) +endforeach() diff --git a/examples/DataflowExamples/FunctionalsGraph/CMakeLists.txt b/examples/DataflowExamples/FunctionalsGraph/CMakeLists.txt new file mode 100644 index 00000000000..3cf726e8a27 --- /dev/null +++ b/examples/DataflowExamples/FunctionalsGraph/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(FunctionalsGraph VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/FunctionalsGraph/main.cpp b/examples/DataflowExamples/FunctionalsGraph/main.cpp new file mode 100644 index 00000000000..623d4a8ac8b --- /dev/null +++ b/examples/DataflowExamples/FunctionalsGraph/main.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include + +#include + +using namespace Ra::Dataflow::Core; + +/* ----------------------------------------------------------------------------------- */ +/** + * \brief Demonstrate how to use a graph to filter a collection + */ +int main( int argc, char* argv[] ) { + //! [Creating an empty graph] + DataflowGraph g { "helloGraph" }; + //! [Creating an empty graph] + + //! [Creating Nodes] + using TransformOperatorSource = Sources::FunctionSourceNode; + auto sourceNode = + std::make_shared>>( "Source" ); + auto mapSource = std::make_shared( "MapOperator" ); + auto transformNode = + std::make_shared>>( "Transformer" ); + // can't use auto here, must explicitely define the type + Functionals::TransformNode>::TransformOperator oneMinusMe = + []( const Scalar& i ) -> Scalar { return 1_ra - i; }; + transformNode->setOperator( oneMinusMe ); + + auto reduceNode = std::make_shared>>( "Minimum" ); + Functionals::ReduceNode>::ReduceOperator getMin = + []( const Scalar& a, const Scalar& b ) -> Scalar { return std::min( a, b ); }; + reduceNode->setOperator( getMin, std::numeric_limits::max() ); + + auto scalarSinkNode = std::make_shared>( "ScalarSink" ); + + auto sinkNode = std::make_shared>>( "Sink" ); + //! [Creating Nodes] + + //! [Adding Nodes to the graph] + g.addNode( sourceNode ); + g.addNode( mapSource ); + g.addNode( transformNode ); + g.addNode( reduceNode ); + g.addNode( sinkNode ); + g.addNode( scalarSinkNode ); + //! [Adding Nodes to the graph] + + //! [Creating links between Nodes] + g.addLink( sourceNode, "to", transformNode, "in" ); + g.addLink( transformNode, "out", sinkNode, "from" ); + g.addLink( transformNode, "out", reduceNode, "in" ); + g.addLink( reduceNode, "out", scalarSinkNode, "from" ); + //! [Creating links between Nodes] + + //! [Verifing the graph can be compiled] + if ( !g.compile() ) { + std::cout << " compilation failed"; + return 1; + } + //! [Verifing the graph can be compiled] + + ///! [Configure the interface ports (input and output of the graph) + auto input = g.getDataSetter( "Source_to" ); + std::vector test; + input->setData( &test ); + + auto opSetter = g.getDataSetter( "MapOperator_f" ); + // can't use auto here, must explicitely define the type + TransformOperatorSource::function_type doubleMe = []( const Scalar& i ) -> Scalar { + return 2_ra * i; + }; + opSetter->setData( &doubleMe ); + + auto output = g.getDataGetter( "Sink_from" ); + auto minimum = g.getDataGetter( "ScalarSink_from" ); + // The reference to the result will not be available before the first run + // auto& result = output->getData>(); + ///! [Configure the interface ports (input and output of the graph) + + //! [Initializing input variable to test the graph] + test.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + for ( int n = 0; n < test.capacity(); ++n ) { + test.push_back( dis( gen ) ); + } + + std::cout << "Input values : \n\t"; + for ( auto ord : test ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Initializing input variable to test the graph] + + //! [Execute the graph] + g.execute(); + // The reference to the result is now available + auto& result = output->getData>(); + auto& minValue = minimum->getData(); + //! [Execute the graph] + + //! [Print the output result] + std::cout << "Output values (oneMinusMe): " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + std::cout << "Min value is : " << minValue << "\n"; + //! [Print the output result] + + //! [Verify the result] + std::cout << "Verifying the result : \n\t"; + + bool execOK { true }; + for ( int i = 0; i < 10; ++i ) { + if ( result[i] != oneMinusMe( test[i] ) ) { + std::cout << "Error at index " << i << " : " << result[i] << " should be " + << oneMinusMe( test[i] ) << "\n\t"; + execOK = false; + } + } + if ( execOK ) { std::cout << "OK\n"; } + else { std::cout << "NOK\n"; } + //! [Verify the result] + + //! [Modifying the graph to add link to map operator] + if ( !g.addLink( mapSource, "f", transformNode, "f" ) ) { + std::cout << "Unable to link port f (" + << ") from " << mapSource->getInstanceName() << " to port f(" + << ") of " << transformNode->getInstanceName() << "\n"; + return 1; + } + //! [Modifying the graph to add link to map operator] + + //! [Execute and test the result] + std::cout << "Rerun the graph with different operator : \n"; + g.execute(); + std::cout << "Output values (doubleMe): " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + std::cout << "Min value is : " << minValue << "\n"; + execOK = true; + for ( int i = 0; i < 10; ++i ) { + if ( result[i] != doubleMe( test[i] ) ) { + std::cout << "Error at index " << i << " : " << result[i] << " should be " + << doubleMe( test[i] ) << "\n\t"; + execOK = false; + } + } + if ( execOK ) { std::cout << "OK\n"; } + else { std::cout << "NOK\n"; } + //! [Execute and test the result] + + return 0; +} diff --git a/examples/DataflowExamples/GraphAsNode/CMakeLists.txt b/examples/DataflowExamples/GraphAsNode/CMakeLists.txt new file mode 100644 index 00000000000..d034a6937cd --- /dev/null +++ b/examples/DataflowExamples/GraphAsNode/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(GraphAsNode VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/GraphAsNode/main.cpp b/examples/DataflowExamples/GraphAsNode/main.cpp new file mode 100644 index 00000000000..12894e5736c --- /dev/null +++ b/examples/DataflowExamples/GraphAsNode/main.cpp @@ -0,0 +1,61 @@ +#include "Core/Containers/MakeShared.hpp" +#include +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +/* ----------------------------------------------------------------------------------- */ +/** + * \brief Demonstrate how to use a graph to filter a collection + */ +int main( int argc, char* argv[] ) { + using RaVector = Ra::Core::VectorArray; + + auto gAsNode = Ra::Core::make_shared( "graphAsNode" ); + + auto predicateNode = gAsNode->addNode( "Selector" ); + auto filterNode = gAsNode->addNode>( "Filter" ); + + Sources::ScalarUnaryPredicateSource::function_type pred = []( Scalar x ) { return x < 0.5; }; + predicateNode->setData( &pred ); + + gAsNode->addLink( predicateNode->getFunctionPort(), filterNode->getPredicatePort() ); + + auto inputIdx = gAsNode->addInput( filterNode->getInPort() ); + auto outputIdx = gAsNode->addOutput( filterNode->getOutPort() ); + + DataflowGraph g { "mainGraph" }; + auto sourceNode = g.addNode>( "Source" ); + auto sinkNode = g.addNode>( "Sink" ); + auto graphNode = g.addNode( gAsNode ); + g.addLink( sourceNode->getOuputPort().get(), gAsNode->getInputByIndex( inputIdx ) ); + g.addLink( gAsNode->getOutputByIndex( outputIdx ), sinkNode->getInPort().get() ); + + if ( !g.compile() ) { + std::cout << " compilation failed"; + return 1; + } + + //! [Verifying the graph can be compiled] + g.saveToJson( "illustrateDoc.json" ); + + ///! [Configure the interface ports (input and output of the graph)] + RaVector test( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + std::generate( test.begin(), test.end(), [&dis, &gen]() { return dis( gen ); } ); + + sourceNode->setData( test ); + + //! [Execute the graph] + g.execute(); + auto& result = sinkNode->getDataByRef(); + std::cout << "Output values (reference): " << result.size() << "\n\t"; + std::copy( result.begin(), result.end(), std::ostream_iterator( std::cout, " " ) ); + std::cout << '\n'; + + return 0; +} diff --git a/examples/DataflowExamples/GraphEditor/CMakeLists.txt b/examples/DataflowExamples/GraphEditor/CMakeLists.txt new file mode 100644 index 00000000000..e3b4b73d69d --- /dev/null +++ b/examples/DataflowExamples/GraphEditor/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(GraphEditor VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS DataflowQtGui) + +# Qt utility functions +include(QtFunctions) + +# Find Qt packages +find_qt_package(COMPONENTS Core Widgets REQUIRED) +set(QT_DEFAULT_MAJOR_VERSION ${QT_DEFAULT_MAJOR_VERSION} PARENT_SCOPE) +set(Qt_LIBRARIES Qt::Core Qt::Widgets) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +qt_wrap_ui(gui_uis ${gui_uis}) + +# -------------------------------------------------------------------------------------------------- +set(app_sources main.cpp) + +# set(app_headers) + +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC ${Qt_LIBRARIES} Radium::DataflowQtGui) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/GraphEditor/main.cpp b/examples/DataflowExamples/GraphEditor/main.cpp new file mode 100644 index 00000000000..3a9a31c6583 --- /dev/null +++ b/examples/DataflowExamples/GraphEditor/main.cpp @@ -0,0 +1,24 @@ +#include +#include +#include + +#include + +int main( int argc, char* argv[] ) { + QApplication app( argc, argv ); + QCoreApplication::setOrganizationName( "STORM-IRIT" ); + QCoreApplication::setApplicationName( "Radium NodeGraph Editor" ); + QCoreApplication::setApplicationVersion( QT_VERSION_STR ); + QCommandLineParser parser; + parser.setApplicationDescription( QCoreApplication::applicationName() ); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument( "file", "The file to open." ); + parser.process( app ); + + Ra::Dataflow::QtGui::GraphEditor::GraphEditorWindow mainWin; + if ( !parser.positionalArguments().isEmpty() ) + mainWin.loadFile( parser.positionalArguments().first() ); + mainWin.show(); + return app.exec(); +} diff --git a/examples/DataflowExamples/GraphSerialization/CMakeLists.txt b/examples/DataflowExamples/GraphSerialization/CMakeLists.txt new file mode 100644 index 00000000000..fe1d256a40d --- /dev/null +++ b/examples/DataflowExamples/GraphSerialization/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(GraphSerialization VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/GraphSerialization/main.cpp b/examples/DataflowExamples/GraphSerialization/main.cpp new file mode 100644 index 00000000000..be105d84f2d --- /dev/null +++ b/examples/DataflowExamples/GraphSerialization/main.cpp @@ -0,0 +1,175 @@ +#include +#include +#include +#include + +#include + +#include + +using namespace Ra::Dataflow::Core; + +/* ----------------------------------------------------------------------------------- */ +/** + * \brief Demonstrate how serialize a graph with custom nodes. + */ +int main( int argc, char* argv[] ) { + + //! [Creating the factory for the custom nodes and add it to the nodes system] + using VectorType = std::vector; + // using VectorType = Ra::Core::VectorArray; + // custom node type are either specialization of templated nodes or user-define nodes class + + // create the custom node factory + auto customFactory = NodeFactoriesManager::createFactory( "ExampleCustomFactory" ); + // add node creators to the factory + customFactory->registerNodeCreator>( + Sources::SingleDataSourceNode::getTypename() + "_", "Custom" ); + customFactory->registerNodeCreator>( + Functionals::FilterNode::getTypename() + "_", "Custom" ); + customFactory->registerNodeCreator>( + Sinks::SinkNode::getTypename() + "_", "Custom" ); + + //! [Creating the factory for the custom node types and add it to the node system] + + { + //! [Creating an empty graph using the custom nodes factory] + DataflowGraph g { "Serialization example" }; + // Add to the graph the custom factory (built-in nodes are automatically managed) + g.addFactory( NodeFactoriesManager::getFactory( "ExampleCustomFactory" ) ); + //! [Creating an empty graph using the custom nodes factory] + + //! [Creating Nodes] + auto sourceNode = std::make_shared>( "Source" ); + // non serializable node using a custom filter + auto filterNode = std::make_shared>( + "Filter", []( const Scalar& x ) { return x > 0.5_ra; } ); + auto sinkNode = std::make_shared>( "Sink" ); + //! [Creating Nodes] + + //! [Adding Nodes to the graph] + g.addNode( sourceNode ); + g.addNode( filterNode ); + g.addNode( sinkNode ); + //! [Adding Nodes to the graph] + + //! [Creating links between Nodes] + g.addLink( sourceNode, "to", filterNode, "in" ); + g.addLink( filterNode, "out", sinkNode, "from" ); + //! [Creating links between Nodes] + + //! [Inspect the graph interface : inputs and outputs port] + auto inputs = g.getAllDataSetters(); + std::cout << "Input ports (" << inputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs ) { + std::cout << "\t\"" << portName << "\" accepting type " << portType << "\n"; + } + auto outputs = g.getAllDataGetters(); + std::cout << "Output ports (" << outputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs ) { + std::cout << "\t\"" << portName << "\" generating type " << portType << "\n"; + } + //! [Inspect the graph interface : inputs and outputs port] + + //! [Verifing the graph can be compiled] + if ( !g.compile() ) { + std::cout << " compilation failed"; + return 1; + } + //! [Verifing the graph can be compiled] + + //! [Serializing the graph] + g.saveToJson( "GraphSerializeExample.json" ); + //! [Serializing the graph] + } + + std::cout << "\t==**==**==*==**==*==**==*==**==*==**==*==**==*==**==\n"; + std::cout << "\t\tLoading and using the graph ...\n"; + std::cout << "\t==**==**==*==**==*==**==*==**==*==**==*==**==*==**==\n"; + + //! [Creating an empty graph and load it from a file] + DataflowGraph g1 { "" }; + g1.loadFromJson( "GraphSerializeExample.json" ); + //! [Creating an empty graph and load it from a file] + + //! [Inspect the graph interface : inputs and outputs port] + auto inputs_g1 = g1.getAllDataSetters(); + std::cout << "Input ports (" << inputs_g1.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs_g1 ) { + std::cout << "\t\"" << portName << "\" accepting type " << portType << "\n"; + } + auto outputs_g1 = g1.getAllDataGetters(); + std::cout << "Output ports (" << outputs_g1.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs_g1 ) { + std::cout << "\t\"" << portName << "\" generating type " << portType << "\n"; + } + //! [Inspect the graph interface : inputs and outputs port] + + //! [Verifing the graph can be compiled] + if ( !g1.compile() ) { + std::cout << "Compilation failed for the loaded graph"; + return 2; + } + //! [Verifing the graph can be compiled] + + //! [Creating input variable to test the graph] + VectorType test; + test.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + for ( int n = 0; n < test.capacity(); ++n ) { + test.push_back( dis( gen ) ); + } + + std::cout << "Input values : \n\t"; + for ( auto ord : test ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Creating input variable to test the graph] + + //! [setting the values processed by the graph] + auto input = g1.getDataSetter( "Source_to" ); + input->setData( &test ); + //! [setting the values processed by the graph] + + //! [Execute the graph] + std::cout << "Executing the loaded graph ...\n"; + g1.execute(); + //! [Execute the graph] + + //! [Print the output result] + auto output = g1.getDataGetter( "Sink_from" ); + VectorType result; + output->getData( result ); + std::cout << "Output values : \n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Print the output result] + + //! [Set the correct filter on the filter node] + auto filter = + std::dynamic_pointer_cast>( g1.getNode( "Filter" ) ); + if ( !filter ) { + std::cerr << "Unable to cast the filter to the right type\n"; + return 3; + } + filter->setFilterFunction( []( const Scalar& f ) { return f > 0.5_ra; } ); + //! [Set the correct filter on the filter node] + + //! [Execute the graph] + std::cout << "Executing the re-parameterized graph ...\n"; + g1.execute(); + output->getData( result ); + std::cout << "Output values : \n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Execute the graph] + + return 0; +} diff --git a/examples/DataflowExamples/HelloGraph/CMakeLists.txt b/examples/DataflowExamples/HelloGraph/CMakeLists.txt new file mode 100644 index 00000000000..6d2ab47e0ae --- /dev/null +++ b/examples/DataflowExamples/HelloGraph/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(HelloGraph VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/HelloGraph/main.cpp b/examples/DataflowExamples/HelloGraph/main.cpp new file mode 100644 index 00000000000..e9be6014fd1 --- /dev/null +++ b/examples/DataflowExamples/HelloGraph/main.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +/* ----------------------------------------------------------------------------------- */ +/** + * \brief Demonstrate how to use a graph to filter a collection + */ +int main( int argc, char* argv[] ) { + using RaVector = Ra::Core::VectorArray; + + //! [Creating an empty graph] + DataflowGraph g { "helloGraph" }; + //! [Creating an empty graph] + + //! [Creating Nodes] + auto sourceNode = g.addNode>( "Source" ); + auto predicateNode = g.addNode( "Selector" ); + auto filterNode = g.addNode>( "Filter" ); + auto sinkNode = g.addNode>( "Sink" ); + //! [Creating Nodes] + + //! [Creating links between Nodes] + // link using nodes port, with compile time type check + // node belongship to the graph is checked at runtime + if ( !g.addLink( sourceNode->getOuputPort(), filterNode->getInPort() ) ) { std::abort(); } + + // link with port names, all runtime check + if ( !g.addLink( predicateNode, "f", filterNode, "f" ) ) { std::abort(); } + // one can also link using node index, it depends on node init, so be sure to have the right + // index + // Functional Port "out" as index 0, and Sink "from" is 0 also + if ( !g.addLink( filterNode, Node::PortIndex { 0 }, sinkNode, Node::PortIndex { 0 } ) ) { + std::abort(); + } + //! [Creating links between Nodes] + + //! [Verifying the graph can be compiled] + if ( !g.compile() ) { + std::cout << " compilation failed"; + return 1; + } + //! [Verifying the graph can be compiled] + + //! [Configure nodes] + RaVector test( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + std::generate( test.begin(), test.end(), [&dis, &gen]() { return dis( gen ); } ); + sourceNode->setData( test ); + + Sources::ScalarUnaryPredicateSource::function_type pred = []( Scalar x ) { return x < 0.5; }; + predicateNode->setData( &pred ); + //! [Configure nodes] + + std::cout << "Input values : \n\t"; + std::copy( test.begin(), test.end(), std::ostream_iterator( std::cout, " " ) ); + std::cout << '\n'; + + //! [Execute the graph] + g.execute(); + //! [Execute the graph] + + //! [Print the output result] + auto& result = sinkNode->getDataByRef(); + std::cout << "Output values (reference): " << result.size() << "\n\t"; + std::copy( result.begin(), result.end(), std::ostream_iterator( std::cout, " " ) ); + std::cout << '\n'; + //! [Print the output result] + + //! [Modify input and rerun the graph] + Sources::ScalarUnaryPredicateSource::function_type predbig = []( Scalar x ) { return x > 0.5; }; + predicateNode->setData( &predbig ); + + g.execute(); + // since result is a ref to node's output, no need to get it again + std::cout << "Output values after second execution: " << result.size() << "\n\t"; + std::copy( result.begin(), result.end(), std::ostream_iterator( std::cout, " " ) ); + std::cout << '\n'; + //! [Modify input and rerun the graph] + + return 0; +} diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index a09f9af4099..7b06fe131fe 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -101,6 +101,8 @@ else() add_subdirectory(IO) # add Gui dependencies add_subdirectory(Gui) + # add Dataflow dependencies + add_subdirectory(Dataflow) # generate radium-option.cmake configuration file file(REMOVE "${CMAKE_INSTALL_PREFIX}/radium-options.cmake") diff --git a/external/Core/CMakeLists.txt b/external/Core/CMakeLists.txt index 06fd9589787..21ce5678226 100644 --- a/external/Core/CMakeLists.txt +++ b/external/Core/CMakeLists.txt @@ -19,7 +19,7 @@ add_custom_target(CoreExternals ALL) if(NOT DEFINED Eigen3_DIR) check_externals_prerequisite() - status_message("" "eigen3" "remote git") + status_message("[CoreExternal]" "eigen3" "remote git") ExternalProject_Add( Eigen3 @@ -41,7 +41,7 @@ endif() if(NOT DEFINED OpenMesh_DIR) check_externals_prerequisite() - status_message("" "OpenMesh" "remote git") + status_message("[CoreExternal]" "OpenMesh" "remote git") # I found some problems when generating xcode project for OpenMesh: the libXXX.dylib link to # libXXX.Major.minor.dylib (eg libOpenMesh.2.1.dylib) is not generated and the script failed. # Need to generate this link manually. TODO, find why only OpenMesh is problematic @@ -63,7 +63,7 @@ endif() if(NOT DEFINED cpplocate_DIR OR NOT cpplocate_DIR) check_externals_prerequisite() - status_message("" "cpplocate" "remote git") + status_message("[CoreExternal]" "cpplocate" "remote git") ExternalProject_Add( cpplocate GIT_REPOSITORY https://github.com/cginternals/cpplocate.git diff --git a/external/Dataflow/CMakeLists.txt b/external/Dataflow/CMakeLists.txt new file mode 100644 index 00000000000..b991d7c59d2 --- /dev/null +++ b/external/Dataflow/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.6) + +project(radiumdataflow-external VERSION 1.0.0) + +include(ExternalProject) +include(ExternalInclude) + +list(APPEND CMAKE_MESSAGE_INDENT "[Dataflow] ") +string(REPLACE ";" "" indent_string "${CMAKE_MESSAGE_INDENT}") +set(indent_string "${indent_string}--") + +# force installing by default all the external projects +set_property(DIRECTORY PROPERTY EP_STEP_TARGETS install) + +# Add fPIC for all dependencies +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +add_custom_target(DataflowExternals ALL) + +if(NOT DEFINED RadiumNodeEditor_DIR) + check_externals_prerequisite() + status_message("" "RadiumNodeEditor" "remote git") + ExternalProject_Add( + RadiumNodeEditor + GIT_REPOSITORY https://github.com/MathiasPaulin/RadiumQtNodeEditor.git + GIT_TAG main + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + INSTALL_DIR "${CMAKE_INSTALL_PREFIX}" + CMAKE_ARGS ${RADIUM_EXTERNAL_CMAKE_OPTIONS} -DCMAKE_INSTALL_PREFIX= + "-DCMAKE_MESSAGE_INDENT=${indent_string}\;" + ) + set_external_dir(RadiumNodeEditor "lib/cmake/RadiumNodeEditor/") + add_dependencies(DataflowExternals RadiumNodeEditor) +else() + status_message("" "RadiumNodeEditor" ${RadiumNodeEditor_DIR}) +endif() diff --git a/scripts/generateFilelistForModule.sh b/scripts/generateFilelistForModule.sh index 9448c419fac..2e3caa7336f 100755 --- a/scripts/generateFilelistForModule.sh +++ b/scripts/generateFilelistForModule.sh @@ -7,12 +7,16 @@ fi BASE=$1 LOWBASE=$(echo "$BASE" | tr '[:upper:]' '[:lower:]') +LOWBASE=$(echo "$LOWBASE" | tr '/' '_') OUTPUT="../src/${BASE}/filelist.cmake" echo "generate [${BASE}] filelist" echo "-- input files from [../src/${BASE}]" echo "-- output vars prefix [${LOWBASE}]" echo "-- output file is [${OUTPUT}]" +DELIM='/' +SLASHES=$( awk -F"$DELIM" '{print NF-1}' <<<"${BASE}" ) +(( SLASHES = 4 + SLASHES )) rm -f "${OUTPUT}" @@ -32,7 +36,7 @@ function genList(){ if [ ! -z "$L" ] then echo "set(${LOWBASE}_${suffix}" >> "${OUTPUT}" - echo "${L}" | cut -f 4- -d/ | grep -v pch.hpp | sort | xargs -n1 echo " " >> "${OUTPUT}" + echo "${L}" | cut -f $SLASHES- -d/ | grep -v pch.hpp | sort | xargs -n1 echo " " >> "${OUTPUT}" echo ")" >> "${OUTPUT}" echo "" >> "${OUTPUT}" fi diff --git a/scripts/list_dep.py b/scripts/list_dep.py index 263e731e273..6c0de6cda84 100755 --- a/scripts/list_dep.py +++ b/scripts/list_dep.py @@ -63,7 +63,7 @@ if len(dep) >0 : modules[filename.parts[0]] = dep dep={} -for module in {'Core', 'Engine', 'IO', 'Gui', 'Headless'}: +for module in {'Core', 'Engine', 'IO', 'Gui', 'Headless', 'Dataflow'}: if module in modules: dep = modules[module] for key in dep: @@ -71,7 +71,7 @@ print("\n\nRadium is compiled and tested with specific version of dependencies, as given in the external's folder CMakeLists.txt and state here for the record\n\n") -for module in {'Core', 'Engine', 'IO', 'Gui', 'Headless'}: +for module in {'Core', 'Engine', 'IO', 'Gui', 'Headless', 'Dataflow'}: if module in modules: dep = modules[module] print(f"* [{module}]") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5ff89b416a7..50b71e8094a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,7 +31,7 @@ set(CONFIG_PACKAGE_LOCATION lib/cmake/Radium) # ----------------------------------------------------------------------------------- # Include sources and declare components -foreach(lib Core Engine IO PluginBase Gui Headless) +foreach(lib Core Engine IO PluginBase Gui Headless Dataflow) string(TOUPPER ${lib} upcase_lib) if(RADIUM_GENERATE_LIB_${upcase_lib}) add_subdirectory(${lib}) diff --git a/src/Core/Random/RandomPointSet.cpp b/src/Core/Random/RandomPointSet.cpp new file mode 100644 index 00000000000..1529c8de6d9 --- /dev/null +++ b/src/Core/Random/RandomPointSet.cpp @@ -0,0 +1,56 @@ +#include + +namespace Ra { +namespace Core { +namespace Random { + +FibonacciSequence::FibonacciSequence( size_t number ) : n { std::max( size_t( 5 ), number ) } {} + +size_t FibonacciSequence::range() { + return n; +} +Scalar FibonacciSequence::operator()( size_t i ) { + return Scalar( i ) / phi; +} + +Scalar VanDerCorputSequence::operator()( unsigned int bits ) { + bits = ( bits << 16u ) | ( bits >> 16u ); + bits = ( ( bits & 0x55555555u ) << 1u ) | ( ( bits & 0xAAAAAAAAu ) >> 1u ); + bits = ( ( bits & 0x33333333u ) << 2u ) | ( ( bits & 0xCCCCCCCCu ) >> 2u ); + bits = ( ( bits & 0x0F0F0F0Fu ) << 4u ) | ( ( bits & 0xF0F0F0F0u ) >> 4u ); + bits = ( ( bits & 0x00FF00FFu ) << 8u ) | ( ( bits & 0xFF00FF00u ) >> 8u ); + return Scalar( float( bits ) * 2.3283064365386963e-10_ra ); // / 0x100000000 +} + +FibonacciPointSet::FibonacciPointSet( size_t n ) : seq( n ) {} + +size_t FibonacciPointSet::range() { + return seq.range(); +} +Ra::Core::Vector2 FibonacciPointSet::operator()( size_t i ) { + return { seq( i ), Scalar( i ) / Scalar( range() ) }; +} + +HammersleyPointSet::HammersleyPointSet( size_t number ) : n( number ) {} + +size_t HammersleyPointSet::range() { + return n; +} + +Ra::Core::Vector2 HammersleyPointSet::operator()( size_t i ) { + return { Scalar( i ) / Scalar( range() ), seq( i ) }; +} + +MersenneTwisterPointSet::MersenneTwisterPointSet( size_t number ) : + gen( 0 ), seq( 0._ra, 1._ra ), n( number ) {} + +size_t MersenneTwisterPointSet::range() { + return n; +} +Ra::Core::Vector2 MersenneTwisterPointSet::operator()( size_t ) { + return { seq( gen ), seq( gen ) }; +} + +} // namespace Random +} // namespace Core +} // namespace Ra diff --git a/src/Core/Random/RandomPointSet.hpp b/src/Core/Random/RandomPointSet.hpp new file mode 100644 index 00000000000..d79f620f98c --- /dev/null +++ b/src/Core/Random/RandomPointSet.hpp @@ -0,0 +1,180 @@ +#pragma once +#include + +#include + +#include + +namespace Ra { +namespace Core { + +/** \brief Random point set utilities. + * + * This namespace contains functions, classes and utilities for 1D, 2D and 3D random point sets + * management. + * + */ +namespace Random { + +/** + * \brief Defines some helper constexpr functions + */ +namespace internal { +Scalar constexpr sqrtNewtonRaphsonHelper( Scalar x, Scalar curr, Scalar prev ) { + return curr == prev ? curr : sqrtNewtonRaphsonHelper( x, 0.5_ra * ( curr + x / curr ), curr ); +} +Scalar constexpr sqrtConstExpr( Scalar x ) { + return sqrtNewtonRaphsonHelper( x, x, 0_ra ); +} +} // namespace internal + +/** \brief Implements the fibonacci sequence + * i --> i/phi + * where phi = (1 + sqrt(5)) / 2 + */ +class RA_CORE_API FibonacciSequence +{ + + static constexpr Scalar phi = ( 1_ra + internal::sqrtConstExpr( 5_ra ) ) / 2_ra; + size_t n; + + public: + explicit FibonacciSequence( size_t number ); + // copyable + FibonacciSequence( const FibonacciSequence& ) = default; + FibonacciSequence& operator=( const FibonacciSequence& ) = default; + // movable + FibonacciSequence( FibonacciSequence&& ) = default; + FibonacciSequence& operator=( FibonacciSequence&& ) = default; + virtual ~FibonacciSequence() = default; + + size_t range(); + Scalar operator()( size_t i ); +}; + +/** \brief 1D Van der Corput sequence + * only implemented for 32bits floats (converted out to Scalar) + */ +struct RA_CORE_API VanDerCorputSequence { + Scalar operator()( unsigned int bits ); +}; + +/** \brief Implements the 2D fibonacci Point set + * points follow the FibonacciSequence + * (i, N) => [i / phi, i / N] + */ +class RA_CORE_API FibonacciPointSet +{ + FibonacciSequence seq; + + public: + explicit FibonacciPointSet( size_t n ); + // copyable + FibonacciPointSet( const FibonacciPointSet& ) = default; + FibonacciPointSet& operator=( const FibonacciPointSet& ) = default; + // movable + FibonacciPointSet( FibonacciPointSet&& ) = default; + FibonacciPointSet& operator=( FibonacciPointSet&& ) = default; + virtual ~FibonacciPointSet() = default; + + size_t range(); + Ra::Core::Vector2 operator()( size_t i ); +}; + +/** \brief 2D Hammersley point set + * + */ +class RA_CORE_API HammersleyPointSet +{ + VanDerCorputSequence seq; + size_t n; + + public: + explicit HammersleyPointSet( size_t number ); + // copyable + HammersleyPointSet( const HammersleyPointSet& ) = default; + HammersleyPointSet& operator=( const HammersleyPointSet& ) = default; + // movable + HammersleyPointSet( HammersleyPointSet&& ) = default; + HammersleyPointSet& operator=( HammersleyPointSet&& ) = default; + virtual ~HammersleyPointSet() = default; + + size_t range(); + Ra::Core::Vector2 operator()( size_t i ); +}; + +/** \brief 2D Random point set + */ +class RA_CORE_API MersenneTwisterPointSet +{ + std::mt19937 gen; + std::uniform_real_distribution seq; + size_t n; + + public: + explicit MersenneTwisterPointSet( size_t number ); + // copyable + MersenneTwisterPointSet( const MersenneTwisterPointSet& ) = default; + MersenneTwisterPointSet& operator=( const MersenneTwisterPointSet& ) = default; + // movable + MersenneTwisterPointSet( MersenneTwisterPointSet&& ) = default; + MersenneTwisterPointSet& operator=( MersenneTwisterPointSet&& ) = default; + virtual ~MersenneTwisterPointSet() = default; + + size_t range(); + Ra::Core::Vector2 operator()( size_t ); +}; + +// https://observablehq.com/@mbostock/spherical-fibonacci-lattice +// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html +/** \brief Map a [0, 1)^2 point set on the unit sphere + * \tparam PointSet the random point set type to map on sphere + */ +template +class SphericalPointSet +{ + PointSet p; + Ra::Core::Vector3 projectOnSphere( const Ra::Core::Vector2&& pt ); + + public: + explicit SphericalPointSet( size_t n ); + // copyable + SphericalPointSet( const SphericalPointSet& ) = default; + SphericalPointSet& operator=( const SphericalPointSet& ) = default; + // movable + SphericalPointSet( SphericalPointSet&& ) = default; + SphericalPointSet& operator=( SphericalPointSet&& ) = default; + virtual ~SphericalPointSet() = default; + + size_t range(); + Ra::Core::Vector3 operator()( size_t i ); +}; + +// ------------------------------------------------------------------------ +// ------------------------ inline methods -------------------------------- + +template +Ra::Core::Vector3 SphericalPointSet::projectOnSphere( const Ra::Core::Vector2&& pt ) { + Scalar theta = std::acos( 2 * pt[1] - 1 ); // 0 <= tetha <= pi + Scalar phi = 2_ra * Scalar( M_PI ) * pt[0]; + return { std::sin( theta ) * std::cos( phi ), + std::sin( theta ) * std::sin( phi ), + std::cos( theta ) }; +} + +template +SphericalPointSet::SphericalPointSet( size_t n ) : p( n ) {} + +template +size_t SphericalPointSet::range() { + return p.range(); +} + +template +Ra::Core::Vector3 SphericalPointSet::operator()( size_t i ) { + return projectOnSphere( p( i ) ); +} + +} // namespace Random +} // namespace Core +} // namespace Ra diff --git a/src/Core/Utils/TypesUtils.hpp b/src/Core/Utils/TypesUtils.hpp index ac3d962d9e3..6305cad088f 100644 --- a/src/Core/Utils/TypesUtils.hpp +++ b/src/Core/Utils/TypesUtils.hpp @@ -1,6 +1,5 @@ #pragma once - -#include +#include #ifndef _WIN32 # include @@ -10,6 +9,7 @@ #endif #include +#include #include @@ -19,11 +19,40 @@ namespace Utils { /// Return the human readable version of the type name T template -const char* demangleType() noexcept; +std::string demangleType() noexcept; /// Return the human readable version of the given object's type template -const char* demangleType( const T& ) noexcept; +std::string demangleType( const T& ) noexcept; + +/// Return the human readable version of the given type name +std::string demangleType( const std::type_index& typeIndex ) noexcept; + +// Check if a type is a container with access to its element type and number +// adapted from https://stackoverflow.com/questions/13830158/check-if-a-variable-type-is-iterable +namespace detail { + +using std::begin; +using std::end; + +template +auto is_container_impl( int ) + -> decltype( begin( std::declval() ) != + end( std::declval() ), // begin/end and operator != + void(), // Handle evil operator , + std::declval().empty(), + std::declval().size(), + ++std::declval() ) )&>(), // operator ++ + void( *begin( std::declval() ) ), // operator* + std::true_type {} ); + +template +std::false_type is_container_impl( ... ); + +} // namespace detail + +template +using is_container = decltype( detail::is_container_impl( 0 ) ); // TypeList taken and adapted from // https://github.com/AcademySoftwareFoundation/openvdb/blob/master/openvdb/openvdb/TypeList.h @@ -89,55 +118,49 @@ struct TypeList { }; #ifdef _WIN32 -// On windows (since MSVC 2019), typeid( T ).name() returns the demangled name -template -const char* demangleType() noexcept { - static auto demangled_name = []() { - std::string retval { typeid( T ).name() }; - removeAllInString( retval, "class " ); - removeAllInString( retval, "struct " ); - replaceAllInString( retval, ",", ", " ); - replaceAllInString( retval, "> >", ">>" ); - return retval; - }(); - - return demangled_name.data(); +// On windows (since MSVC 2019), typeid( T ).name() (and then typeIndex.name() returns the demangled +// name +inline std::string demangleType( const std::type_index& typeIndex ) noexcept { + std::string retval = typeIndex.name(); + removeAllInString( retval, "class " ); + removeAllInString( retval, "struct " ); + removeAllInString( retval, "__cdecl" ); + replaceAllInString( retval, "& __ptr64", "&" ); + replaceAllInString( retval, ",", ", " ); + replaceAllInString( retval, " >", ">" ); + replaceAllInString( retval, "__int64", "long" ); + replaceAllInString( retval, "const &", "const&" ); + return retval; } #else +// On Linux/macos, use the C++ ABI demangler +inline std::string demangleType( const std::type_index& typeIndex ) noexcept { + int error = 0; + std::string retval; + char* name = abi::__cxa_demangle( typeIndex.name(), 0, 0, &error ); + if ( error == 0 ) { retval = name; } + else { + // error : -1 --> memory allocation failed + // error : -2 --> not a valid mangled name + // error : other --> __cxa_demangle + retval = std::string( "Type demangler error : " ) + std::to_string( error ); + } + std::free( name ); + removeAllInString( retval, "__1::" ); // or "::__1" ? + replaceAllInString( retval, " >", ">" ); + return retval; +} +#endif template -const char* demangleType() noexcept { +std::string demangleType() noexcept { // once per one type - static auto demangled_name = []() { - int error = 0; - std::string retval; - char* name = abi::__cxa_demangle( typeid( T ).name(), 0, 0, &error ); - - switch ( error ) { - case 0: - retval = name; - break; - case -1: - retval = "memory allocation failed"; - break; - case -2: - retval = "not a valid mangled name"; - break; - default: - retval = "__cxa_demangle failed"; - break; - } - std::free( name ); - removeAllInString( retval, "__1::" ); // or "::__1" ? - replaceAllInString( retval, "> >", ">>" ); - return retval; - }(); - - return demangled_name.data(); + static auto demangled_name = demangleType( std::type_index( typeid( T ) ) ); + return demangled_name; } -#endif + // calling with instances template -const char* demangleType( const T& ) noexcept { +std::string demangleType( const T& ) noexcept { return demangleType(); } diff --git a/src/Core/filelist.cmake b/src/Core/filelist.cmake index 7aabbe1b640..4760bf220f9 100644 --- a/src/Core/filelist.cmake +++ b/src/Core/filelist.cmake @@ -35,6 +35,7 @@ set(core_sources Geometry/TriangleMesh.cpp Geometry/Volume.cpp Geometry/deprecated/TopologicalMesh.cpp + Random/RandomPointSet.cpp Resources/Resources.cpp Tasks/TaskQueue.cpp Utils/Attribs.cpp @@ -107,6 +108,7 @@ set(core_headers Math/Math.hpp Math/Quadric.hpp RaCore.hpp + Random/RandomPointSet.hpp Resources/Resources.hpp Tasks/Task.hpp Tasks/TaskQueue.hpp diff --git a/src/Dataflow/CMakeLists.txt b/src/Dataflow/CMakeLists.txt new file mode 100644 index 00000000000..14e45548a65 --- /dev/null +++ b/src/Dataflow/CMakeLists.txt @@ -0,0 +1,35 @@ +set(ra_dataflow_target Dataflow) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflow_target}] ") + +project(${ra_dataflow_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +set(dataflow_headers RaDataflow.hpp) +add_library(${ra_dataflow_target} INTERFACE) + +if(RADIUM_GENERATE_LIB_CORE) + add_subdirectory(Core) + add_dependencies(${ra_dataflow_target} DataflowCore) + target_link_libraries(${ra_dataflow_target} INTERFACE DataflowCore) +endif() + +# if(RADIUM_GENERATE_LIB_ENGINE) add_subdirectory(Rendering) add_dependencies(${ra_dataflow_target} +# DataflowRendering) target_link_libraries(${ra_dataflow_target} INTERFACE DataflowRendering) +# endif() + +if(RADIUM_GENERATE_LIB_GUI) + add_subdirectory(QtGui) + add_dependencies(${ra_dataflow_target} DataflowQtGui) + target_link_libraries(${ra_dataflow_target} INTERFACE DataflowQtGui) +endif() + +message(STATUS "Configuring library ${ra_dataflow_target} with standard settings") +target_include_directories( + ${ra_dataflow_target} INTERFACE $ + $ +) +configure_radium_library( + TARGET ${ra_dataflow_target} COMPONENT + PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in FILES "${dataflow_headers}" +) +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflow_target} PARENT_SCOPE) +set(RADIUM_MISSING_COMPONENTS ${RADIUM_MISSING_COMPONENTS} PARENT_SCOPE) diff --git a/src/Dataflow/Config.cmake.in b/src/Dataflow/Config.cmake.in new file mode 100644 index 00000000000..562e08464e7 --- /dev/null +++ b/src/Dataflow/Config.cmake.in @@ -0,0 +1,44 @@ +# -------------- Configuration of the Radium Dataflow targets and definitions ----------------------- +# Setup Dataflow and check for dependencies +if (Dataflow_FOUND AND NOT TARGET Dataflow) + set(Configure_Dataflow ON) + # verify dependencies + if(NOT DataflowCore_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::Dataflow: dependency DataflowCore not found") + set(Configure_Dataflow OFF) + endif() + endif() + + if(NOT DataflowQtGui_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowQtGui/RadiumDataflowQtGuiConfig.cmake") + set(DataflowQtGui_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowQtGui/RadiumDataflowQtGuiConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::Dataflow: dependency DataflowQtGui not found") + set(Configure_Dataflow OFF) + endif() + endif() + +# to be uncommented when dataflow rendering subpackage will be available +# if(NOT DataflowRendering_FOUND) +# if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowRendering/RadiumDataflowRenderingConfig.cmake") +# set(DataflowRendering_FOUND TRUE) +# include(${CMAKE_CURRENT_LIST_DIR}/../DataflowRendering/RadiumDataflowRenderingConfig.cmake) +# else() +# set(Radium_FOUND False) +# set(Radium_NOT_FOUND_MESSAGE "Radium::Dataflow: dependency DataflowRendering not found") +# set(Configure_Dataflow OFF) +# endif() +# endif() +endif() + +# configure Dataflow component +if(Configure_Dataflow) + include("${CMAKE_CURRENT_LIST_DIR}/DataflowTargets.cmake") +endif() diff --git a/src/Dataflow/Core/CMakeLists.txt b/src/Dataflow/Core/CMakeLists.txt new file mode 100644 index 00000000000..4db4eb2428c --- /dev/null +++ b/src/Dataflow/Core/CMakeLists.txt @@ -0,0 +1,43 @@ +set(ra_dataflowcore_target DataflowCore) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflowcore_target}] ") + +project(${ra_dataflowcore_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +include(filelist.cmake) + +add_library( + ${ra_dataflowcore_target} SHARED ${dataflow_core_sources} ${dataflow_core_headers} + ${dataflow_core_private} +) + +# This one should be extracted directly from parent project properties. +target_compile_definitions(${ra_dataflowcore_target} PRIVATE RA_DATAFLOW_EXPORTS) + +target_compile_options(${ra_dataflowcore_target} PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) +if("${CMAKE_CXX_COMPILER_ID}" MATCHES "MSVC") + target_compile_options(${ra_dataflowcore_target} PRIVATE /bigobj) +endif() + +add_dependencies(${ra_dataflowcore_target} Core) +target_link_libraries(${ra_dataflowcore_target} PUBLIC Core) + +message(STATUS "Configuring library ${ra_dataflowcore_target} with standard settings") +configure_radium_target(${ra_dataflowcore_target}) +# configure the library only. The package is a sub-package and should be configured independently +configure_radium_library( + TARGET ${ra_dataflowcore_target} COMPONENT TARGET_DIR "Dataflow/Core" + FILES "${dataflow_core_headers}" +) +# Generate cmake package +configure_radium_package( + NAME ${ra_dataflowcore_target} PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + PACKAGE_DIR "lib/cmake/Radium/${ra_dataflowcore_target}" NAME_PREFIX "Radium" +) + +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflowcore_target} PARENT_SCOPE) + +if(RADIUM_ENABLE_PCH) + target_precompile_headers(${ra_dataflowcore_target} PRIVATE pch.hpp) +endif() + +list(REMOVE_AT CMAKE_MESSAGE_INDENT -1) diff --git a/src/Dataflow/Core/Config.cmake.in b/src/Dataflow/Core/Config.cmake.in new file mode 100644 index 00000000000..200aa5af5f9 --- /dev/null +++ b/src/Dataflow/Core/Config.cmake.in @@ -0,0 +1,21 @@ +# -------------- Configuration of the Radium DataflowCore targets and definitions ----------------------- +# Setup Engine and check for dependencies + +if (DataflowCore_FOUND AND NOT TARGET DataflowCore) + set(Configure_DataflowCore ON) + # verify dependencies + if(NOT Core_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Core/RadiumCoreConfig.cmake") + set(Core_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Core/RadiumCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowCore: dependency Core not found") + set(Configure_DataflowCore OFF) + endif() + endif() +endif() + +if(Configure_DataflowCore) + include("${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/DataflowCoreTargets.cmake") +endif() diff --git a/src/Dataflow/Core/DataflowGraph.cpp b/src/Dataflow/Core/DataflowGraph.cpp new file mode 100644 index 00000000000..aa5d8e46306 --- /dev/null +++ b/src/Dataflow/Core/DataflowGraph.cpp @@ -0,0 +1,774 @@ +#include +#include + +#include +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +using namespace Ra::Core::Utils; + +DataflowGraph::DataflowGraph( const std::string& name ) : DataflowGraph( name, getTypename() ) {} + +DataflowGraph::DataflowGraph( const std::string& instanceName, const std::string& typeName ) : + Node( instanceName, typeName ) { + // This will allow to edit subgraph in an independent editor + addEditableParameter( new EditableParameter( instanceName, *this ) ); + // A graph always use the builtin nodes factory + addFactory( NodeFactoriesManager::getDataFlowBuiltInsFactory() ); +} + +void DataflowGraph::init() { + if ( m_ready ) { + Node::init(); + std::for_each( m_nodesByLevel.begin(), m_nodesByLevel.end(), []( const auto& level ) { + std::for_each( level.begin(), level.end(), []( auto node ) { + if ( !node->isInitialized() ) { node->init(); } + } ); + } ); + } +} + +bool DataflowGraph::execute() { + if ( !m_ready ) { + if ( !compile() ) { return false; } + } + bool result = true; + std::for_each( m_nodesByLevel.begin(), m_nodesByLevel.end(), [&result]( const auto& level ) { + std::for_each( level.begin(), level.end(), [&result]( auto node ) { + result = result && node->execute(); + } ); + } ); + return result; +} + +void DataflowGraph::destroy() { + std::for_each( + m_nodesByLevel.begin(), m_nodesByLevel.end(), []( auto& level ) { level.clear(); } ); + m_nodesByLevel.clear(); + m_nodes.clear(); + m_factories.reset(); + m_dataSetters.erase( m_dataSetters.begin(), m_dataSetters.end() ); + Node::destroy(); + needsRecompile(); +} + +void DataflowGraph::saveToJson( const std::string& jsonFilePath ) { + if ( !jsonFilePath.empty() ) { + nlohmann::json data; + toJson( data ); + std::ofstream file( jsonFilePath ); + file << std::setw( 4 ) << data << std::endl; + m_shouldBeSaved = false; + } +} + +void DataflowGraph::toJsonInternal( nlohmann::json& data ) const { + nlohmann::json factories = nlohmann::json::array(); + nlohmann::json nodes = nlohmann::json::array(); + nlohmann::json connections = nlohmann::json::array(); + nlohmann::json model; + nlohmann::json graph; + + if ( m_factories ) { + for ( const auto& [name, factory] : *m_factories ) { + // do not save the standard factory, it will always be there + if ( name != NodeFactoriesManager::dataFlowBuiltInsFactoryName ) { + factories.push_back( name ); + } + } + graph["factories"] = factories; + } + + for ( const auto& n : m_nodes ) { + nlohmann::json nodeData; + n->toJson( nodeData ); + nodes.push_back( nodeData ); + for ( const auto& input : n->getInputs() ) { + if ( input->isLinked() ) { + nlohmann::json link = nlohmann::json::object(); + auto portOut = input->getLink(); + auto nodeOut = portOut->getNode(); + link["out_node"] = nodeOut->getInstanceName(); + link["out_port"] = portOut->getName(); + link["in_node"] = n->getInstanceName(); + link["in_port"] = input->getName(); + connections.push_back( link ); + } + } + } + + // write the common content of the Node to the json data + graph["nodes"] = nodes; + graph["connections"] = connections; + // Fill the specific concrete node information + data.emplace( "graph", graph ); +} + +bool DataflowGraph::loadFromJson( const std::string& jsonFilePath ) { + + if ( !nlohmann::json::accept( std::ifstream( jsonFilePath ) ) ) { + LOG( logERROR ) << jsonFilePath << " is not a valid json file !!"; + return false; + } + + std::ifstream file( jsonFilePath ); + nlohmann::json j; + file >> j; + m_shouldBeSaved = false; + return fromJson( j ); +} + +std::pair, std::string> +getLinkInfo( const std::string& which, + const nlohmann::json& linkData, + const std::map>& nodeByName ) { + std::string field = which + "_node"; + std::shared_ptr node { nullptr }; + + auto itNode = nodeByName.find( linkData[field] ); + if ( itNode != nodeByName.end() ) { node = itNode->second; } + else { + // Error, could not find the node + std::string msg = + std::string { "Node " } + which + " not found in cache " + " : " + linkData.dump(); + return { nullptr, msg }; + } + + std::string port; + field = which + "_port"; + if ( linkData.contains( field ) ) { + auto p = node->getPortByName( which, linkData[field] ).second; + if ( p != nullptr ) { port = p->getName(); } + } + else { + field = which + "_index"; + if ( linkData.contains( field ) ) { + auto p = node->getPortByIndex( which, Node::PortIndex { int { linkData[field] } } ); + if ( p != nullptr ) { port = p->getName(); } + } + } + if ( port.empty() ) { + std::string msg = std::string { "Port " } + which + " not found in node " + + node->getInstanceName() + " : " + linkData.dump(); + return { nullptr, msg }; + } + return { node, port }; +} + +bool DataflowGraph::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "graph" ) ) { + // indicate that the graph must be recompiled after loading + needsRecompile(); + // load the graph + m_factories.reset( new NodeFactorySet ); + addFactory( NodeFactoriesManager::getDataFlowBuiltInsFactory() ); + if ( data["graph"].contains( "factories" ) ) { + auto factories = data["graph"]["factories"]; + for ( const auto& factoryName : factories ) { + // Do not add factories already registered for the graph. + if ( m_factories->hasFactory( factoryName ) ) { continue; } + auto factory = NodeFactoriesManager::getFactory( factoryName ); + if ( factory ) { addFactory( factory ); } + else { + LOG( logERROR ) + << "DataflowGraph::loadFromJson : Unable to find a factory with name " + << factoryName; + return false; + } + } + } + std::map> nodeByName; + auto nodes = data["graph"]["nodes"]; + for ( auto& n : nodes ) { + if ( !n["model"].contains( "name" ) ) { + LOG( logERROR ) << "Found a node without model description." << n.dump() + << "Unable to build an instance."; + return false; + } + std::string nodeTypeName = n["model"]["name"]; + std::string instanceName; + + if ( n.contains( "instance" ) ) { instanceName = n["instance"]; } + else { + LOG( logERROR ) << "Found a node of type " << nodeTypeName + << " without identification "; + return false; + } + auto newNode = m_factories->createNode( nodeTypeName, n, this ); + if ( newNode ) { + if ( !instanceName.empty() ) { + auto [it, inserted] = nodeByName.insert( { instanceName, newNode } ); + if ( !inserted ) { + LOG( logERROR ) << "DataflowGraph::loadFromJson : duplicated node name " + << nodeTypeName; + return false; + } + } + } + else { + LOG( logERROR ) << "Unable to create the node " << nodeTypeName; + return false; + } + } + auto links = data["graph"]["connections"]; + for ( auto& l : links ) { + auto [nodeFrom, fromOutput] = getLinkInfo( "out", l, nodeByName ); + if ( nodeFrom == nullptr ) { + LOG( logERROR ) << "DataflowGraph::loadFromJson: error when parsing JSON." + << " Could not find the link source (" << fromOutput + << "). Link not added."; + return false; + } + auto [nodeTo, toInput] = getLinkInfo( "in", l, nodeByName ); + if ( nodeTo == nullptr ) { + LOG( logERROR ) << "DataflowGraph::loadFromJson: error when parsing JSON." + << " Could not find the link source (" << toInput + << "). Link not added."; + return false; + } + if ( !addLink( nodeFrom, fromOutput, nodeTo, toInput ) ) { + LOG( logERROR ) + << "DataflowGraph::loadFromJson: error when parsing JSON" + << ": Could not add a link (missing or wrong information, please refer to " + "the previous error messages). Link not added."; + return false; + } + } + } + return true; +} + +bool DataflowGraph::canAdd( const Node* newNode ) const { + return findNode( newNode ) == -1; +} + +bool DataflowGraph::addNode( std::shared_ptr newNode ) { + // Check if the new node already exists (= same name and type) + if ( canAdd( newNode.get() ) ) { + if ( newNode->getInputs().empty() || newNode->getOutputs().empty() ) { + bool ( DataflowGraph::*addGraphIOPort )( PortBase* ) = newNode->getInputs().empty() + ? &DataflowGraph::addSetter + : &DataflowGraph::addGetter; + auto& interfaces = newNode->buildInterfaces( this ); + for ( auto p : interfaces ) { + ( this->*addGraphIOPort )( p ); + } + } + m_nodes.emplace_back( std::move( newNode ) ); + needsRecompile(); + return true; + } + else { return false; } +} + +bool DataflowGraph::removeNode( std::shared_ptr node ) { + // This is to prevent graph destruction from the graph editor, depending on how it is used + if ( m_nodesAndLinksProtected ) { return false; } + + // Check if the node is in the list already exists (= same name) + int index = -1; + if ( ( index = findNode( node.get() ) ) == -1 ) { return false; } + else { + if ( node->getInputs().empty() || node->getOutputs().empty() ) { + bool ( DataflowGraph::*removeGraphIOPort )( const std::string& ) = + node->getInputs().empty() ? &DataflowGraph::removeSetter + : &DataflowGraph::removeGetter; + for ( auto& p : node->getInterfaces() ) { + ( this->*removeGraphIOPort )( p->getName() ); + } + } + m_nodes.erase( m_nodes.begin() + index ); + needsRecompile(); + return true; + } +} + +bool DataflowGraph::checkPortCompatibility( const Node* nodeFrom, + Node::PortIndex portOutIdx, + const PortBase* portOut, + const Node* nodeTo, + Node::PortIndex portInIdx, + const PortBase* portIn ) { + // Compare types + if ( !( portIn->getType() == portOut->getType() ) ) { + Log::addLinkTypeMismatch( nodeFrom, portOutIdx, portOut, nodeTo, portInIdx, portIn ); + return false; + } + + // Check if input is connected + if ( portIn->isLinked() ) { + Log::alreadyLinked( nodeTo, portIn ); + return false; + } + return true; +} + +void nodeNotFoundMessage( const std::string& type, const std::string& name, const Node* node ) { + LOG( logERROR ) << "DataflowGraph::addLink Unable to find " << type << "input port " << name + << " from destination node " << node->getInstanceName() << " (" + << node->getTypeName() << ")"; +} + +bool DataflowGraph::addLink( const std::shared_ptr& nodeFrom, + const std::string& nodeFromOutputName, + const std::shared_ptr& nodeTo, + const std::string& nodeToInputName ) { + if ( !checkNodeValidity( nodeFrom.get(), nodeTo.get() ) ) { return false; } + + auto [inputIdx, inputPort] = nodeTo->getInputByName( nodeToInputName ); + if ( !inputPort ) { + nodeNotFoundMessage( "input", nodeToInputName, nodeTo.get() ); + return false; + } + auto [outputIdx, outputPort] = nodeFrom->getOutputByName( nodeFromOutputName ); + if ( !outputPort ) { + nodeNotFoundMessage( "output", nodeFromOutputName, nodeFrom.get() ); + return false; + } + + // Compare types + if ( !checkPortCompatibility( + nodeFrom.get(), outputIdx, outputPort, nodeTo.get(), inputIdx, inputPort ) ) { + return false; + } + + // port can be connected + inputPort->connect( outputPort ); + // The state of the graph changes, set it to not ready + needsRecompile(); + return true; +} + +bool DataflowGraph::addLink( const std::shared_ptr& nodeFrom, + Node::PortIndex portOutIdx, + const std::shared_ptr& nodeTo, + Node::PortIndex portInIdx ) { + if ( !checkNodeValidity( nodeFrom.get(), nodeTo.get() ) ) { return false; } + + auto portOut = nodeFrom->getOutputByIndex( portOutIdx ); + auto portIn = nodeTo->getInputByIndex( portInIdx ); + + if ( !portOut ) { + Log::badPortIdx( "output", nodeFrom->getTypeName(), portOutIdx ); + return false; + } + if ( !portIn ) { + Log::badPortIdx( "input", nodeTo->getTypeName(), portInIdx ); + return false; + } + + // Compare types + if ( !checkPortCompatibility( + nodeFrom.get(), portOutIdx, portOut, nodeTo.get(), portInIdx, portIn ) ) + return false; + + // port can be connected + portIn->connect( portOut ); + // The state of the graph changes, set it to not ready + needsRecompile(); + return true; +} + +bool DataflowGraph::addLink( Node::PortBaseRawPtr outputPort, Node::PortBaseRawPtr inputPort ) { + if ( !inputPort->is_input() || outputPort->is_input() ) { return false; } + auto nodeFrom = outputPort->getNode(); + auto nodeTo = inputPort->getNode(); + if ( !checkNodeValidity( nodeFrom, nodeTo ) ) { return false; } + // Compare types + if ( !checkPortCompatibility( + nodeFrom, Node::PortIndex {}, outputPort, nodeTo, Node::PortIndex {}, inputPort ) ) { + return false; + } + // port can be connected + inputPort->connect( outputPort ); + // The state of the graph changes, set it to not ready + needsRecompile(); + return true; +} + +bool DataflowGraph::removeLink( std::shared_ptr node, const std::string& nodeInputName ) { + // This is to prevent graph destruction from the graph editor, depending on how it is used + if ( m_nodesAndLinksProtected ) { return false; } + + // Check node's existence in the graph + if ( findNode( node.get() ) == -1 ) { return false; } + + // Check if node's input exists + int found = -1; + int index = 0; + for ( auto& input : node->getInputs() ) { + if ( input->getName() == nodeInputName ) { + found = index; + break; + } + index++; + } + if ( found == -1 ) { return false; } + + node->getInputs()[found]->disconnect(); + needsRecompile(); + return true; +} + +int DataflowGraph::findNode( const Node* node ) const { + auto foundIt = std::find_if( + m_nodes.begin(), m_nodes.end(), [node]( const auto& p ) { return *p == *node; } ); + if ( foundIt != m_nodes.end() ) { return std::distance( m_nodes.begin(), foundIt ); } + else { return -1; } +} + +bool DataflowGraph::findNode2( const Node* node ) const { + if ( !node ) return false; + + for ( const auto& n : m_nodes ) { + if ( n.get() == node ) return true; + auto g = dynamic_cast( n.get() ); + if ( g ) { + if ( g->findNode2( node ) ) return true; + } + } + return false; +} + +bool DataflowGraph::compile() { + // Find useful nodes (directly or indirectly connected to a Sink) + std::unordered_map>> infoNodes; + for ( auto const& n : m_nodes ) { + // Find all sinks + if ( n->getOutputs().size() == 0 ) { + // Add the sink in the useful nodes set if any of his port is linked + bool activeSink { false }; + for ( const auto& p : n->getInputs() ) { + activeSink |= p->isLinked(); + } + if ( activeSink ) { + infoNodes.emplace( n.get(), std::pair>( 0, {} ) ); + // recursively add the predecessors of the sink + backtrackGraph( n.get(), infoNodes ); + } + } + } + // Compute the level (rank of execution) of useful nodes + int maxLevel = 0; + for ( auto& infNode : infoNodes ) { + auto n = infNode.first; + // Compute the nodes' level starting from sources + if ( n->getInputs().empty() ) { + // Tag successors + for ( auto const successor : infNode.second.second ) { + infoNodes[successor].first = + std::max( infoNodes[successor].first, infoNodes[n].first + 1 ); + maxLevel = std::max( maxLevel, + std::max( infoNodes[successor].first, + goThroughGraph( successor, infoNodes ) ) ); + } + } + } + m_nodesByLevel.clear(); + m_nodesByLevel.resize( infoNodes.size() != 0 ? maxLevel + 1 : 0 ); + for ( auto& infNode : infoNodes ) { + m_nodesByLevel[infNode.second.first].push_back( infNode.first ); + } + + // For each level + for ( auto& lvl : m_nodesByLevel ) { + // For each node + for ( size_t j = 0; j < lvl.size(); j++ ) { + if ( !lvl[j]->compile() ) { return m_ready = false; } + // For each input + for ( size_t k = 0; k < lvl[j]->getInputs().size(); k++ ) { + if ( lvl[j]->getInputs()[k]->isLinkMandatory() && + !lvl[j]->getInputs()[k]->isLinked() ) { + return m_ready = false; + } + } + } + } + m_ready = true; + init(); + return m_ready; +} + +void DataflowGraph::clearNodes() { + for ( size_t i = 0; i < m_nodesByLevel.size(); i++ ) { + m_nodesByLevel[i].clear(); + m_nodesByLevel[i].shrink_to_fit(); + } + m_nodesByLevel.clear(); + m_nodesByLevel.shrink_to_fit(); + m_nodes.erase( m_nodes.begin(), m_nodes.end() ); + m_nodes.shrink_to_fit(); + m_inputs.erase( m_inputs.begin(), m_inputs.end() ); + m_inputs.shrink_to_fit(); + m_outputs.erase( m_outputs.begin(), m_outputs.end() ); + m_outputs.shrink_to_fit(); + m_dataSetters.erase( m_dataSetters.begin(), m_dataSetters.end() ); + m_shouldBeSaved = true; +} + +void DataflowGraph::backtrackGraph( + Node* current, + std::unordered_map>>& infoNodes ) { + for ( auto& input : current->getInputs() ) { + if ( input->getLink() ) { + Node* previous = input->getLink()->getNode(); + if ( previous ) { + auto previousInInfoNodes = infoNodes.find( previous ); + if ( previousInInfoNodes != infoNodes.end() ) { + // If the previous node is already in the map, + // find if the current node is already a successor node + auto& previousSuccessors = previousInInfoNodes->second.second; + bool foundCurrent = std::any_of( previousSuccessors.begin(), + previousSuccessors.end(), + [current]( auto c ) { return c == current; } ); + if ( !foundCurrent ) { + // If the current node is not a successor node, add it to the list + previousSuccessors.push_back( current ); + } + } + else { + // Add node to info nodes + std::vector successors; + successors.push_back( current ); + infoNodes.emplace( + previous, + std::pair>( 0, std::move( successors ) ) ); + backtrackGraph( previous, infoNodes ); + } + } + } + } +} + +int DataflowGraph::goThroughGraph( + Node* current, + std::unordered_map>>& infoNodes ) { + int maxLevel = 0; + if ( infoNodes.find( current ) != infoNodes.end() ) { + for ( auto const& successor : infoNodes[current].second ) { + infoNodes[successor].first = + std::max( infoNodes[successor].first, infoNodes[current].first + 1 ); + maxLevel = + std::max( infoNodes[successor].first, goThroughGraph( successor, infoNodes ) ); + } + } + return maxLevel; +} + +bool DataflowGraph::addSetter( PortBase* in ) { + bool found = false; + if ( m_dataSetters.find( in->getName() ) == m_dataSetters.end() ) { + addInput( in ); + auto portOut = std::shared_ptr( in->reflect( this, in->getName() ) ); + m_dataSetters.emplace( std::make_pair( + in->getName(), + DataSetter { DataSetterDesc { portOut, portOut->getName(), portOut->getTypeName() }, + in } ) ); + found = true; + } + return found; +} + +bool DataflowGraph::removeSetter( const std::string& setterName ) { + bool removed = false; + auto itS = m_dataSetters.find( setterName ); + if ( itS != m_dataSetters.end() ) { + auto& [desPort, in] = itS->second; + removeInput( in ); + m_dataSetters.erase( itS ); + removed = true; + } + return removed; +} + +bool DataflowGraph::addGetter( PortBase* out ) { + if ( out->is_input() ) { return false; } + // This is very similar to addOutput, except the data can't be set. + // Data pointer must be set by any sink at compile time, in the init function to refer to + // the data fetched from the associated input link + bool found = false; + // TODO check if this verification is needed ? + for ( auto& output : m_outputs ) { + if ( output->getName() == out->getName() ) { + found = true; + break; + } + } + if ( !found ) { m_outputs.emplace_back( out ); } + return !found; +} + +bool DataflowGraph::removeGetter( const std::string& getterName ) { + auto getterP = std::find_if( m_outputs.begin(), m_outputs.end(), [getterName]( const auto& p ) { + return p->getName() == getterName; + } ); + bool found = false; + if ( getterP != m_outputs.end() ) { + m_outputs.erase( getterP ); + found = true; + } + return found; +} + +bool DataflowGraph::releaseDataSetter( const std::string& portName ) { + auto setter = m_dataSetters.find( portName ); + bool found = false; + if ( setter != m_dataSetters.end() ) { + auto [desc, in] = setter->second; + in->disconnect(); + found = true; + } + return found; +} + +// Why is this method useful if it is the same than getDataSetter ? +bool DataflowGraph::activateDataSetter( const std::string& portName ) { + return getDataSetter( portName ) != nullptr; +} + +std::shared_ptr DataflowGraph::getDataSetter( const std::string& portName ) { + auto setter = m_dataSetters.find( portName ); + std::shared_ptr p { nullptr }; + if ( setter != m_dataSetters.end() ) { + auto [desc, in] = setter->second; + p = std::get<0>( desc ); + in->disconnect(); + p->connect( in ); + } + return p; +} + +std::vector DataflowGraph::getAllDataSetters() const { + std::vector r; + r.reserve( m_dataSetters.size() ); + for ( auto& s : m_dataSetters ) { + r.push_back( s.second.first ); + } + return r; +} + +PortBase* DataflowGraph::getDataGetter( const std::string& portName ) { + auto portIt = std::find_if( m_outputs.begin(), m_outputs.end(), [portName]( const auto& p ) { + return p->getName() == portName; + } ); + PortBase* g { nullptr }; + if ( portIt != m_outputs.end() ) { g = portIt->get(); } + return g; +} + +std::vector DataflowGraph::getAllDataGetters() const { + std::vector r; + r.reserve( m_outputs.size() ); + for ( auto& portOut : m_outputs ) { + r.emplace_back( portOut.get(), portOut->getName(), portOut->getTypeName() ); + } + return r; +} + +std::shared_ptr DataflowGraph::getNode( const std::string& instanceNameNode ) const { + auto nodeIt = + std::find_if( m_nodes.begin(), m_nodes.end(), [instanceNameNode]( const auto& n ) { + return n->getInstanceName() == instanceNameNode; + } ); + if ( nodeIt != m_nodes.end() ) { return *nodeIt; } + LOG( logERROR ) << "DataflowGraph::getNode : The node with the instance name \"" + << instanceNameNode << "\" has not been found"; + return { nullptr }; +} + +std::shared_ptr DataflowGraph::loadGraphFromJsonFile( const std::string& filename ) { + if ( !nlohmann::json::accept( std::ifstream( filename ) ) ) { + LOG( logERROR ) << filename << " is not a valid json file !!"; + return nullptr; + } + + std::ifstream jsonFile( filename ); + nlohmann::json j; + jsonFile >> j; + + bool valid = false; + if ( j.contains( "instance" ) && j.contains( "model" ) ) { + valid = j["model"].contains( "name" ); + } + if ( !valid ) { + LOG( logERROR ) << "loadGraphFromJsonFile :" << filename + << " does not contain a valid json NodeGraph\n"; + return nullptr; + } + std::string instanceName = j["instance"]; + std::string graphType = j["model"]["name"]; + LOG( logINFO ) << "Loading the graph " << instanceName << ", with type " << graphType << "\n"; + + auto& fctMngr = Ra::Dataflow::Core::NodeFactoriesManager::getFactoryManager(); + auto ndldd = fctMngr.createNode( graphType, j ); + if ( ndldd == nullptr ) { + LOG( logERROR ) << "Unable to load a graph with type " << graphType << "\n"; + return nullptr; + } + + auto graph = std::dynamic_pointer_cast( ndldd ); + if ( graph != nullptr ) { + graph->m_shouldBeSaved = false; + return graph; + } + + LOG( logERROR ) << "Loaded graph not inheriting from DataflowGraph " << graphType << "\n"; + + return nullptr; +} + +bool DataflowGraph::checkNodeValidity( const Node* nodeFrom, const Node* nodeTo ) { + using namespace Ra::Core::Utils; // Check node "from" existence in the graph + if ( !findNode2( nodeFrom ) ) { + Log::unableToFind( "initial node", nodeFrom->getInstanceName() ); + return false; + } + + // Check node "to" existence in the graph + if ( !findNode2( nodeTo ) ) { + Log::unableToFind( "destination node", nodeTo->getInstanceName() ); + return false; + } + return true; +} + +void DataflowGraph::Log::alreadyLinked( const Node* node, const PortBase* port ) { + LOG( logERROR ) << "DataflowGraph::addLink destination port not available (already linked) for " + << node->getInstanceName() << " (" << node->getTypeName() << "), port " + << port->getName(); +} + +void DataflowGraph::Log::addLinkTypeMismatch( const Node* nodeFrom, + Node::PortIndex portOutIdx, + const PortBase* portOut, + const Node* nodeTo, + Node::PortIndex portInIdx, + const PortBase* portIn ) { + LOG( logERROR ) << "DataflowGraph::addLink type mismatch from " << nodeFrom->getInstanceName() + << " (" << nodeFrom->getTypeName() << ") / " << portOut->getName() << " (" + << portOutIdx << " with type " << portOut->getTypeName() << ")" + << " to " << nodeTo->getInstanceName() << " (" << nodeTo->getTypeName() + << ") / " << portIn->getName() << " (" << portInIdx << " with type " + << portIn->getTypeName() << ") "; +} + +void DataflowGraph::Log::unableToFind( const std::string& type, const std::string& instanceName ) { + LOG( logERROR ) << "DataflowGraph::addLink Unable to find " << type << " " << instanceName; +} + +void DataflowGraph::Log::badPortIdx( const std::string& type, + const std::string& instanceName, + Node::PortIndex idx ) { + LOG( logERROR ) << "DataflowGraph::addLink node " << instanceName << "as no " << type + << " port with index " << idx; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/DataflowGraph.hpp b/src/Dataflow/Core/DataflowGraph.hpp new file mode 100644 index 00000000000..1efcfc121ad --- /dev/null +++ b/src/Dataflow/Core/DataflowGraph.hpp @@ -0,0 +1,371 @@ +#pragma once +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/** + * \todo : clarify what is a source and how to use it + * if sources must be used through interface port only, delete the set_data on all sources + * \todo Are node aliases a should-have (to make editing more user friendly)??? + */ + +/** + * \brief Represent a set of connected nodes that define a Direct Acyclic Computational Graph + * Ownership of nodes is given to the graph at construction time. + * \todo make a "graph embedding node" that allow to seemlesly integrate a graph as a node in + * another graph + * --> Edition of a graph will allow loading and saving to a file directly + * --> Edition of an embeded graph will defer loading and saving to the parent graph + * --> for this, need to decide if a subgraph is stored in the json of its parent or in a + * separate file + */ +class RA_DATAFLOW_API DataflowGraph : public Node +{ + public: + /// Constructor. + /// The nodes pointing to external data are created here. + /// \param name The name of the render graph. + explicit DataflowGraph( const std::string& name ); + virtual ~DataflowGraph() = default; + + void init() override; + bool execute() override; + void destroy() override; + + /// \brief Set the factories to use when loading a graph. + /// \param factories new factory set that will replace the existing factory set if any. + void setNodeFactories( std::shared_ptr factories ) { m_factories = factories; } + + /// \brief Get the node factories associated with the graph. + /// \return returns nullptr if no factory set is associated with the graph. + std::shared_ptr getNodeFactories() const { return m_factories; } + + /// \brief Add a factory to the factory set of the graph. + /// Creates the factory set if it does not exists + /// \param f a shared pointer to the factory to be added. The name of this factory + void addFactory( std::shared_ptr f ); + + /// \brief Remove a factory from the factory set of the graph. + /// \param name the name of the factory to remove + /// \return true if the factory was found and removed + bool removeFactory( const std::string& name ) { return m_factories->removeFactory( name ); } + + /// \brief Loads nodes and links from a JSON file. + /// \param jsonFilePath The path to the JSON file. + /// \return true if the file was loaded, false if an error occurs. + bool loadFromJson( const std::string& jsonFilePath ); + + /// \brief Saves nodes and links to a JSON file. + /// \param jsonFilePath The path to the JSON file. + void saveToJson( const std::string& jsonFilePath ); + + /// \brief Adds a node to the render graph. + /// Adds interface ports to the node newNode and the corresponding input and output ports to + /// the graph. + /// \param newNode The node to add to the graph. + /// \return a pair with a bool and a raw pointer to the Node. If the bool is true, the raw + /// pointer is owned by the graph. If the bool is false, the raw pointer ownership is left to + /// the caller. + virtual bool addNode( std::shared_ptr newNode ); + template + std::shared_ptr addNode( U&&... u ); + + /// \brief Removes a node from the render graph. + /// Removes input and output ports, corresponding to interface ports of the node, from the + /// graph. \param node The node to remove from the graph. \return true if the node was removed + /// and the given pointer is set to nullptr, false else + virtual bool removeNode( std::shared_ptr node ); + + /// Connects two nodes of the render graph. + /// The two nodes must already be in the render graph (with the addNode(Node* newNode) + /// function), the first node's in port must be free and the connected in port and out port must + /// have the same type of data. + /// \param nodeFrom The node that contains the out port. + /// \param nodeFromOutputName The name of the out port in nodeFrom. + /// \param nodeTo The node that contains the in port. + /// \param nodeToInputName The name of the in port in nodeTo. + bool addLink( const std::shared_ptr& nodeFrom, + const std::string& nodeFromOutputName, + const std::shared_ptr& nodeTo, + const std::string& nodeToInputName ); + + bool addLink( const std::shared_ptr& nodeFrom, + Node::PortIndex portOutIdx, + const std::shared_ptr& nodeTo, + Node::PortIndex portInIdx ); + + bool addLink( Node::PortBaseRawPtr outputPort, Node::PortBaseRawPtr inputPort ); + + template + bool addLink( const std::shared_ptr>& outputPort, + const std::shared_ptr>& inputPort ); + + /// + /// \brief Removes the link connected to a node's input port + /// \param node the node to unlink + /// \param nodeInputName the name of the port to unlink + /// \return true if link is removed, false if not. + bool removeLink( std::shared_ptr, const std::string& nodeInputName ); + + /// \brief Get the vector of all the nodes on the graph + /// \return + const std::vector>& getNodes() const { return m_nodes; } + + /// Gets a specific node according to its instance name. + /// \param instanceNameNode The instance name of the node. + std::shared_ptr getNode( const std::string& instanceNameNode ) const; + + /// Gets the nodes ordered by level (after compilation) + const std::vector>& getNodesByLevel() const { return m_nodesByLevel; } + + /// Compile the render graph to check its validity and simplify it. + /// The compilation has multiple goals: + /// - Remove the nodes that have no direct or indirect connections to sink nodes + /// - Order the nodes by level according to their dependencies + /// - Check if every mandatory port is linked + bool compile() override; + + /// Gets the number of nodes + size_t getNodesCount() const { return m_nodes.size(); } + + /// Deletes all nodes from the render graph. + virtual void clearNodes(); + + /// Test if the graph is compiled + bool isCompiled() const { return m_ready; } + + /// Mark the graph as needing recompilation (useful to force recompilation and resources + /// update) + inline void needsRecompile(); + + /// \brief Gets an output port connected to the named input port of the graph. + /// Return the connected output port if success, sharing the ownership with the caller. + /// This output port could then be used through setter->setData( ptr ) to set the graph + /// input from the data pointer owned by the caller. \note As ownership is shared with the + /// caller, the graph must survive the returned pointer to be able to use the dataSetter.. + /// \params portName The name of the input port of the graph + std::shared_ptr getDataSetter( const std::string& portName ); + + /// \brief disconnect the data setting port from its inputs. + bool releaseDataSetter( const std::string& portName ); + /// \brief connect the data setting port from its inputs. + bool activateDataSetter( const std::string& portName ); + + /// \brief Returns an alias to the named output port of the graph. + /// Allows to get the data stored at this port after the execution of the graph. + /// \note ownership is left to the graph, not shared. The graph must survive the returned + /// pointer to be able to use the dataGetter. + /// \params portName the name of the output port + PortBase* getDataGetter( const std::string& portName ); + + /// \brief Data setter descriptor. + /// A Data setter descriptor is composed of an output port (linked by construction to an + /// input port of the graph), its name and its type. Use setData on the output port to pass + /// data to the graph + using DataSetterDesc = std::tuple, std::string, std::string>; + + /// \brief Data getter descriptor. + /// A Data getter descriptor is composed of an output port (belonging to any node of the + /// graph), its name and its type. Use getData on the output port to extract data from the + /// graph. \note, a dataGetter is valid only after successful compilation of the graph. + /// \todo find a way to test the validity of the getter (invalid if no path exists from any + /// source port to the associated sink port) + using DataGetterDesc = std::tuple; + + /// Creates a vector that stores all the existing DataSetters (\see getDataSetter) of the + /// graph. + /// TODO : Verify why, when listing the data setters, they are connected ... + std::vector getAllDataSetters() const; + + /// Creates a vector that stores all the existing DataGetters (\see getDataGetter) of the + /// graph. A tuple is composed of an output port belonging to the graph, its name its type. + std::vector getAllDataGetters() const; + bool findNode2( const Node* node ) const; + + bool shouldBeSaved() { return m_shouldBeSaved; } + + static const std::string& getTypename(); + + /** + * \brief Load a graph from the given file. + * \param filename + * Any type of graph that inherits from DataflowGraph can be loaded by this function as soon + * as the appropriate constructor is registered in the node factory. \return The loaded + * graph, as a DataFlowGraph pointer to be downcast to the correct type + */ + static std::shared_ptr loadGraphFromJsonFile( const std::string& filename ); + + /** + * \brief protect nodes and links from deletion. + * \param on true to protect, false to unprotect. + */ + void setNodesAndLinksProtection( bool on ) { m_nodesAndLinksProtected = on; } + + /** + * \brief get the protection status protect nodes and links from deletion + * \return the protection status + */ + bool getNodesAndLinksProtection() const { return m_nodesAndLinksProtected; } + + using Node::addInput; + using Node::addOutput; + + protected: + /** Allow derived class to construct the graph with their own static type + */ + DataflowGraph( const std::string& instanceName, const std::string& typeName ); + + bool fromJsonInternal( const nlohmann::json& data ) override; + void toJsonInternal( nlohmann::json& ) const override; + + /// \brief Test if a node can be added to a graph + /// \param newNode const naked pointer to the candidate node + /// \return true if ownership could be transferred to the graph. + virtual bool canAdd( const Node* newNode ) const; + + /// Returns the index of the given node in the graph. + /// if there is none, returns -1. + /// \param name The name of the node to find. + int findNode( const Node* node ) const; + + private: + // Internal helper functions + /// Internal compilation function that allows to go back in the render graph while filling + /// an information map. \param current The current node. \param infoNodes The map that + /// contains information about nodes. + void backtrackGraph( Node* current, + std::unordered_map>>& infoNodes ); + /// Internal compilation function that allows to go through the render graph, using an + /// information map. + /// \param current The current node. + /// \param infoNodes The map that contains information about nodes. + int goThroughGraph( Node* current, + std::unordered_map>>& infoNodes ); + + /// \brief Adds an input port to the graph and associate it with a dataSetter. + /// This port is aliased as an interface port in a source node of the graph. + /// This function checks if there is no input port with the same name already + /// associated with the graph. + bool addSetter( PortBase* in ); + + /// \brief Remove the given setter from the graph + /// \param setterName + /// \return true if the setter was removed, false else. + bool removeSetter( const std::string& setterName ); + + /// \brief Adds an out port for a Graph and register it as a dataGetter. + /// This port is aliased as an interface port in a sink node of the graph. + /// This function checks if there is no out port with the same name already + /// associated with the graph. + /// \param out The port to add. + bool addGetter( PortBase* out ); + + /// \brief Remove the given getter from the graph + /// \param getterName + /// \return true if the getter was removed, false else. + bool removeGetter( const std::string& getterName ); + + bool checkNodeValidity( const Node* nodeFrom, const Node* nodeTo ); + static bool checkPortCompatibility( const Node* nodeFrom, + Node::PortIndex portOutIdx, + const PortBase* portOut, + const Node* nodeTo, + Node::PortIndex portInIdx, + const PortBase* portIn ); + class RA_DATAFLOW_API Log + { + public: + static void alreadyLinked( const Node* node, const PortBase* port ); + static void addLinkTypeMismatch( const Node* nodeFrom, + Node::PortIndex portOutIdx, + const PortBase* portOut, + const Node* nodeTo, + Node::PortIndex portInIdx, + const PortBase* portIn ); + static void unableToFind( const std::string& type, const std::string& instanceName ); + static void + badPortIdx( const std::string& type, const std::string& instanceName, Node::PortIndex idx ); + }; + + /// Flag that indicates if the graph should be saved to a file + /// This flag is useless outside an load/edit/save scenario + bool m_shouldBeSaved { false }; + + /// Flag set after successful compilation indicating graph is ready to be executed + /// This flag is reset as soon as the graph is modified. + bool m_ready { false }; + + /// The node factory to use for loading + std::shared_ptr m_factories; + /// The unordered list of nodes. + std::vector> m_nodes; + // Internal node levels representation + /// The list of nodes ordered by levels. + /// Two nodes at the same level have no dependency between them. + std::vector> m_nodesByLevel; + + /// Data setters management : used to pass parameter to the graph when the graph is not + /// embedded into another graph (inputs are here for this case). A dataSetter is an + /// outputPort, associated to an input port of the graph. The connection between these ports + /// can be activated/deactivated using activateDataSetter/releaseDataSetter + using DataSetter = std::pair; + std::map m_dataSetters; + + bool m_nodesAndLinksProtected { false }; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline void DataflowGraph::addFactory( std::shared_ptr f ) { + if ( !m_factories ) { m_factories.reset( new NodeFactorySet ); } + m_factories->addFactory( f ); +} +template +std::shared_ptr DataflowGraph::addNode( U&&... u ) { + auto ret = std::make_shared( std::forward( u )... ); + if ( addNode( ret ) ) return ret; + return nullptr; +} + +template +bool DataflowGraph::addLink( const std::shared_ptr>& outputPort, + const std::shared_ptr>& inputPort ) { + using namespace Ra::Core::Utils; + + static_assert( std::is_same_v, "in and out port type mismatch" ); + auto nodeFrom = outputPort->getNode(); + auto nodeTo = inputPort->getNode(); + + if ( !checkNodeValidity( nodeFrom, nodeTo ) ) { return false; } + + if ( inputPort->isLinked() ) { + Log::alreadyLinked( nodeTo, inputPort.get() ); + return false; + } + inputPort->connect( outputPort.get() ); + + // The state of the graph changes, set it to not ready + needsRecompile(); + return true; +} + +inline void DataflowGraph::needsRecompile() { + m_shouldBeSaved = true; + m_ready = false; +} + +inline const std::string& DataflowGraph::getTypename() { + static std::string demangledTypeName { "Core DataflowGraph" }; + return demangledTypeName; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/EditableParameter.hpp b/src/Dataflow/Core/EditableParameter.hpp new file mode 100644 index 00000000000..ea6b5ac1737 --- /dev/null +++ b/src/Dataflow/Core/EditableParameter.hpp @@ -0,0 +1,97 @@ +#pragma once +#include + +#include +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/// \brief Basic introspection for Node internal data edition +/// This base class gives key information to associate editing capabilities (and gui) to +/// an node internal data. +/// \note This class seems to be very similar in its aim than the Ra::Engine::RenderParameter and +/// their editing capabilities through Ra::Gui::ParameterSetEditor. But, in order to be more +/// general, this class does not depend on Engine. +/// \todo Unify with Ra::Engine::RenderParameter (using Core only parameters set) +/// +struct RA_DATAFLOW_API EditableParameterBase { + /// \name Constructors + /// @{ + /// \brief delete default constructors. + EditableParameterBase() = delete; + EditableParameterBase( const EditableParameterBase& ) = delete; + EditableParameterBase& operator=( const EditableParameterBase& ) = delete; + + /// Construct an base editable parameter from its name and type hash + EditableParameterBase( const std::string& name, std::type_index typeIdx ); + ///@} + + virtual ~EditableParameterBase() = default; + std::string getName() const; + std::type_index getType() const; + /// Constraints on the edited data : json object describing the constraints + /// Add constraints or associated data to the editable. + void setConstraints( const nlohmann::json& constraints ); + /// get the constraints or the associated data from the editable. + const nlohmann::json& getConstraints() const; + + private: + std::string m_name { "" }; + std::type_index m_typeIdx; + + /// Constraints on the edited data + nlohmann::json m_constraints; +}; + +template +struct EditableParameter : public EditableParameterBase { + /// \name Constructors + /// @{ + /// \brief delete default constructors. + EditableParameter() = delete; + EditableParameter( const EditableParameter& ) = delete; + EditableParameter& operator=( const EditableParameter& ) = delete; + + /// Construct an editable parameter from its name and type hash + EditableParameter( const std::string& name, T& data ); + ///@} + + /// The data to edit. + /// This is a reference to any data stored in a node and that the user could change + T& m_data; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline EditableParameterBase::EditableParameterBase( const std::string& name, + std::type_index typeIdx ) : + m_name( name ), m_typeIdx( typeIdx ) {} + +template +EditableParameter::EditableParameter( const std::string& name, T& data ) : + EditableParameterBase( name, typeid( T ) ), m_data( data ) {} + +inline std::string EditableParameterBase::getName() const { + return m_name; +} + +inline std::type_index EditableParameterBase::getType() const { + return m_typeIdx; +} + +inline void EditableParameterBase::setConstraints( const nlohmann::json& constraints ) { + m_constraints = constraints; +} + +inline const nlohmann::json& EditableParameterBase::getConstraints() const { + return m_constraints; +} +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Enumerator.hpp b/src/Dataflow/Core/Enumerator.hpp new file mode 100644 index 00000000000..f8e1d8446a1 --- /dev/null +++ b/src/Dataflow/Core/Enumerator.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/** + * \brief This class might be removed in a future and replaced by an instantiation of + * Ra::Core::Utils::BijectiveAssociation. + * + * This class allows to associate Values of type T to an int. + * Used right now to build the Node edition UI. + * \tparam T + */ +template +class Enumerator : public Ra::Core::Utils::Observable&> +{ + std::vector m_values; + size_t m_currentIndex { 0 }; + T* m_currentValue; + + public: + explicit Enumerator( std::initializer_list values ); + const T& get() const; + size_t size() const; + bool set( size_t p ); + bool set( const T& v ); + typename std::vector::const_iterator begin() const; + typename std::vector::const_iterator end() const; + const T& operator[]( size_t p ) const; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +Enumerator::Enumerator( std::initializer_list values ) : + m_values { values }, m_currentValue { m_values.data() } {} + +template +const T& Enumerator::get() const { + return *m_currentValue; +} + +template +size_t Enumerator::size() const { + return m_values.size(); +} + +template +bool Enumerator::set( size_t p ) { + if ( p < m_values.size() ) { + m_currentValue = m_values.data() + p; + m_currentIndex = p; + this->notify( *this ); + return true; + } + else { return false; } +} + +template +bool Enumerator::set( const T& v ) { + size_t p = 0; + for ( const auto& e : m_values ) { + if ( e == v ) { return set( p ); } + p++; + } + return false; +} + +template +typename std::vector::const_iterator Enumerator::begin() const { + return m_values.cbegin(); +} + +template +typename std::vector::const_iterator Enumerator::end() const { + return m_values.cend(); +} + +template +const T& Enumerator::operator[]( size_t p ) const { + return m_values.at( p ); +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Node.cpp b/src/Dataflow/Core/Node.cpp new file mode 100644 index 00000000000..56a582b2c64 --- /dev/null +++ b/src/Dataflow/Core/Node.cpp @@ -0,0 +1,103 @@ +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +using namespace Ra::Core::Utils; + +Node::Node( const std::string& instanceName, const std::string& typeName ) : + m_typeName { typeName }, m_instanceName { instanceName } {} + +bool Node::fromJson( const nlohmann::json& data ) { + if ( data.empty() ) { + // This is to avoid wrong error message when creating node from the editor + return true; + } + + if ( data.contains( "instance" ) ) { m_instanceName = data["instance"]; } + else { + LOG( logERROR ) << "Missing required instance name when loading node " << m_instanceName; + return false; + } + // get the common content of the Node from the json data + bool loaded = false; + if ( data.contains( "model" ) ) { + // get the specific concrete node information + const auto& datamodel = data["model"]; + loaded = fromJsonInternal( datamodel ); + } + else { + LOG( logERROR ) << "Missing required model when loading a Dataflow::Node"; + loaded = false; + } + // get the supplemental information related to application/gui/... + for ( auto& [key, value] : data.items() ) { + if ( key != "instance" && key != "model" ) { m_extraJsonData.emplace( key, value ); } + } + return loaded; +} + +void Node::toJson( nlohmann::json& data ) const { + + // write the common content of the Node to the json data + data["instance"] = m_instanceName; + + nlohmann::json model; + model["name"] = m_typeName; + + // Fill the specific concrete node information (model instance) + toJsonInternal( model ); + data.emplace( "model", model ); + + // store the supplemental information related to application/gui/... + for ( auto& [key, value] : m_extraJsonData.items() ) { + if ( key != "instance" && key != "model" ) { data.emplace( key, value ); } + } +} + +void Node::addJsonMetaData( const nlohmann::json& data ) { + for ( auto& [key, value] : data.items() ) { + m_extraJsonData[key] = value; + } +} + +Node::IndexAndPortRawPtr +Node::getPortByName( const std::string& type, const std::string& name ) const { + const auto& ports = ( type == "in" ) ? m_inputs : m_outputs; + return getPortByName( ports, name ); +} + +Node::IndexAndPortRawPtr +Node::getInputByName( const std::string& name ) const { + return getPortByName( m_inputs, name ); +} + +Node::IndexAndPortRawPtr +Node::getOutputByName( const std::string& name ) const { + return getPortByName( m_outputs, name ); +} + +Node::IndexAndPortRawPtr +Node::getPortByName( const PortCollection& ports, const std::string& name ) const { + auto itp = std::find_if( + ports.begin(), ports.end(), [n = name]( const auto& p ) { return p->getName() == n; } ); + PortBase* fprt { nullptr }; + PortIndex portIndex; + if ( itp != ports.cend() ) { + fprt = itp->get(); + portIndex = std::distance( ports.begin(), itp ); + } + return { portIndex, fprt }; +} + +PortBase* Node::getPortByIndex( const std::string& type, PortIndex idx ) const { + const auto& ports = ( type == "in" ) ? m_inputs : m_outputs; + return getPortBase( ports, idx ); +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Node.hpp b/src/Dataflow/Core/Node.hpp new file mode 100644 index 00000000000..bc692cff1a9 --- /dev/null +++ b/src/Dataflow/Core/Node.hpp @@ -0,0 +1,506 @@ +#pragma once +#include "Core/Utils/Index.hpp" +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/** \brief Base abstract class for all the nodes added and used by the node system. + * A node represent a function acting on some input data and generating some outputs. + * To build a computation graph, nodes should be added to the graph, which is itself a node + * (\see Ra::Dataflow::Core::DataflowGraph) and linked together through their input/output port. + * + * Nodes computes their function using the input data collecting from the input ports, + * in an evaluation context (possibly empty) defined by their internal data to generate results + * sent to their output ports. + * + */ +class RA_DATAFLOW_API Node +{ + public: + using PortIndex = Ra::Core::Utils::Index; + template + using PortPtr = std::shared_ptr; + template + using PortRawPtr = typename PortPtr::element_type*; + template + using PortInPtr = PortPtr>; + template + using PortInRawPtr = typename PortInPtr::element_type*; + template + using PortOutPtr = PortPtr>; + template + using PortOutRawPtr = typename PortOutPtr::element_type*; + + using PortBasePtr = PortPtr; + using PortBaseRawPtr = typename PortPtr::element_type*; + + using PortCollection = std::vector>; + + template + using IndexAndPortRawPtr = std::pair; + + /// \name Constructors + /// @{ + /// \brief delete default constructors. + /// \see https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-copy-virtual + Node() = delete; + Node( const Node& ) = delete; + Node& operator=( const Node& ) = delete; + /// @} + + /// \brief make Node a base abstract class + virtual ~Node() = default; + + /// \brief Two nodes are considered equal if there type and instance names are the same. + bool operator==( const Node& o_node ); + + /// \name Function execution control + /// @{ + /// \brief Initializes the node content + /// The init() function should be called once at the beginning of the lifetime of the node by + /// the owner of the node (the graph which contains the node). + /// Its goal is to initialize the node's internal data if any. + /// The base version do nothing. + virtual void init(); + + /// \brief Compile the node to check its validity + /// Only nodes defining a full computation graph will need to override this method. + /// The base version do nothing. + /// \return the compilation status + virtual bool compile(); + + /// \brief Executes the node. + /// Execute the node function on the input ports (to be fetched) and write the results to the + /// output ports. + /// \return the execution status. + virtual bool execute() = 0; + + /// \brief delete the node content + /// The destroy() function is called once at the end of the lifetime of the node. + /// Its goal is to free the internal data that have been allocated. + virtual void destroy(); + /// @} + + /// \name Control the interfaces of the nodes (inputs, outputs, internal data, ...) + /// @{ + + /// \brief Get an input port by its name + /// \param type either "in" or "out", the directional type of the port + /// \param name + /// \return an alias pointer on the requested port if it exists, nullptr else + IndexAndPortRawPtr getPortByName( const std::string& type, + const std::string& name ) const; + IndexAndPortRawPtr getInputByName( const std::string& name ) const; + IndexAndPortRawPtr getOutputByName( const std::string& name ) const; + + /// \brief Get an input port by its index + /// \param type either "in" or "out", the directional type of the port + /// \param idx + /// \return an alias pointer on the requested port if it exists, nullptr else + PortBaseRawPtr getPortByIndex( const std::string& type, PortIndex idx ) const; + PortBaseRawPtr getInputByIndex( PortIndex idx ) const { return getPortBase( m_inputs, idx ); } + PortBaseRawPtr getOutputByIndex( PortIndex idx ) const { return getPortBase( m_outputs, idx ); } + + template + PortInRawPtr getInputByIndex( PortIndex idx ) const { + return getPort>( m_inputs, idx ); + } + + template + PortOutRawPtr getOutputByIndex( PortIndex idx ) const { + return getPort>( m_outputs, idx ); + } + + /// \brief Gets the in ports of the node. + /// Input ports are own to the node. + const PortCollection& getInputs() const; + + /// \brief Gets the out ports of the node. + /// Output ports are own to the node. + const PortCollection& getOutputs() const; + + /// \brief Build the interface ports of the node + /// Derived node can override the default implementation that build an interface port for each + /// input or output port (e.g. if several inputs have the same type T, make an interface that is + /// a vector of T*) + virtual const std::vector& buildInterfaces( Node* parent ); + + /// \brief Get the interface ports of the node + const std::vector& getInterfaces() const; + + /// \brief Gets the editable parameters of the node. + /// used only by the node editor gui to build the editon widget + const std::vector>& getEditableParameters(); + /// @} + + /// \name Identification methods + /// @{ + /// \brief Gets the type name of the node. + const std::string& getTypeName() const; + + /// \brief Gets the instance name of the node. + const std::string& getInstanceName() const; + + /// \brief Sets the instance name (rename) the node + void setInstanceName( const std::string& newName ); + + /// @} + + /// \name Serialization of a node + /// @{ + /// \todo : specify the json format for nodes and what is expected from the following methods + + /// \brief serialize the content of the node. + /// Fill the given json object with the json representation of the concrete node. + void toJson( nlohmann::json& data ) const; + + /// \brief unserialized the content of the node. + /// Fill the node from its json representation + bool fromJson( const nlohmann::json& data ); + + /// \brief Add a metadata to the node to store application specific information. + /// used, e.g. by the node editor gui to save node position in the graphical canvas. + void addJsonMetaData( const nlohmann::json& data ); + + /// \brief Give access to extra json data stored on the node. + const nlohmann::json& getJsonMetaData(); + /// @} + + /// \brief Returns the demangled type name of the node or any human readable representation of + /// the type name. + /// This is a public static member each node must define to be serializable + static const std::string& getTypename(); + + inline bool isInitialized() const { return m_initialized; } + + protected: + /// Construct the base node given its name and type + /// \param instanceName The name of the node + /// \param typeName The type name of the node + Node( const std::string& instanceName, const std::string& typeName ); + + IndexAndPortRawPtr getPortByName( const PortCollection& ports, + const std::string& name ) const; + + PortBaseRawPtr getPortBase( const PortCollection& ports, PortIndex idx ) const { + if ( 0 <= idx && size_t( idx ) < ports.size() ) { return ports[idx].get(); } + return nullptr; + } + + PortBaseRawPtr getPortBaseNoCheck( const PortCollection& ports, PortIndex idx ) const { + return ports[idx].get(); + } + + template + T getPort( const PortCollection& ports, PortIndex idx ) const { + return static_cast( getPortBase( ports, idx ) ); + } + + /// internal json representation of the Node. + /// Default implementation warn about unsupported deserialization. + /// Effective deserialzation must be implemented by inheriting classes. + /// Be careful with template specialization and function member overriding in derived classes. + virtual bool fromJsonInternal( const nlohmann::json& ) { + LOG( Ra::Core::Utils::logDEBUG ) << "Unable deserializing " << getTypeName() << "."; + return true; + } + + /// internal json representation of the Node. + /// Default implementation warn about unsupported serialization. + /// Effective serialzation must be implemented by inheriting classes. + /// Be careful with template specialization and function member overriding in derived classes. + virtual void toJsonInternal( nlohmann::json& data ) const { + data["comment"] = std::string { "Could not serialized " } + getTypeName(); + LOG( Ra::Core::Utils::logDEBUG ) + << "Unable to save data when serializing a " << getTypeName() << "."; + } + + /// Adds an in port to the node. + /// This function checks if the port is an input port. + /// \param in The in port to add. + bool addInput( PortBase* in ); + PortIndex addInput( PortBasePtr in ); + PortIndex addOutput( PortBasePtr out ); + PortIndex addPort( PortCollection&, std::shared_ptr in ); + + template + PortInPtr addInputPort( U&&... u ) { + auto idx = addInput( std::make_shared>( this, std::forward( u )... ) ); + return getInputPort( idx ); + } + template + PortOutPtr addOutputPort( U&&... u ) { + auto idx = addOutput( std::make_shared>( this, std::forward( u )... ) ); + return getOutputPort( idx ); + } + + template + PortInPtr getInputPort( PortIndex idx ) { + return std::static_pointer_cast>( m_inputs[idx] ); + } + + template + PortOutPtr getOutputPort( PortIndex idx ) { + return std::static_pointer_cast>( m_outputs[idx] ); + } + + PortBasePtr getInputPort( PortIndex idx ) { return m_inputs[idx]; } + PortBasePtr getOutputPort( PortIndex idx ) { return m_outputs[idx]; } + + ///\todo remove these it not needed by dataflow graph + /// \brief remove the given input port from the managed input ports + /// \param in the port to remove + /// \return true if the port was removed (the in pointer is the set to nullptr), false else + bool removeInput( PortBase*& in ); + void removeInput( PortIndex idx ) { m_inputs[idx].reset(); } + + /// Adds an out port to the node and the data associated with it. + /// This function checks if there is no out port with the same name already associated with this + /// node. + /// \param out The in port to add. + /// \param data The data associated with the port. + template + void addOutput( PortOut* out, T* data ); + + /// \brief remove the given output port from the managed input ports + /// \param out the port to remove + /// \return true if the port was removed (the out pointer is the set to nullptr), false else + bool removeOutput( PortBase*& out ); + void removeOutput( PortIndex idx ) { m_outputs[idx].reset(); } + + /// \brief Adds an editable parameter to the node if it does not already exist. + /// \note the node will take ownership of the editable object. + /// \param editableParameter The editable parameter to add. + bool addEditableParameter( EditableParameterBase* editableParameter ); + + /// Remove an editable parameter to the node if it does exist. + /// \param name The name of the editable parameter to remove. + /// \return true if the editable parameter is found and removed. + bool removeEditableParameter( const std::string& name ); + + /// \brief get a typed reference to the editable parameter. + /// \tparam E The type of the expected editable parameter. + /// \param name The name of the editable parameter to get. + /// \return the pointer to the editable parameter if any, nullptr if not. + template + EditableParameter* getEditableParameter( const std::string& name ); + + /// \brief Flag that checks if the node is already initialized + bool m_initialized { false }; + /// The type name of the node. Initialized once at construction + std::string m_typeName; + /// The instance name of the node + std::string m_instanceName; + /// The in ports of the node (own by the node) + std::vector m_inputs; + /// The out ports of the node (own by the node) + std::vector> m_outputs; + /// The reflected ports of the node if it is only a source or sink node. + /// This stores only aliases as interface ports will belong to the parent + /// node (i.e. the graph this node belongs to) + std::vector m_interface; + + /// The editable parameters of the node + /// \todo replace this by a Ra::Core::VariableSet + std::vector> m_editableParameters; + + /// Additional data on the node, added by application or gui or ... + nlohmann::json m_extraJsonData; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline void Node::init() { + m_initialized = true; +} + +inline void Node::destroy() { + m_interface.clear(); + m_inputs.clear(); + m_outputs.clear(); + m_editableParameters.clear(); +} + +inline const nlohmann::json& Node::getJsonMetaData() { + return m_extraJsonData; +} + +inline const std::string& Node::getTypeName() const { + return m_typeName; +} + +inline const std::string& Node::getInstanceName() const { + return m_instanceName; +} + +inline void Node::setInstanceName( const std::string& newName ) { + m_instanceName = newName; +} + +inline const Node::PortCollection& Node::getInputs() const { + return m_inputs; +} + +inline const Node::PortCollection& Node::getOutputs() const { + return m_outputs; +} + +inline const std::vector& Node::buildInterfaces( Node* parent ) { + m_interface.clear(); + m_interface.shrink_to_fit(); + const std::vector>* readFrom = + m_inputs.empty() ? &m_outputs : &m_inputs; + m_interface.reserve( readFrom->size() ); + for ( const auto& p : *readFrom ) { + m_interface.emplace_back( p->reflect( parent, getInstanceName() + '_' + p->getName() ) ); + } + return m_interface; +} + +inline const std::vector& Node::getInterfaces() const { + return m_interface; +} + +inline const std::vector>& Node::getEditableParameters() { + return m_editableParameters; +} + +inline bool Node::operator==( const Node& o_node ) { + return ( m_typeName == o_node.getTypeName() ) && ( m_instanceName == o_node.getInstanceName() ); +} + +inline bool Node::addInput( PortBase* in ) { + if ( !in->is_input() ) { return false; } + bool found = false; + for ( auto& input : m_inputs ) { + if ( input->getName() == in->getName() ) { found = true; } + } + if ( !found ) { m_inputs.emplace_back( in ); } + return !found; +} + +inline Node::PortIndex Node::addPort( PortCollection& ports, std::shared_ptr port ) { + PortIndex idx; + // look for a free slot + auto it = std::find_if( ports.begin(), ports.end(), []( const auto& port ) { return !port; } ); + if ( it != ports.end() ) { + it->swap( port ); + idx = std::distance( ports.begin(), it ); + } + else { + ports.push_back( std::move( port ) ); + idx = ports.size() - 1; + } + return idx; +} + +inline Node::PortIndex Node::addInput( std::shared_ptr in ) { + CORE_ASSERT( in->is_input(), "in port must be is_input" ); + return addPort( m_inputs, std::move( in ) ); +} + +inline Node::PortIndex Node::addOutput( std::shared_ptr out ) { + CORE_ASSERT( !out->is_input(), "out port must be not is_input" ); + return addPort( m_outputs, std::move( out ) ); +} + +inline bool Node::removeInput( PortBase*& in ) { + auto itP = std::find_if( + m_inputs.begin(), m_inputs.end(), [in]( const auto& p ) { return p.get() == in; } ); + if ( itP != m_inputs.end() ) { + m_inputs.erase( itP ); + in = nullptr; + return true; + } + return false; +} + +template +void Node::addOutput( PortOut* out, T* data ) { + bool found = false; + for ( auto& output : m_outputs ) { + if ( output->getName() == out->getName() ) { found = true; } + } + if ( !found ) { + m_outputs.emplace_back( out ); + out->setData( data ); + } +} + +inline bool Node::removeOutput( PortBase*& out ) { + auto outP = std::find_if( + m_outputs.begin(), m_outputs.end(), [out]( const auto& p ) { return p.get() == out; } ); + if ( outP != m_outputs.end() ) { + m_outputs.erase( outP ); + out = nullptr; + return true; + } + return false; +} + +inline bool Node::addEditableParameter( EditableParameterBase* editableParameter ) { + auto edIt = std::find_if( + m_editableParameters.begin(), + m_editableParameters.end(), + [name = editableParameter->getName()]( const auto& p ) { return p->getName() == name; } ); + if ( edIt == m_editableParameters.end() ) { + m_editableParameters.emplace_back( editableParameter ); + } + return edIt == m_editableParameters.end(); +} + +inline bool Node::removeEditableParameter( const std::string& name ) { + bool found = false; + auto it = m_editableParameters.begin(); + while ( it != m_editableParameters.end() ) { + if ( ( *it ).get()->getName() == name ) { + m_editableParameters.erase( it ); + found = true; + break; + } + ++it; + } + return found; +} + +template +inline EditableParameter* Node::getEditableParameter( const std::string& name ) { + auto it = m_editableParameters.begin(); + while ( it != m_editableParameters.end() ) { + if ( ( *it ).get()->getName() == name ) { + auto p = dynamic_cast*>( ( *it ).get() ); + if ( p != nullptr ) { return *p; } + } + ++it; + } + return nullptr; +} + +inline const std::string& Node::getTypename() { + static std::string demangledTypeName { "Abstract Node" }; + return demangledTypeName; +} + +inline bool Node::compile() { + return true; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/NodeFactory.cpp b/src/Dataflow/Core/NodeFactory.cpp new file mode 100644 index 00000000000..5c54d4c5508 --- /dev/null +++ b/src/Dataflow/Core/NodeFactory.cpp @@ -0,0 +1,104 @@ +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { +const std::string dataFlowBuiltInsFactoryName { "DataFlowBuiltIns" }; +} + +NodeFactory::NodeFactory( std::string name ) : m_name( std::move( name ) ) {} + +auto NodeFactory::getName() const -> std::string { + return m_name; +} + +auto NodeFactory::createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph ) -> std::shared_ptr { + if ( auto itr = m_nodesCreators.find( nodeType ); itr != m_nodesCreators.end() ) { + auto node = std::shared_ptr { itr->second.first( data ) }; + if ( owningGraph != nullptr ) { owningGraph->addNode( node ); } + return node; + } + return nullptr; +} + +auto NodeFactory::registerNodeCreator( const std::string& nodeType, + NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory ) -> bool { + + if ( auto itr = m_nodesCreators.find( nodeType ); itr == m_nodesCreators.end() ) { + m_nodesCreators[nodeType] = { std::move( nodeCreator ), nodeCategory }; + return true; + } + LOG( Ra::Core::Utils::logWARNING ) + << "NodeFactory (" << getName() + << ") : trying to add an already existing node creator for type " << nodeType << "."; + return false; +} + +auto NodeFactory::nextNodeId() -> size_t { + return ++m_nodesCreated; +} + +auto NodeFactorySet::createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph ) -> std::shared_ptr { + for ( const auto& itr : m_factories ) { + if ( auto node = itr.second->createNode( nodeType, data, owningGraph ); node ) { + return node; + } + } + LOG( Ra::Core::Utils::logERROR ) << "NodeFactorySet: unable to find constructor for " + << nodeType << " in any managed factory."; + return nullptr; +} + +namespace NodeFactoriesManager { + +/** + * \brief Allow static initialization without init order problems + * \return The manager singleton + */ +auto getFactoryManager() -> NodeFactorySet& { + static NodeFactorySet s_factoryManager {}; + return s_factoryManager; +} + +auto registerFactory( NodeFactorySet::mapped_type factory ) -> bool { + return getFactoryManager().addFactory( std::move( factory ) ); +} + +auto createFactory( const NodeFactorySet::key_type& name ) -> NodeFactorySet::mapped_type { + auto factory = getFactory( name ); + if ( factory == nullptr ) { + factory = std::make_shared( name ); + registerFactory( factory ); + } + return factory; +} + +auto getFactory( const NodeFactorySet::key_type& name ) -> NodeFactorySet::mapped_type { + auto& fctMgr = getFactoryManager(); + if ( auto factory = fctMgr.find( name ); factory != fctMgr.end() ) { return factory->second; } + return nullptr; +} + +auto unregisterFactory( const NodeFactorySet::key_type& name ) -> bool { + return getFactoryManager().removeFactory( name ); +} + +auto getDataFlowBuiltInsFactory() -> NodeFactorySet::mapped_type { + return getFactory( NodeFactoriesManager::dataFlowBuiltInsFactoryName ); +} + +} // namespace NodeFactoriesManager + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/NodeFactory.hpp b/src/Dataflow/Core/NodeFactory.hpp new file mode 100644 index 00000000000..fce92df0cbd --- /dev/null +++ b/src/Dataflow/Core/NodeFactory.hpp @@ -0,0 +1,321 @@ +#pragma once +#include + +#include + +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +class DataflowGraph; + +/** + * NodeFactory store a set of functions allowing to dynamically create dataflow nodes. + * A NodeFactory is used when loading a node graph from a json representation of the graph to + * instantiate all the loaded nodes. + * Each DataflowGraph must have a reference to the nodeFactory to be used to create all the node he + * has. + * + */ +class RA_DATAFLOW_API NodeFactory +{ + public: + /** Creates an empty factory with the given name */ + explicit NodeFactory( std::string name ); + + [[nodiscard]] auto getName() const -> std::string; + + /** Function that creates and initialize a node. + * Typical implementation of such a function should do the following : + * { + * auto node = new ConcreteNodeType( unique_instance_name ); + * node->fromJson( data); + * return node; + * } + */ + using NodeCreatorFunctor = std::function( const nlohmann::json& data )>; + + /** + * Associate, for a given concrete node type, a custom NodeCreatorFunctor + * @tparam T Concrete node type identifier + * \param nodeCreator Functor to create an node of the corresponding concrete node type. + * \param nodeCategory Category of the node. + * \return true if the node creator is successfully added, false if not (e.g. due to a name + * collision). + */ + template + auto registerNodeCreator( NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory = "RadiumNodes" ) -> bool; + + /** + * Associate, for a given concrete node type, a generic NodeCreatorFunctor + * @tparam T Concrete node type identifier + * \param instanceNamePrefix prefix of the node instance name (will be called "prefix_i" with i + * a unique number. + * \param nodeCategory Category of the node. + * \return true if the node creator is successfully added, false if not (e.g. due to a name + * collision). + */ + template + auto registerNodeCreator( const std::string& instanceNamePrefix, + const std::string& nodeCategory = "RadiumNodes" ) -> bool; + /** + * Associate, for a given concrete node type name, a NodeCreatorFunctor + * \param nodeType the name of the concrete type + * (the same as what is obtained by T::getTypename() on a node of type T) + * \param nodeCreator Functor to create an node of the corresponding concrete node type. + * \return true if the node creator is successfully added, false if not (e.g. due to a name + * collision). + */ + auto registerNodeCreator( const std::string& nodeType, + NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory = "RadiumNodes" ) -> bool; + + /** + * Get an unique, increasing node id. + * \return + */ + auto nextNodeId() -> size_t; + + /** Create a node of the requested type. + * The node is filled with the given json content. + * If owningGraph is non null, the node is added to this graph + * \param nodeType Type name of the node to be created. + * \param data json representation of the node data (might be empty) + * \param owningGraph if non null, the node is added to ths graph + * \return the new node. Ownership of the returned pointer is left to the caller. + */ + [[nodiscard]] auto createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph = nullptr ) -> std::shared_ptr; + + /** + * The type of the associative container used to store the factory + * this container associate a concrete node type name to a pair + * + * The name of the node category is helpful for graphical NodeGraph editor. + * By default this is set to be "RadiumNodes" + */ + using ContainerType = + std::unordered_map>; + /** + * Get a const reference on the associative map + * \return + */ + [[nodiscard]] auto getFactoryMap() const -> const ContainerType&; + + private: + ContainerType m_nodesCreators; + size_t m_nodesCreated { 0 }; + std::string m_name; +}; + +/** + * NodeFactorySet store a set of NodeFactory + */ +class RA_DATAFLOW_API NodeFactorySet +{ + public: + using container_type = std::map>; + + using key_type = container_type::key_type; + using mapped_type = container_type::mapped_type; + using value_type = container_type::value_type; + + using const_iterator = container_type::const_iterator; + using iterator = container_type::iterator; + + /** + * Add a factory to the set of factories available + * \param factory the factory + * \return true if the factory was inserted, false if the insertion was prevented by an + * already existing factory with the same name. + */ + auto addFactory( mapped_type factory ) -> bool; + + /** + * \brief Test if a factory exists in the set with the given name + * \param name The name of the factory to search for + * \return an optional that is empty (evaluates to false) if no factory exists with the given + * name or that contains the existing factory. + */ + auto hasFactory( const key_type& name ) -> Ra::Core::Utils::optional; + + /** + * \brief Remove the identified factory from the set + * \param name the name of the factory to remove + * \return true if the factory was removed, false if the factory does not exist in the set. + */ + auto removeFactory( const key_type& name ) -> bool; + + /** + * + * \return the created node, nullptr if there is no construction functor registered for the + * type. + */ + /** + * \brief Create a node using one of the functor (if it exists) registered in one factory for + * the given type name. \param nodeType name of the node type (as simplified by Radium + * demangler) to create \param data json data to fill the created node \param owningGraph Graph + * in which the node should be added, if not nullptr. \return The created node, nullptr in case + * of failure + */ + [[nodiscard]] auto createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph = nullptr ) -> std::shared_ptr; + + /* Wrappers to the interface of the underlying container + * see https://en.cppreference.com/w/cpp/container/map + */ + auto begin() const -> const_iterator; + auto end() const -> const_iterator; + auto cbegin() const -> const_iterator; + auto cend() const -> const_iterator; + auto find( const key_type& key ) const -> const_iterator; + auto insert( value_type value ) -> std::pair; + auto erase( const key_type& key ) -> size_t; + + private: + container_type m_factories; +}; + +/** + * Implement a NodeFactoryManager that stores a set of factories available to the system. + * Such a manager will be populated with Core::Dataflow node factories (Specialized sources, + * specialized sink, ...) and will allow users to register its own factories. + * + * When creating a graph, the set of needed factories should be given as a constructor parameter + * or built by adding factories identifier to the graph. + * + * When a graph is saved, the name of the factories he needs will be exported as string array + * in the json. + * + * When a graph is loaded, the set of factories is built using the factories array in the json. + * + * @note: the factory name "DataFlowBuiltIns" is reserved and correspond to the base nodes available + * for each dataflow graph (Specialized sources, specialized sink, ...). This factory will be + * automatically added to all created factory set. + */ +namespace NodeFactoriesManager { +/** Names of the system Builtins factories (automatically added to each graph) */ +extern const std::string dataFlowBuiltInsFactoryName; + +RA_DATAFLOW_API auto getFactoryManager() -> NodeFactorySet&; + +/** Register a factory into the manager. + * The key will be fetched from the factory (its name) + */ +/** + * \brief Register a factory into the manager. + * The key will be fetched from the factory (its name) + * \param factory + * \return true if the factory was registered, false if not (e.g. due to name collision). + */ +RA_DATAFLOW_API auto registerFactory( NodeFactorySet::mapped_type factory ) -> bool; + +/** + * \brief Create and register a factory to the manager. + * \param name The name of the factory to create + * \return a configurable factory. + */ +RA_DATAFLOW_API auto createFactory( const NodeFactorySet::key_type& name ) + -> NodeFactorySet::mapped_type; + +/** + * \brief Gets the given factory from the manager + * \param name The name of the factory to get + * \return a shared_ptr to the requested factory, nullptr if the factory does not exist. + */ +RA_DATAFLOW_API auto getFactory( const NodeFactorySet::key_type& name ) + -> NodeFactorySet::mapped_type; + +/** + * \brief Unregister the factory from the manager + * \param name The name of the factory to unregister + * \return true if the factory was unregistered, false if not (e.g. for names not being managed). + */ +RA_DATAFLOW_API auto unregisterFactory( const NodeFactorySet::key_type& name ) -> bool; + +/** + * \brief Gets the factory for nodes exported by the Core dataflow library. + * \return + */ +RA_DATAFLOW_API auto getDataFlowBuiltInsFactory() -> NodeFactorySet::mapped_type; +} // namespace NodeFactoriesManager + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +auto NodeFactory::registerNodeCreator( NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory ) -> bool { + return registerNodeCreator( T::getTypename(), std::move( nodeCreator ), nodeCategory ); +} + +template +auto NodeFactory::registerNodeCreator( const std::string& instanceNamePrefix, + const std::string& nodeCategory ) -> bool { + return registerNodeCreator( + T::getTypename(), + [this, instanceNamePrefix]( const nlohmann::json& data ) { + std::string instanceName; + if ( data.contains( "instance" ) ) { instanceName = data["instance"]; } + else { instanceName = instanceNamePrefix + std::to_string( this->nextNodeId() ); } + auto node = std::make_shared( instanceName ); + node->fromJson( data ); + return node; + }, + nodeCategory ); +} + +inline auto NodeFactory::getFactoryMap() const -> const NodeFactory::ContainerType& { + return m_nodesCreators; +} + +inline auto NodeFactorySet::addFactory( NodeFactorySet::mapped_type factory ) -> bool { + const auto [loc, inserted] = insert( { factory->getName(), std::move( factory ) } ); + return inserted; +} + +inline auto NodeFactorySet::hasFactory( const NodeFactorySet::key_type& name ) + -> Ra::Core::Utils::optional { + if ( auto fct = m_factories.find( name ); fct != m_factories.end() ) { return fct->second; } + return {}; +} + +inline auto NodeFactorySet::removeFactory( const NodeFactorySet::key_type& name ) -> bool { + return erase( name ); +} +inline auto NodeFactorySet::begin() const -> NodeFactorySet::const_iterator { + return m_factories.begin(); +} +inline auto NodeFactorySet::end() const -> NodeFactorySet::const_iterator { + return m_factories.end(); +} +inline auto NodeFactorySet::cbegin() const -> NodeFactorySet::const_iterator { + return m_factories.cbegin(); +} +inline auto NodeFactorySet::cend() const -> NodeFactorySet::const_iterator { + return m_factories.cend(); +} +inline auto NodeFactorySet::find( const NodeFactorySet::key_type& key ) const + -> NodeFactorySet::const_iterator { + return m_factories.find( key ); +} +inline auto NodeFactorySet::insert( NodeFactorySet::value_type value ) + -> std::pair { + return m_factories.insert( std::move( value ) ); +} +inline auto NodeFactorySet::erase( const NodeFactorySet::key_type& key ) -> size_t { + return m_factories.erase( key ); +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.cpp b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.cpp new file mode 100644 index 00000000000..cd7a666cc71 --- /dev/null +++ b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.cpp @@ -0,0 +1,36 @@ +#include +DATAFLOW_LIBRARY_INITIALIZER_DECL( CoreNodes ); + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +void registerStandardFactories() { + if ( getFactory( NodeFactoriesManager::dataFlowBuiltInsFactoryName ) ) { return; } + auto coreFactory = createFactory( NodeFactoriesManager::dataFlowBuiltInsFactoryName ); + /* --- Sources --- */ + Private::registerSourcesFactories( coreFactory ); + + /* --- Sinks --- */ + Private::registerSinksFactories( coreFactory ); + + /* --- Functionals */ + Private::registerFunctionalsFactories( coreFactory ); + + /* --- Graphs --- */ + coreFactory->registerNodeCreator( DataflowGraph::getTypename() + "_", "Graph" ); +} +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra + +DATAFLOW_LIBRARY_INITIALIZER_IMPL( CoreNodes ) { + Ra::Dataflow::Core::NodeFactoriesManager::registerStandardFactories(); +} diff --git a/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.hpp b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.hpp new file mode 100644 index 00000000000..c8fd82d811c --- /dev/null +++ b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.hpp @@ -0,0 +1,38 @@ +#pragma once +#include + +#include +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { + +/** + * \brief Create the node system default factory. + * + * The default factory of the node system contains instantiation of the nodes below for the + * following type + * - Scalar, float, double int, unsigned int + * - Color, VectorDf, VectorDd, VectorDi, VectorDi (where D in {2, 3, 4} + * + * List of instanced nodes for any TYPE above + * - SingleDataSourceNode and SingleDataSourceNode> + * - FunctionSourceNode, FunctionSourceNode + * - FunctionSourceNode, FunctionSourceNode + * - SinkNode, SinkNode + * - FilterNode> + * - TransformNode>, ReduceNode> + * - BinaryOpNode, BinaryOpNode>, BinaryOpNode + * + * All these node might be serialized/unserialized without any additional nor custom factory. + * + * If needed, the definition of all these type aliases can be included using one of the headers + * - #include + * - #include + * - #include + */ +RA_DATAFLOW_API void registerStandardFactories(); +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/BinaryOpNode.hpp b/src/Dataflow/Core/Nodes/Functionals/BinaryOpNode.hpp new file mode 100644 index 00000000000..2e3323da326 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/BinaryOpNode.hpp @@ -0,0 +1,259 @@ +#pragma once +#include "Core/CoreMacros.hpp" +#include "Dataflow/Core/Port.hpp" +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** + * \brief namespace containing template utilities for management of BinaryOpNode content + */ +namespace internal { + +/** + * \brief Type traits giving access to value_type and const ref type + * \tparam A + * \tparam is_container + */ +template +struct ArgTypeHelperInternal { + using value_type = A; + using const_value_ref = const A&; +}; + +/** + * \brief Partial specialization for container type + * \tparam A + */ +template +struct ArgTypeHelperInternal { + using value_type = typename A::value_type; + using const_value_ref = const typename A::value_type&; +}; + +/** + * \brief CRTP + * \tparam A + */ +template +struct ArgTypeHelper : public ArgTypeHelperInternal::value> {}; + +// Find a way to evaluate the copy/move semantic of t_out +/** + * \brief Manage the call to y = f(a, b) according to inputs aand ouput types of the node + * \tparam t_a Type of the source data for argument a of the function + * \tparam t_b Type of the source data for argument b of the function + * \tparam t_out Type of the ouput data sent by the node + * \tparam funcType Profile of the operator f + * \tparam it_a true if t_a is a container + * \tparam it_b true if t_b is a container + * \tparam it_out true if t_out is a container + */ +template ::value, + bool it_b = Ra::Core::Utils::is_container::value, + bool it_out = Ra::Core::Utils::is_container::value> +struct ExecutorHelper { + static t_out executeInternal( t_a&, t_b&, funcType ) { + static_assert( ( ( it_a || it_b ) ? it_out : !it_out ), "Invalid template parameter " ); + } +}; + +/** + * \brief Call of an operator to transform two container into another container. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { + t_out res; + std::transform( a.begin(), + a.end(), + b.begin(), + std::back_inserter( res ), + [f]( typename ArgTypeHelper::const_value_ref x, + typename ArgTypeHelper::const_value_ref y ) -> + typename ArgTypeHelper::value_type { return f( x, y ); } ); + return res; + } +}; + +/** + * \brief Call of an operator to transform a container and a scalar into a container. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { + t_out res; + std::transform( a.begin(), + a.end(), + std::back_inserter( res ), + [&b, f]( typename ArgTypeHelper::const_value_ref x ) -> + typename ArgTypeHelper::value_type { return f( x, b ); } ); + return res; + } +}; + +/** + * \brief Call of an operator to transform a scalar and a container into a container. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { + t_out res; + std::transform( b.begin(), + b.end(), + std::back_inserter( res ), + [&a, f]( typename ArgTypeHelper::const_value_ref x ) -> + typename ArgTypeHelper::value_type { return f( a, x ); } ); + return res; + } +}; + +/** + * \brief Call of an operator to transform two scalars into a scalar. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { return f( a, b ); } +}; +} // namespace internal + +/** \brief Apply a binary operation on its input. + * \tparam t_a type of the first argument + * \tparam t_b type of the second argument + * \tparam t_out type of the result + * + * This node apply an operator f on its input such that : + * - if t_a, t_b and t_out are collections, r[i] = f(a[i], b[i]) for all elements i in the + * collections. + * - if t_a and t_out are collections, t_b an object, r[i] = f(a[i], b) for all elements i in the + * collections. + * - if t_b and t_out are collections, t_a an object, r[i] = f(a, b[i]) for all elements i in the + * collections. + * - if t_a, t_b and t_out are objects, r = f(a, b). + * All other configurations of t_a, t_b and t_out are illegal. + * + * This node has three inputs : + * - a : port accepting the input data of type t_a. Must be linked. + * - b : port accepting the input data of type t_b. Must be linked. + * - f : port accepting an operator with profile std::function. + * Link to this port is not mandatory, the operator might be set once for the node. + * If this port is linked, the operator will be taken from the port. + * + * This node has one output : + * - out : port giving a t_out such that out = std::transform(a, b, f) + */ +template +class BinaryOpNode : public Node +{ + public: + /** + * BinaryOp operator profile + */ + using Arg1_type = typename internal::ArgTypeHelper::const_value_ref; + using Arg2_type = typename internal::ArgTypeHelper::const_value_ref; + using Res_type = typename internal::ArgTypeHelper::value_type; + using BinaryOperator = std::function; + + using PortA = PortIn; + using PortB = PortIn; + using PortF = PortIn; + using PortR = PortOut; + /** + * \brief Construct an null operator + * \param instanceName + */ + explicit BinaryOpNode( const std::string& instanceName ) : + BinaryOpNode( instanceName, getTypename(), []( Arg1_type, Arg2_type ) -> Res_type { + return Res_type {}; + } ) {} + + /** + * \brief Construct a BinaryOpNode with the given operator + * \param instanceName + * \param op + */ + BinaryOpNode( const std::string& instanceName, BinaryOperator op ) : + BinaryOpNode( instanceName, getTypename(), op ) {} + + void init() override { + m_result = t_out {}; + Node::init(); + } + + bool execute() override { + auto f = m_operator; + + if ( m_portF->isLinked() ) { f = m_portF->getData(); } + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portA->isLinked() && m_portB->isLinked() ) { + m_result = internal::ExecutorHelper::executeInternal( + m_portA->getData(), m_portB->getData(), f ); + } + return true; + } + + /// \brief Sets the operator to be evaluated by the node. + void setOperator( BinaryOperator op ) { m_operator = op; } + + protected: + BinaryOpNode( const std::string& instanceName, + const std::string& typeName, + BinaryOperator op ) : + Node( instanceName, typeName ), + m_operator( op ), + m_portA { addInputPort( "a" ) }, + m_portB { addInputPort( "b" ) }, + m_portF { addInputPort( "f" ) }, + m_portR { addOutputPort( &m_result, "r" ) } + + { + m_portA->mustBeLinked(); + m_portB->mustBeLinked(); + } + + void toJsonInternal( nlohmann::json& data ) const override { Node::toJsonInternal( data ); } + + bool fromJsonInternal( const nlohmann::json& data ) override { + return Node::fromJsonInternal( data ); + } + + private: + /// \brief the used operator + BinaryOperator m_operator = []( Arg1_type, Arg2_type ) -> Res_type { return Res_type {}; }; + t_out m_result; + + /// @{ + /// Store pore index for direct access. + Node::PortInPtr m_portA {}; + Node::PortInPtr m_portB {}; + Node::PortInPtr m_portF {}; + Node::PortOutPtr m_portR {}; + + /// @} + + public: + static const std::string& getTypename() { + static std::string demangledName = + std::string { "BinaryOp<" } + Ra::Dataflow::Core::simplifiedDemangledType() + + " x " + Ra::Dataflow::Core::simplifiedDemangledType() + " -> " + + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; + } +}; + +// implementation of the methods are inlined +// see issue .inl coding style #1011 + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/CoreDataFunctionals.hpp b/src/Dataflow/Core/Nodes/Functionals/CoreDataFunctionals.hpp new file mode 100644 index 00000000000..f93ebceba64 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/CoreDataFunctionals.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include + +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +using namespace Ra::Core; + +// This macro does not end with semicolon. To be added when calling it +#define DECLARE_FUNCTIONALS( SUFFIX, TYPE ) \ + using ArrayFilter##SUFFIX = FilterNode>; \ + using ArrayTransformer##SUFFIX = TransformNode>; \ + using ArrayReducer##SUFFIX = ReduceNode>; \ + using BinaryOp##SUFFIX = BinaryOpNode; \ + using BinaryOp##SUFFIX##Array = BinaryOpNode>; \ + using BinaryPredicate##SUFFIX = BinaryOpNode + +/* Not yet supported + using BinaryPredicate##SUFFIX##Array = BinaryOpNode, + Ra::Core::VectorArray, bool> +*/ + +DECLARE_FUNCTIONALS( Float, float ); +DECLARE_FUNCTIONALS( Double, double ); +DECLARE_FUNCTIONALS( Scalar, Scalar ); +DECLARE_FUNCTIONALS( Int, int ); +DECLARE_FUNCTIONALS( UInt, unsigned int ); +DECLARE_FUNCTIONALS( Color, Utils::Color ); +DECLARE_FUNCTIONALS( Vector2f, Vector2f ); +DECLARE_FUNCTIONALS( Vector2d, Vector2d ); +DECLARE_FUNCTIONALS( Vector3f, Vector3f ); +DECLARE_FUNCTIONALS( Vector3d, Vector3d ); +DECLARE_FUNCTIONALS( Vector4f, Vector4f ); +DECLARE_FUNCTIONALS( Vector4d, Vector4d ); +DECLARE_FUNCTIONALS( Vector2i, Vector2i ); +DECLARE_FUNCTIONALS( Vector3i, Vector3i ); +DECLARE_FUNCTIONALS( Vector4i, Vector4i ); +DECLARE_FUNCTIONALS( Vector2ui, Vector2ui ); +DECLARE_FUNCTIONALS( Vector3ui, Vector3ui ); +DECLARE_FUNCTIONALS( Vector4ui, Vector4ui ); + +// TODO, instanciate nodes for otherypes ? + +#undef DECLARE_FUNCTIONALS +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/FilterNode.hpp b/src/Dataflow/Core/Nodes/Functionals/FilterNode.hpp new file mode 100644 index 00000000000..a5916fa0f24 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/FilterNode.hpp @@ -0,0 +1,141 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** \brief Filter on iterable collection. + * \tparam coll_t the collection to filter. Must respect the SequenceContainer requirements + * \tparam v_t (optional), type of the element in the collection. Default to coll_t::value_type + * \see https://en.cppreference.com/w/cpp/named_req/SequenceContainer + * + * This node apply an operator f on its input such that to keep only elements validated by a + * predicate : + * + * This node has two inputs : + * - in : port accepting the input data of type coll_t. Must be linked. + * - f : port accepting an operator with profile std::function. + * Link to this port is not mandatory, the operator might be set once for the node. + * If this port is linked, the operator will be taken from the port. + * + * This node has one output : + * - out : port giving a coll_t such that out = std::copy_if(a, f) + */ +template +class FilterNode : public Node +{ + public: + /** + * unaryPredicate Type + */ + using UnaryPredicate = std::function; + + /** + * \brief Construct a filter accepting all its input ( true() lambda ) + * \param instanceName + */ + explicit FilterNode( const std::string& instanceName ); + + /** + * \brief Construct a filter with the given predicate + * \param instanceName + * \param predicate + */ + FilterNode( const std::string& instanceName, UnaryPredicate predicate ); + + void init() override; + bool execute() override; + + /// Sets the filtering predicate on the node + void setFilterFunction( UnaryPredicate predicate ); + Node::PortInPtr getInPort() { return m_portIn; } + Node::PortInPtr getPredicatePort() { return m_portPredicate; } + Node::PortOutPtr getOutPort() { return m_portOut; } + + protected: + FilterNode( const std::string& instanceName, + const std::string& typeName, + UnaryPredicate predicate ); + + void toJsonInternal( nlohmann::json& data ) const override { Node::toJsonInternal( data ); } + bool fromJsonInternal( const nlohmann::json& data ) override { + return Node::fromJsonInternal( data ); + } + + private: + UnaryPredicate m_predicate; + coll_t m_elements; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + Node::PortInPtr m_portIn; + Node::PortInPtr m_portPredicate; + Node::PortOutPtr m_portOut; + /// @} + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +FilterNode::FilterNode( const std::string& instanceName ) : + FilterNode( instanceName, getTypename(), []( v_t ) { return true; } ) {} + +template +FilterNode::FilterNode( const std::string& instanceName, UnaryPredicate predicate ) : + FilterNode( instanceName, getTypename(), predicate ) {} + +template +void FilterNode::setFilterFunction( UnaryPredicate predicate ) { + m_predicate = predicate; +} + +template +void FilterNode::init() { + Node::init(); + m_elements.clear(); +} + +template +bool FilterNode::execute() { + auto f = m_portPredicate->isLinked() ? m_portPredicate->getData() : m_predicate; + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portIn->isLinked() ) { + const auto& inData = m_portIn->getData(); + m_elements.clear(); + // m_elements.reserve( inData.size() ); // --> this is not a requirement of + // SequenceContainer + std::copy_if( inData.begin(), inData.end(), std::back_inserter( m_elements ), f ); + } + return true; +} + +template +const std::string& FilterNode::getTypename() { + static std::string demangledName = + std::string { "Filter<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +template +FilterNode::FilterNode( const std::string& instanceName, + const std::string& typeName, + UnaryPredicate filterFunction ) : + Node( instanceName, typeName ), + m_predicate( filterFunction ), + m_portIn { addInputPort( "in" ) }, + m_portPredicate { addInputPort( "f" ) }, + m_portOut { addOutputPort( &m_elements, "out" ) } { + m_portIn->mustBeLinked(); +} + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/ReduceNode.hpp b/src/Dataflow/Core/Nodes/Functionals/ReduceNode.hpp new file mode 100644 index 00000000000..7267a450240 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/ReduceNode.hpp @@ -0,0 +1,158 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** \brief Reduce an iterable collection using a given operator + * \tparam coll_t the collection to reduce. Must respect the SequenceContainer requirements + * \tparam v_t (optional), type of the element in the collection. Default to coll_t::value_type + * \see https://en.cppreference.com/w/cpp/named_req/SequenceContainer + * + * This node has three inputs : + * - in : port accepting a coll_t data. Linking to this port is mandatory + * - f : port accepting an operator with profile std::function. + * Linking to this port is not mandatory, the operator might be set once for the node or is the + * default value for v_t. If this port is linked, the operator will be taken from the port. + * - init : port accepting a v_t to serve as initial value. + * Linking to this port is not mandatory. + * If this port is linked, the initial value will be taken from the port. + * + * This node has one output : + * - out : port giving a v_t such that out = std::reduce(in, f, init) (std::accumulate if less + * than C++17) + */ +// TODO, allow to specify the type of the reduced information. This will allow, e.g, given a +// collection of X, to compute a tuple containing mean and standard deviation of the collection +// as reduction (using online Welford algo). +template +class ReduceNode : public Node +{ + public: + /** + * Trasformation operator profile + */ + using ReduceOperator = std::function; + + /** + * \brief Construct an identity transformer + * \param instanceName + */ + explicit ReduceNode( const std::string& instanceName ); + + /** + * \brief Construct a transformer with the given operator + * \param instanceName + * \param op + * \param initialValue + */ + ReduceNode( const std::string& instanceName, ReduceOperator op, v_t initialValue = v_t {} ); + + void init() override; + bool execute() override; + + /// Sets the operator on the node + void setOperator( ReduceOperator op, v_t initialValue = v_t {} ); + + protected: + ReduceNode( const std::string& instanceName, + const std::string& typeName, + ReduceOperator op, + v_t initialValue ); + + void toJsonInternal( nlohmann::json& data ) const override { Node::toJsonInternal( data ); } + bool fromJsonInternal( const nlohmann::json& data ) override { + return Node::fromJsonInternal( data ); + } + + private: + ReduceOperator m_operator; + v_t m_result; + v_t m_init; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + Node::PortInPtr m_portIn { new PortIn( "in", this ) }; + Node::PortInPtr m_portF { new PortIn( "f", this ) }; + Node::PortInPtr m_portInit { new PortIn( "init", this ) }; + Node::PortOutPtr m_portOut { new PortOut( "out", this ) }; + /// @} + + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +ReduceNode::ReduceNode( const std::string& instanceName ) : + ReduceNode( + instanceName, + getTypename(), + []( const v_t& a, const v_t& ) -> v_t { return a; }, + v_t {} ) {} + +template +ReduceNode::ReduceNode( const std::string& instanceName, + ReduceOperator op, + v_t initialValue ) : + ReduceNode( instanceName, getTypename(), op, initialValue ) {} + +template +void ReduceNode::setOperator( ReduceOperator op, v_t initialValue ) { + m_operator = op; + m_init = initialValue; +} + +template +void ReduceNode::init() { + Node::init(); + m_result = m_init; +} + +template +bool ReduceNode::execute() { + auto f = m_portF->isLinked() ? m_portF->getData() : m_operator; + auto iv = m_portInit->isLinked() ? m_portInit->getData() : m_init; + m_result = iv; + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portIn->isLinked() ) { + const auto& inData = m_portIn->getData(); + m_result = std::accumulate( inData.begin(), inData.end(), iv, f ); + } + return true; +} + +template +const std::string& ReduceNode::getTypename() { + static std::string demangledName = + std::string { "Reduce<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +template +ReduceNode::ReduceNode( const std::string& instanceName, + const std::string& typeName, + ReduceOperator op, + v_t initialValue ) : + Node( instanceName, typeName ), + m_operator( op ), + m_init( initialValue ), + m_portIn { addInputPort( "in" ) }, + m_portF { addInputPort( "f" ) }, + m_portInit { addInputPort( "init" ) }, + m_portOut { addOutputPort( &m_result, "out" ) } + +{ + m_portIn->mustBeLinked(); +} + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/TransformNode.hpp b/src/Dataflow/Core/Nodes/Functionals/TransformNode.hpp new file mode 100644 index 00000000000..ef8f96416d7 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/TransformNode.hpp @@ -0,0 +1,135 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** \brief Transform an iterable collection + * \tparam coll_t the collection to transform. Must respect the SequenceContainer requirements + * \tparam v_t (optional), type of the element in the collection. Default to coll_t::value_type + * \see https://en.cppreference.com/w/cpp/named_req/SequenceContainer + * + * This node has two inputs : + * - in : port accepting a coll_t data. Linking to this port is mandatory + * - f : port accepting an operator with profile std::function. + * Linking to this port is not mandatory, the operator might be set once for the node. + * If this port is linked, the operator will be taken from the port. + * + * This node has one output : + * - out : port giving a coll_t such that out = std::transform(in, f) + */ +template +class TransformNode : public Node +{ + public: + /** + * Trasformation operator profile + */ + using TransformOperator = std::function; + + /** + * \brief Construct an identity transformer + * \param instanceName + */ + explicit TransformNode( const std::string& instanceName ); + + /** + * \brief Construct a transformer with the given operator + * \param instanceName + * \param filterFunction + */ + TransformNode( const std::string& instanceName, TransformOperator op ); + + void init() override; + bool execute() override; + + /// Sets the operator on the node + void setOperator( TransformOperator op ); + + protected: + TransformNode( const std::string& instanceName, + const std::string& typeName, + TransformOperator op ); + + void toJsonInternal( nlohmann::json& data ) const override { Node::toJsonInternal( data ); } + bool fromJsonInternal( const nlohmann::json& data ) override { + return Node::fromJsonInternal( data ); + } + + private: + TransformOperator m_operator; + coll_t m_elements; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + PortInPtr m_portIn; + PortInPtr m_portOperator; + PortOutPtr m_portOut; + /// @} + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +TransformNode::TransformNode( const std::string& instanceName ) : + TransformNode( instanceName, getTypename(), []( v_t ) { return v_t {}; } ) {} + +template +TransformNode::TransformNode( const std::string& instanceName, TransformOperator op ) : + TransformNode( instanceName, getTypename(), op ) {} + +template +void TransformNode::setOperator( TransformOperator op ) { + m_operator = op; +} + +template +void TransformNode::init() { + Node::init(); + m_elements.clear(); +} + +template +bool TransformNode::execute() { + auto f = m_portOperator->isLinked() ? m_portOperator->getData() : m_operator; + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portIn->isLinked() ) { + const auto& inData = m_portIn->getData(); + m_elements.clear(); + // m_elements.reserve( inData.size() ); // --> this is not a requirement of + // SequenceContainer + std::transform( inData.begin(), inData.end(), std::back_inserter( m_elements ), f ); + } + return true; +} + +template +const std::string& TransformNode::getTypename() { + static std::string demangledName = + std::string { "Transform<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +template +TransformNode::TransformNode( const std::string& instanceName, + const std::string& typeName, + TransformOperator op ) : + Node( instanceName, typeName ), + m_operator( op ), + m_portIn { addInputPort( "in" ) }, + m_portOperator { addInputPort( "f" ) }, + m_portOut { addOutputPort( &m_elements, "out" ) } { + m_portIn->mustBeLinked(); +} + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.cpp b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.cpp new file mode 100644 index 00000000000..f2b5bae2733 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.cpp @@ -0,0 +1,61 @@ +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +namespace Private { + +/** TODO : replace this by factory autoregistration at compile time */ +#define ADD_FUNCTIONALS_TO_FACTORY( FACTORY, NAMESPACE, SUFFIX ) \ + FACTORY->registerNodeCreator( \ + NAMESPACE::ArrayFilter##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::ArrayTransformer##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::ArrayReducer##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryOp##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryOp##SUFFIX##Array::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryPredicate##SUFFIX::getTypename() + "_", #NAMESPACE ) + +/* + * not yet supported + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryPredicate##SUFFIX##Array::getTypename() + "_", #NAMESPACE ) +*/ + +void registerFunctionalsFactories( NodeFactorySet::mapped_type factory ) { + /* --- Functionals */ +#ifdef CORE_USE_DOUBLE + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Float ); +#else + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Double ); +#endif + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Scalar ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Int ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, UInt ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Color ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2f ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2d ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3f ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3d ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4f ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4d ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2i ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2ui ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3i ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3ui ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4i ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4ui ); +} +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.hpp b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.hpp new file mode 100644 index 00000000000..0d02ecdb766 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { +namespace Private { + +void registerFunctionalsFactories( NodeFactorySet::mapped_type factory ); + +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.cpp b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.cpp new file mode 100644 index 00000000000..e1fa6713761 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.cpp @@ -0,0 +1,53 @@ +#include +#include +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +namespace Private { + +/** TODO : replace this by factory autoregistration at compile time */ + +#define ADD_SINKS_TO_FACTORY( FACTORY, NAMESPACE, PREFIX ) \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##Sink::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##ArraySink::getTypename() + "_", #NAMESPACE ) + +void registerSinksFactories( NodeFactorySet::mapped_type factory ) { + /* --- Sinks --- */ + // bool could not be declared as others, because of the specificity of std::vector that is + // not compatible with Ra::Core::VectorArray implementation see + // https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no + // Ra::Core::VectorArray of bool + factory->registerNodeCreator( Sinks::BooleanSink::getTypename() + "_", + "Sinks" ); +#ifdef CORE_USE_DOUBLE + ADD_SINKS_TO_FACTORY( factory, Sinks, Float ); +#else + ADD_SINKS_TO_FACTORY( factory, Sinks, Double ); +#endif + ADD_SINKS_TO_FACTORY( factory, Sinks, Scalar ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Int ); + ADD_SINKS_TO_FACTORY( factory, Sinks, UInt ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Color ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2f ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2d ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3f ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3d ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4f ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4d ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2i ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2ui ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3i ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3ui ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4i ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4ui ); +} +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.hpp b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.hpp new file mode 100644 index 00000000000..a048cb71cff --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { +namespace Private { + +void registerSinksFactories( NodeFactorySet::mapped_type factory ); + +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.cpp b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.cpp new file mode 100644 index 00000000000..ab9e2d50611 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.cpp @@ -0,0 +1,62 @@ +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +namespace Private { + +/** TODO : replace this by factory autoregistration at compile time */ +#define ADD_SOURCES_TO_FACTORY( FACTORY, NAMESPACE, PREFIX ) \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##Source::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##ArraySource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##UnaryFunctionSource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##BinaryFunctionSource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##UnaryPredicateSource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##BinaryPredicateSource::getTypename() + "_", #NAMESPACE ) + +void registerSourcesFactories( NodeFactorySet::mapped_type factory ) { + /* --- Sources --- */ + // bool could not be declared as others, because of the specificity of std::vector that is + // not compatible with Ra::Core::VectorArray implementation see + // https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no + // Ra::Core::VectorArray of bool + factory->registerNodeCreator( + Sources::BooleanSource::getTypename() + "_", "Sources" ); + // prevent Scalar type collision with float or double in factory +#ifdef CORE_USE_DOUBLE + ADD_SOURCES_TO_FACTORY( factory, Sources, Float ); +#else + ADD_SOURCES_TO_FACTORY( factory, Sources, Double ); +#endif + ADD_SOURCES_TO_FACTORY( factory, Sources, Scalar ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Int ); + ADD_SOURCES_TO_FACTORY( factory, Sources, UInt ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Color ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2f ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2d ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3f ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3d ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4f ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4d ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2i ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2ui ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3i ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3ui ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4i ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4ui ); +} +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.hpp b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.hpp new file mode 100644 index 00000000000..f40f2c50151 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { +namespace Private { + +void registerSourcesFactories( NodeFactorySet::mapped_type factory ); + +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sinks/CoreDataSinks.hpp b/src/Dataflow/Core/Nodes/Sinks/CoreDataSinks.hpp new file mode 100644 index 00000000000..854bc12e2f4 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sinks/CoreDataSinks.hpp @@ -0,0 +1,47 @@ +#pragma once +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sinks { + +using namespace Ra::Core; + +#define DECLARE_SINKS( PREFIX, TYPE ) \ + using PREFIX##Sink = SinkNode; \ + using PREFIX##ArraySink = SinkNode> + +// bool could not be declared as others, because of the specificity of std::vector that is not +// compatible with Ra::Core::VectorArray implementation see +// https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no +// Ra::Core::VectorArray of bool +using BooleanSink = SinkNode; +DECLARE_SINKS( Float, float ); +DECLARE_SINKS( Double, double ); +DECLARE_SINKS( Scalar, Scalar ); +DECLARE_SINKS( Int, int ); +DECLARE_SINKS( UInt, unsigned int ); +DECLARE_SINKS( Color, Utils::Color ); +DECLARE_SINKS( Vector2f, Vector2f ); +DECLARE_SINKS( Vector2d, Vector2d ); +DECLARE_SINKS( Vector3f, Vector3f ); +DECLARE_SINKS( Vector3d, Vector3d ); +DECLARE_SINKS( Vector4f, Vector4f ); +DECLARE_SINKS( Vector4d, Vector4d ); +DECLARE_SINKS( Vector2i, Vector2i ); +DECLARE_SINKS( Vector3i, Vector3i ); +DECLARE_SINKS( Vector4i, Vector4i ); +DECLARE_SINKS( Vector2ui, Vector2ui ); +DECLARE_SINKS( Vector3ui, Vector3ui ); +DECLARE_SINKS( Vector4ui, Vector4ui ); + +#undef DECLARE_SINKS +} // namespace Sinks +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sinks/SinkNode.hpp b/src/Dataflow/Core/Nodes/Sinks/SinkNode.hpp new file mode 100644 index 00000000000..7eae4e6f79e --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sinks/SinkNode.hpp @@ -0,0 +1,107 @@ +#pragma once +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sinks { + +/** + * \brief Base class for nodes that will store the result of a computation graph. + * @tparam T The type of the data to serve. + */ +template +class SinkNode : public Node +{ + protected: + SinkNode( const std::string& instanceName, const std::string& typeName ); + + public: + explicit SinkNode( const std::string& name ) : SinkNode( name, SinkNode::getTypename() ) {} + + /** + * \brief initialize the interface port data pointer + */ + void init() override; + bool execute() override; + + /** + * Get the delivered data + * @return a copy of the delivered data. + */ + T getData() const; + /** + * Get the delivered data + * @return a const ref to the delivered data. + */ + const T& getDataByRef() const; + + Node::PortInPtr getInPort() { return m_portIn; } + + protected: + /// \todo why are these empty ? + void toJsonInternal( nlohmann::json& ) const override {} + bool fromJsonInternal( const nlohmann::json& ) override { return true; } + + private: + /// \todo : allow user to specify where to store the data ? (i.e. make this a shared_ptr ?). + // If yes, add a method setDataStorage(std::shared_ptr v) + T m_data; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + Node::PortInPtr m_portIn; + /// @} + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +SinkNode::SinkNode( const std::string& instanceName, const std::string& typeName ) : + Node( instanceName, typeName ), m_portIn { addInputPort( "from" ) } { + m_portIn->mustBeLinked(); +} + +template +void SinkNode::init() { + // this should be done only once (or when the address of local data changes) + if ( m_interface[0] != nullptr ) { + auto interfacePort = static_cast*>( m_interface[0] ); + interfacePort->setData( &m_data ); + } + Node::init(); +} + +template +bool SinkNode::execute() { + if ( m_portIn->hasData() ) { + m_data = m_portIn->getData(); + return true; + } + return false; +} + +template +T SinkNode::getData() const { + return m_data; +} + +template +const T& SinkNode::getDataByRef() const { + return m_data; +} + +template +const std::string& SinkNode::getTypename() { + static std::string demangledName = + std::string { "Sink<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +} // namespace Sinks +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sources/CoreDataSources.hpp b/src/Dataflow/Core/Nodes/Sources/CoreDataSources.hpp new file mode 100644 index 00000000000..4beb4759449 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sources/CoreDataSources.hpp @@ -0,0 +1,184 @@ +#pragma once +#include +#include + +#include +#include +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sources { +// TODO unify editable and non editable data sources + +// declare synonyms for convenient sources +// This macro does not end with semicolon. To be added when calling it +#define DECLARE_COREDATA_SOURCES( PREFIX, TYPE ) \ + using PREFIX##Source = SingleDataSourceNode; \ + using PREFIX##ArraySource = SingleDataSourceNode>; \ + using PREFIX##UnaryFunctionSource = FunctionSourceNode; \ + using PREFIX##BinaryFunctionSource = FunctionSourceNode; \ + using PREFIX##UnaryPredicateSource = FunctionSourceNode; \ + using PREFIX##BinaryPredicateSource = FunctionSourceNode; + +using namespace Ra::Core; + +// bool could not be declared as others, because of the specificity of std::vector that is not +// compatible with Ra::Core::VectorArray implementation see +// https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no +// Ra::Core::VectorArray of bool +using BooleanSource = SingleDataSourceNode; +DECLARE_COREDATA_SOURCES( Float, float ) +DECLARE_COREDATA_SOURCES( Double, double ) +DECLARE_COREDATA_SOURCES( Scalar, Scalar ) +DECLARE_COREDATA_SOURCES( Int, int ) +DECLARE_COREDATA_SOURCES( UInt, unsigned int ) +DECLARE_COREDATA_SOURCES( Color, Utils::Color ) +DECLARE_COREDATA_SOURCES( Vector2f, Vector2f ) +DECLARE_COREDATA_SOURCES( Vector2d, Vector2d ) +DECLARE_COREDATA_SOURCES( Vector3f, Vector3f ) +DECLARE_COREDATA_SOURCES( Vector3d, Vector3d ) +DECLARE_COREDATA_SOURCES( Vector4f, Vector4f ) +DECLARE_COREDATA_SOURCES( Vector4d, Vector4d ) +DECLARE_COREDATA_SOURCES( Vector2i, Vector2i ) +DECLARE_COREDATA_SOURCES( Vector3i, Vector3i ) +DECLARE_COREDATA_SOURCES( Vector4i, Vector4i ) +DECLARE_COREDATA_SOURCES( Vector2ui, Vector2ui ) +DECLARE_COREDATA_SOURCES( Vector3ui, Vector3ui ) +DECLARE_COREDATA_SOURCES( Vector4ui, Vector4ui ) + +#undef DECLARE_COREDATA_SOURCES + +// Partial specialisation for editable data sources +#define SPECIALIZE_EDITABLE_SOURCE( TYPE, NAME ) \ + template <> \ + inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : \ + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { \ + setEditable( #NAME ); \ + } \ + \ + template <> \ + inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { \ + data[#NAME] = *getData(); \ + } \ + \ + template <> \ + inline bool SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { \ + if ( data.contains( #NAME ) ) { \ + TYPE v = data[#NAME]; \ + setData( v ); \ + } \ + return true; \ + } + +SPECIALIZE_EDITABLE_SOURCE( bool, boolean ) +SPECIALIZE_EDITABLE_SOURCE( float, number ) +SPECIALIZE_EDITABLE_SOURCE( double, number ) +SPECIALIZE_EDITABLE_SOURCE( int, value ) +SPECIALIZE_EDITABLE_SOURCE( unsigned int, value ) + +#undef SPECIALIZE_EDITABLE_SOURCE + +// Color specialization need different implementation (as well as any Ra::Vectorxx) +template <> +inline SingleDataSourceNode::SingleDataSourceNode( + const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "color" ); +} + +template <> +inline void +SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["color"] = Ra::Core::Utils::Color::linearRGBTosRGB( *getData() ); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "color" ) ) { + std::array c = data["color"]; + auto v = + Ra::Core::Utils::Color::sRGBToLinearRGB( Ra::Core::Utils::Color( c[0], c[1], c[2] ) ); + setData( std::move( v ) ); + } + return true; +} + +// Ra::Core::Vector4 specialization +template <> +inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "vector" ); +} + +template <> +inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["vector"] = *getData(); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "vector" ) ) { + std::array v = data["vector"]; + auto vec = Ra::Core::Vector4( v[0], v[1], v[2], v[3] ); + setData( vec ); + } + return true; +} + +// Ra::Core::Vector3 specialization +template <> +inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "vector" ); +} + +template <> +inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["vector"] = *getData(); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "vector" ) ) { + std::array v = data["vector"]; + auto vec = Ra::Core::Vector3( v[0], v[1], v[2] ); + setData( vec ); + } + return true; +} + +// Ra::Core::Vector2 specialization +template <> +inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "vector" ); +} + +template <> +inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["vector"] = *getData(); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "vector" ) ) { + std::array v = data["vector"]; + auto vec = Ra::Core::Vector2( v[0], v[1] ); + setData( vec ); + } + return true; +} + +} // namespace Sources +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sources/FunctionSource.hpp b/src/Dataflow/Core/Nodes/Sources/FunctionSource.hpp new file mode 100644 index 00000000000..da8e8e166d3 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sources/FunctionSource.hpp @@ -0,0 +1,106 @@ +#pragma once +#include "Dataflow/Core/NodeFactory.hpp" +#pragma once +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sources { + +/** + * \brief Node that deliver a std::function + * \tparam R return type of the function + * \tparam Type of the function arguments + */ +template +class FunctionSourceNode : public Node +{ + + public: + using function_type = std::function; + + explicit FunctionSourceNode( const std::string& name ) : + FunctionSourceNode( name, FunctionSourceNode::getTypename() ) {} + + bool execute() override; + + /** \brief Set the function to be delivered by the node. + * @param data + */ + void setData( function_type* data ); + + /** + * \brief Get the delivered data + * @return The non owning pointer (alias) to the delivered data. + */ + function_type* getData() const; + + Node::PortOutPtr getFunctionPort() { return m_portOut; } + + protected: + FunctionSourceNode( const std::string& instanceName, const std::string& typeName ); + + bool fromJsonInternal( const nlohmann::json& data ) override { + return Node::fromJsonInternal( data ); + } + void toJsonInternal( nlohmann::json& data ) const override { Node::toJsonInternal( data ); } + + /// @{ + /// The data provided by the node + function_type m_localData { []( Args... ) { return R {}; } }; + function_type* m_data { &m_localData }; + /// @} + + /// Alias to the output port + Node::PortOutPtr m_portOut; + + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +FunctionSourceNode::FunctionSourceNode( const std::string& instanceName, + const std::string& typeName ) : + Node( instanceName, typeName ), m_portOut { addOutputPort( m_data, "f" ) } {} + +template +bool FunctionSourceNode::execute() { + auto interfacePort = static_cast*>( m_interface[0] ); + if ( interfacePort->isLinked() ) { m_data = &( interfacePort->getData() ); } + else { m_data = &m_localData; } + m_portOut->setData( m_data ); + return true; +} + +template +void FunctionSourceNode::setData( function_type* data ) { + m_localData = *data; + m_data = &m_localData; + m_portOut->setData( m_data ); +} + +template +typename FunctionSourceNode::function_type* +FunctionSourceNode::getData() const { + return m_data; +} + +template +const std::string& FunctionSourceNode::getTypename() { + static std::string demangledTypeName = + std::string { "Source<" } + Ra::Dataflow::Core::simplifiedDemangledType() + + ">"; + return demangledTypeName; +} + +} // namespace Sources +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sources/SingleDataSourceNode.hpp b/src/Dataflow/Core/Nodes/Sources/SingleDataSourceNode.hpp new file mode 100644 index 00000000000..7b8055c8738 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sources/SingleDataSourceNode.hpp @@ -0,0 +1,140 @@ +#pragma once +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sources { +/** + * \brief Base class for nodes that will give access to some input data to the graph. + * This class can be used to feed nodes on a dataflow graph with some data coming + * from outside the graph or from the source node itself. + * + * The data delivered by the node can be explicitly set/get or can be made editable. + * + * @tparam T The type of the data to serve. + */ +template +class SingleDataSourceNode : public Node +{ + protected: + SingleDataSourceNode( const std::string& instanceName, const std::string& typeName ); + + public: + // warning, hacky specialization for set editable + explicit SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) {} + + bool execute() override; + + /** \brief Set the data to be delivered by the node. + * @param data + */ + void setData( T data ); + + /** + * \brief Get the delivered data + * @return The non owning pointer (alias) to the delivered data. + */ + T* getData() const; + + /** + * \brief Set the delivered data editable using the given name + * Give access to the internal data storage. + * If the node interface is connected, the edition will not result on a propagation to the + * graph as internal data storage will be superseeded by the data from the interface. + * @param name Name of the data as it will appear on edition gui. If not given, the default + * name "Data" will be used. + */ + void setEditable( const std::string& name = "Data" ); + + /** + * \brief Remove the delivered data from being editable + * @param name Name of the data given when calling setEditable + */ + void removeEditable( const std::string& name = "Data" ); + + std::shared_ptr> getOuputPort() { return m_portOut; } + + protected: + bool fromJsonInternal( const nlohmann::json& data ) override { + return Node::fromJsonInternal( data ); + } + void toJsonInternal( nlohmann::json& data ) const override { Node::toJsonInternal( data ); } + + private: + /// @{ + /// The data provided by the node + /// Used to deliver (and edit) data when the interface is not connected. + T m_localData; + /// Ownership of this pointer is left to the caller + T* m_data { &m_localData }; + /// @} + + /// Alias to the output port + Node::PortOutPtr m_portOut; + + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +SingleDataSourceNode::SingleDataSourceNode( const std::string& instanceName, + const std::string& typeName ) : + Node( instanceName, typeName ), m_portOut { addOutputPort( m_data, "to" ) } {} + +template +bool SingleDataSourceNode::execute() { + // interfaces ports are at the same index as output ports + auto interfacePort = static_cast*>( m_interface[0] ); + if ( interfacePort->isLinked() ) { + // use external storage to deliver data + m_data = &( interfacePort->getData() ); + } + else { + // use local storage to deliver data + m_data = &m_localData; + } + m_portOut->setData( m_data ); + return true; +} + +template +void SingleDataSourceNode::setData( T data ) { + m_localData = std::move( data ); +} + +/// \todo weird to have getData do not return what is set... +template +T* SingleDataSourceNode::getData() const { + return m_data; +} + +template +void SingleDataSourceNode::setEditable( const std::string& name ) { + Node::addEditableParameter( new EditableParameter( name, m_localData ) ); +} + +template +void SingleDataSourceNode::removeEditable( const std::string& name ) { + Node::removeEditableParameter( name ); +} + +template +const std::string& SingleDataSourceNode::getTypename() { + static std::string demangledTypeName = + std::string { "Source<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledTypeName; +} + +} // namespace Sources +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Port.hpp b/src/Dataflow/Core/Port.hpp new file mode 100644 index 00000000000..ccaa300842b --- /dev/null +++ b/src/Dataflow/Core/Port.hpp @@ -0,0 +1,422 @@ +#pragma once +#include + +#include + +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +class Node; + +/** + * \brief Base class for nodes' ports + * A port is a strongly typed extremity of connections between nodes. + * \warning when comparing and using typed port, beware of the const qualifier + * that is not always exposed by the C++ type system. There are some undefined behavior concerning + * const_casts and const qualifier in the C++ documentation + * (https://en.cppreference.com/w/cpp/language/const_cast). + * + */ +class RA_DATAFLOW_API PortBase +{ + private: + /// The name of the port. + std::string m_name { "" }; + /// The port's data's type's index. + std::type_index m_type; + /// A pointer to the node this port belongs to. + Node* m_node { nullptr }; /// \todo switch to shared_ptr ? + + protected: + /// Flag that tells if the port is linked. + bool m_isLinked { false }; + /// Flag that tells if the port must have a connection + bool m_isLinkMandatory { false }; + + public: + /// \name Constructors + /// @{ + /// \brief delete default constructors. + /// \see https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-copy-virtual + /// Constructors. + PortBase() = delete; + PortBase( const PortBase& ) = delete; + PortBase& operator=( const PortBase& ) = delete; + + /// @param name The name of the port. + /// @param type The data's type's hash. + /// @param node The pointer to the node associated with the port. + PortBase( const std::string& name, std::type_index type, Node* node ); + /// @} + + /// \brief make PortBase a base abstract class + virtual ~PortBase() = default; + + /// Gets the port's name. + const std::string& getName() const; + /// Gets the type of the data (efficient for comparisons). + std::type_index getType() const; + /// Gets a pointer to the node this port belongs to. + Node* getNode() const; + virtual bool hasData(); + // TODO : getData() to avoid dynamic_cast to get the data of the PortOut. + /// Returns true if the port is linked + bool isLinked() const; + /// Returns true if the port is flagged as being mandatory linked + bool isLinkMandatory() const; + /// Flags the port as being mandatory linked + void mustBeLinked(); + virtual PortBase* getLink() = 0; + virtual bool accept( PortBase* other ); + virtual bool connect( PortBase* other ) = 0; + virtual bool disconnect() = 0; + /// Returns a reflected (In <-> Out) port of the same type + virtual PortBase* reflect( Node* node, std::string name ) = 0; + /// Returns true if the port is an input port + virtual bool is_input() { return false; } + + /// Allows to get data stored at this port if it is an output port. + /// This method copy the data onto the given object + /// @params t The reference to store the data of this port + template + void getData( T& t ); + + /// Allows to get data stored at this port if it is an output port. + /// This method do not copy the data but gives a reference to the transmitted object. + /// TODO Verify the robustness of this + /// @params t The reference to store the data of this port + template + T& getData(); + + /// Check if this port is an output port, then takes a pointer to the data this port will point + /// to. + /// @param data The pointer to the data. + template + void setData( T* data ); + + std::string getTypeName() const; +}; + +/** + * \brief Output port delivering data of Type T. + * Output port stores a non-owning pointer to the data that will be made available on a connection. + * \tparam T The type of the delivered data. + */ +template +class PortOut : public PortBase +{ + private: + /// The data the port points to. + + /// Use raw ptr, data belongs to node and can be plain stack variable + T* m_data { nullptr }; + + public: + using DataType = T; + /// \name Constructors + /// @{ + /// \brief delete default constructors. + PortOut() = delete; + PortOut( const PortOut& ) = delete; + PortOut& operator=( const PortOut& ) = delete; + /// Constructor. + /// @param name The name of the port. + /// @param node The pointer to the node associated with the port. + /// \todo remove this one + PortOut( const std::string& name, Node* node ); + PortOut( Node* node, const std::string& name ); + PortOut( Node* node, T* data, const std::string& name ); + /// @} + + /// Gets a reference to the data this ports points to. + T& getData(); + /// Takes a pointer to the data this port will point to. + /// @param data The pointer to the data. + void setData( T* data ); + /// Returns true if the pointer to the data is not null. + bool hasData() override; + /// Returns nullptr because this port is an out port. + PortBase* getLink() override; + /// Returns false because out ports can not accept connection. + /// @param o The other port to test the connection. + bool accept( PortBase* ) override; + /// Calls the connect(PortBase* o) function of the o node because out ports can not connect. + /// Also sets m_isLinked. + /// @param o The other port to connect. + bool connect( PortBase* o ) override; + /// Returns false because out ports can not disconnect. + bool disconnect() override; + /// Returns a portIn of the same type + PortBase* reflect( Node* node, std::string name ) override; +}; + +/** + * \brief Input port accepting data of type T. + * An input port does not staore the data but is an accessor to the data stored on the connected + * output port. An Input port is observable and notify its observers at each connect/disconnect + * event. \tparam T The accepted data type + */ +template +class PortIn : public PortBase, + public Ra::Core::Utils::Observable&, bool> +{ + private: + /// A pointer to the out port this port is connected to. + PortOut* m_from = nullptr; + + public: + using DataType = T; + /// \name Constructors + /// @{ + /// \brief delete default constructors. + PortIn() = delete; + PortIn( const PortIn& ) = delete; + PortIn& operator=( const PortIn& ) = delete; + /// Constructor. + /// @param name The name of the port. + /// @param node The pointer to the node associated with the port. + ///\todo remove this one + PortIn( const std::string& name, Node* node ); + PortIn( Node* node, const std::string& name ); + /// @} + + /// Gets the out port this port is connected to. + PortBase* getLink() override; + /// Returns true if the port is linked to an output port that has data. + bool hasData() override; + /// Gets a reference to the data pointed by the connected out port. + /// \note no verification is made about the availability of the data. + T& getData(); + /// Checks if there is not out port already connected and if the data types are the same. + /// @param o The other port to test the connection + bool accept( PortBase* other ) override; + /// Connects this in port and the other out port if there is no out port already connected and + /// if the data types are the same. Also sets m_isLinked. + /// @param o The other port to connect. + bool connect( PortBase* other ) override; + /// Disconnects this port if it is connected. + /// Also sets m_isLinked to false. + bool disconnect() override; + /// Returns a portOut of the same type + PortBase* reflect( Node* node, std::string name ) override; + /// Returns true if the port is an input port + bool is_input() override; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline PortBase::PortBase( const std::string& name, std::type_index type, Node* node ) : + m_name( name ), m_type( type ), m_node( node ) {} + +inline const std::string& PortBase::getName() const { + return m_name; +} + +inline std::type_index PortBase::getType() const { + return m_type; +} + +inline std::string PortBase::getTypeName() const { + return simplifiedDemangledType( m_type ); +} + +inline Node* PortBase::getNode() const { + return m_node; +} + +inline bool PortBase::hasData() { + return false; +} + +inline bool PortBase::isLinked() const { + return m_isLinked; +} + +inline bool PortBase::isLinkMandatory() const { + return m_isLinkMandatory; +} + +inline void PortBase::mustBeLinked() { + m_isLinkMandatory = true; +} + +inline bool PortBase::accept( PortBase* other ) { + return m_type == other->getType(); +} + +template +PortOut::PortOut( const std::string& name, Node* node ) : PortBase( name, typeid( T ), node ) {} + +template +PortOut::PortOut( Node* node, const std::string& name ) : PortBase( name, typeid( T ), node ) {} + +template +PortOut::PortOut( Node* node, T* data, const std::string& name ) : + PortBase( name, typeid( T ), node ), m_data { data } {} + +template +T& PortOut::getData() { + return *m_data; +} + +template +void PortOut::setData( T* data ) { + m_data = data; +} + +template +bool PortOut::hasData() { + return m_data != nullptr; +} + +template +PortBase* PortOut::getLink() { + return nullptr; +} + +template +bool PortOut::accept( PortBase* ) { + return false; +} + +template +bool PortOut::connect( PortBase* o ) { + m_isLinked = o->connect( this ); + return m_isLinked; +} + +template +void PortBase::setData( T* data ) { + if ( !is_input() ) { + auto thisOut = dynamic_cast*>( this ); + if ( thisOut ) { + thisOut->setData( data ); + return; + } + LOG( Ra::Core::Utils::logERROR ) + << "Unable to set data with type " << simplifiedDemangledType( *data ) << " on port " + << getName() << " which expect " << getTypeName() << ".\n"; + std::abort(); + } + LOG( Ra::Core::Utils::logERROR ) + << "Could not call PortBase::setData(T* data) on the input port " << getName() << ".\n"; + std::abort(); +} + +template +void PortBase::getData( T& t ) { + if ( !is_input() ) { + auto thisOut = dynamic_cast*>( this ); + if ( thisOut ) { + t = thisOut->getData(); + return; + } + LOG( Ra::Core::Utils::logERROR ) + << "Unable to get data with type " << simplifiedDemangledType() << " on port " + << getName() << " which expect " << getTypeName() << ".\n"; + std::abort(); + } + LOG( Ra::Core::Utils::logERROR ) + << "Could not call PortBase::getData( T& t ) on the input port " << getName() << ".\n"; + std::abort(); +} + +template +T& PortBase::getData() { + if ( !is_input() ) { + auto thisOut = dynamic_cast*>( this ); + if ( thisOut ) { return thisOut->getData(); } + LOG( Ra::Core::Utils::logERROR ) + << "Unable to get data with type " << simplifiedDemangledType() << " on port " + << getName() << " which expect " << getTypeName() << ".\n"; + std::abort(); + } + LOG( Ra::Core::Utils::logERROR ) + << "Could not call T& PortBase::getData() on the input port " << getName() << ".\n"; + std::abort(); +} + +template +bool PortOut::disconnect() { + return false; +} + +template +PortBase* PortOut::reflect( Node* node, std::string name ) { + return new PortIn( name, node ); +} + +/** + * PortIn is an Observable&> that notifies its observers at connect/disconnect event + * @tparam T + */ +template +PortIn::PortIn( const std::string& name, Node* node ) : PortBase( name, typeid( T ), node ) {} + +template +PortIn::PortIn( Node* node, const std::string& name ) : PortBase( name, typeid( T ), node ) {} + +template +PortBase* PortIn::getLink() { + return m_from; +} + +template +T& PortIn::getData() { + return m_from->getData(); +} + +template +inline bool PortIn::hasData() { + if ( isLinked() ) { return m_from->hasData(); } + return false; +} + +template +bool PortIn::accept( PortBase* other ) { + if ( !m_from && ( other->getType() == getType() ) ) { return PortBase::accept( other ); } + return false; +} + +template +bool PortIn::connect( PortBase* other ) { + if ( accept( other ) ) { + m_from = static_cast*>( other ); + m_isLinked = true; + // notify after connect + this->notify( getName(), *this, true ); + } + return m_isLinked; +} + +template +bool PortIn::disconnect() { + if ( m_isLinked ) { + // notify before disconnect + this->notify( getName(), *this, false ); + m_from = nullptr; + m_isLinked = false; + return true; + } + return false; +} +template +PortBase* PortIn::reflect( Node* node, std::string name ) { + return new PortOut( name, node ); +} + +template +bool PortIn::is_input() { + return true; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/TypeDemangler.cpp b/src/Dataflow/Core/TypeDemangler.cpp new file mode 100644 index 00000000000..1f6105d6f17 --- /dev/null +++ b/src/Dataflow/Core/TypeDemangler.cpp @@ -0,0 +1,52 @@ +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace TypeInternal { + +/** \todo verify windows specific type demangling needs. + * + */ +RA_DATAFLOW_API auto makeTypeReadable( const std::string& fullType ) -> std::string { + static std::map knownTypes { + { "std::", "" }, + { "__cxx11::", "" }, + { ", std::allocator", "" }, + { ", std::allocator", "" }, + { ", std::allocator", "" }, + { ", std::allocator", "" }, + { "Ra::Core::VectorArray", "RaVector" }, + { "Ra::Core::Utils::ColorBase", "Color" }, + { "Ra::Core::Utils::ColorBase", "Color" }, + { "Eigen::Matrix", "Vector2" }, + { "Eigen::Matrix", "Vector2d" }, + { "Eigen::Matrix", "Vector2i" }, + { "Eigen::Matrix", "Vector2ui" }, + { "Eigen::Matrix", "Vector3" }, + { "Eigen::Matrix", "Vector3d" }, + { "Eigen::Matrix", "Vector3i" }, + { "Eigen::Matrix", "Vector3ui" }, + { "Eigen::Matrix", "Vector4" }, + { "Eigen::Matrix", "Vector4d" }, + { "Eigen::Matrix", "Vector4i" }, + { "Eigen::Matrix", "Vector4ui" }, + // Windows (visual studio 2022) specific name fix + { " __ptr64", "" } }; + + auto processedType = fullType; + for ( const auto& [key, value] : knownTypes ) { + Ra::Core::Utils::replaceAllInString( processedType, key, value ); + } + return processedType; +} + +} // namespace TypeInternal +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/TypeDemangler.hpp b/src/Dataflow/Core/TypeDemangler.hpp new file mode 100644 index 00000000000..eb337a39d66 --- /dev/null +++ b/src/Dataflow/Core/TypeDemangler.hpp @@ -0,0 +1,53 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/// \brief Return the human readable version of the type name T with simplified radium type names +template +auto simplifiedDemangledType() noexcept -> std::string; + +/// \brief Return the human readable version of the type name T with simplified radium type names +template +auto simplifiedDemangledType( const T& ) noexcept -> std::string; + +/// \brief Return the human readable version of the type name whose index is known, with simplified +/// radium type names +/// \param typeName The typeIndex whose simplified named is requested +/// \return The Radium-simplified type name +RA_DATAFLOW_API auto simplifiedDemangledType( const std::type_index& typeName ) noexcept + -> std::string; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +namespace TypeInternal { +RA_DATAFLOW_API auto makeTypeReadable( const std::string& ) -> std::string; +} + +template +auto simplifiedDemangledType() noexcept -> std::string { + static auto demangled_name = []() { + std::string demangledType = + TypeInternal::makeTypeReadable( Ra::Core::Utils::demangleType() ); + return demangledType; + }(); + return demangled_name; +} + +template +auto simplifiedDemangledType( const T& ) noexcept -> std::string { + return simplifiedDemangledType(); +} + +inline auto simplifiedDemangledType( const std::type_index& typeName ) noexcept -> std::string { + return TypeInternal::makeTypeReadable( Ra::Core::Utils::demangleType( typeName ) ); +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/filelist.cmake b/src/Dataflow/Core/filelist.cmake new file mode 100644 index 00000000000..7ff6168557c --- /dev/null +++ b/src/Dataflow/Core/filelist.cmake @@ -0,0 +1,36 @@ +# ---------------------------------------------------- +# This file can be generated from a script: +# To do so, run "./generateFilelistForModule.sh Dataflow/Core" +# from ./scripts directory +# ---------------------------------------------------- + +set(dataflow_core_sources DataflowGraph.cpp Node.cpp NodeFactory.cpp Nodes/CoreBuiltInsNodes.cpp + TypeDemangler.cpp +) + +set(dataflow_core_headers + DataflowGraph.hpp + EditableParameter.hpp + Enumerator.hpp + Node.hpp + NodeFactory.hpp + Nodes/CoreBuiltInsNodes.hpp + Nodes/Functionals/BinaryOpNode.hpp + Nodes/Functionals/CoreDataFunctionals.hpp + Nodes/Functionals/FilterNode.hpp + Nodes/Functionals/ReduceNode.hpp + Nodes/Functionals/TransformNode.hpp + Nodes/Sinks/CoreDataSinks.hpp + Nodes/Sinks/SinkNode.hpp + Nodes/Sources/CoreDataSources.hpp + Nodes/Sources/FunctionSource.hpp + Nodes/Sources/SingleDataSourceNode.hpp + Port.hpp + TypeDemangler.hpp +) + +set(dataflow_core_private + Nodes/Private/FunctionalsNodeFactory.hpp Nodes/Private/FunctionalsNodeFactory.cpp + Nodes/Private/SinksNodeFactory.hpp Nodes/Private/SinksNodeFactory.cpp + Nodes/Private/SourcesNodeFactory.hpp Nodes/Private/SourcesNodeFactory.cpp +) diff --git a/src/Dataflow/Core/pch.hpp b/src/Dataflow/Core/pch.hpp new file mode 100644 index 00000000000..6f70f09beec --- /dev/null +++ b/src/Dataflow/Core/pch.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/Dataflow/QtGui/CMakeLists.txt b/src/Dataflow/QtGui/CMakeLists.txt new file mode 100644 index 00000000000..c3d2a17472e --- /dev/null +++ b/src/Dataflow/QtGui/CMakeLists.txt @@ -0,0 +1,64 @@ +set(ra_dataflowqtgui_target DataflowQtGui) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflowqtgui_target}] ") + +project(${ra_dataflowqtgui_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +find_package(RadiumNodeEditor REQUIRED NO_DEFAULT_PATH) + +include(filelist.cmake) + +# Qt utility functions +include(QtFunctions) + +# Find Qt packages +find_qt_package(COMPONENTS Core Widgets OpenGL Xml REQUIRED) +set(QT_DEFAULT_MAJOR_VERSION ${QT_DEFAULT_MAJOR_VERSION} PARENT_SCOPE) +set(Qt_LIBRARIES Qt::Core Qt::Widgets Qt::OpenGL Qt::Xml) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +qt_wrap_ui(gui_uis ${gui_uis}) + +# configure library +find_package(PowerSlider REQUIRED) + +add_library( + ${ra_dataflowqtgui_target} SHARED ${dataflow_qtgui_sources} ${dataflow_qtgui_headers} + ${dataflow_qtgui_resources} +) + +# This one should be extracted directly from parent project properties. +target_compile_definitions(${ra_dataflowqtgui_target} PRIVATE RA_DATAFLOW_EXPORTS) + +target_compile_options(${ra_dataflowqtgui_target} PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) + +target_include_directories(${ra_dataflowqtgui_target} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +add_dependencies(${ra_dataflowqtgui_target} DataflowCore Gui) + +target_link_libraries( + ${ra_dataflowqtgui_target} PUBLIC ${Qt_LIBRARIES} DataflowCore Gui + PUBLIC RadiumNodeEditor::RadiumNodeEditor PRIVATE PowerSlider::PowerSlider +) + +message(STATUS "Configuring library ${ra_dataflowqtgui_target} with standard settings") +configure_radium_target(${ra_dataflowqtgui_target}) +# configure the library only. The package is a sub-package and should be configured independently +configure_radium_library( + TARGET ${ra_dataflowqtgui_target} COMPONENT TARGET_DIR "Dataflow/QtGui" + FILES "${dataflow_qtgui_headers}" +) +# Generate cmake package +configure_radium_package( + NAME ${ra_dataflowqtgui_target} PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + PACKAGE_DIR "lib/cmake/Radium/${ra_dataflowqtgui_target}" NAME_PREFIX "Radium" +) + +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflowqtgui_target} PARENT_SCOPE) + +if(RADIUM_ENABLE_PCH) + target_precompile_headers(${ra_dataflowqtgui_target} PRIVATE pch.hpp) +endif() + +list(REMOVE_AT CMAKE_MESSAGE_INDENT -1) diff --git a/src/Dataflow/QtGui/Config.cmake.in b/src/Dataflow/QtGui/Config.cmake.in new file mode 100644 index 00000000000..42e8dadec3b --- /dev/null +++ b/src/Dataflow/QtGui/Config.cmake.in @@ -0,0 +1,43 @@ +# -------------- Configuration of the Radium DataflowCore targets and definitions ----------------------- +# Setup Engine and check for dependencies + +if (DataflowQtGui_FOUND AND NOT TARGET DataflowQtGui) + set(Configure_DataflowQtGui ON) + # verify dependencies + if(NOT DataflowCore_FOUND) + # if in source dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake) + else() + # if in install dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowQtGui: dependency DataflowCore not found") + set(Configure_DataflowQtGui OFF) + endif() + endif() + endif() + if(NOT Gui_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Gui/RadiumGuiConfig.cmake") + set(Gui_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Gui/RadiumGuiConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowQtGui: dependency Gui not found") + set(Configure_DataflowQtGui OFF) + endif() + endif() +endif() + +if(Configure_DataflowQtGui) + set(RadiumNodeEditor_DIR "@RadiumNodeEditor_DIR@") + find_dependency(RadiumNodeEditor REQUIRED NO_DEFAULT_PATH) + if(MSVC OR MSVC_IDE OR MINGW) + add_imported_dir(FROM RadiumNodeEditor::RadiumNodeEditor TO RadiumExternalDlls_location) + endif() + include("${CMAKE_CURRENT_LIST_DIR}/../Dataflow/QtGui/DataflowQtGuiTargets.cmake") +endif() diff --git a/src/Dataflow/QtGui/GraphEditor/ConnectionStatusData.hpp b/src/Dataflow/QtGui/GraphEditor/ConnectionStatusData.hpp new file mode 100644 index 00000000000..e96973f3a27 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/ConnectionStatusData.hpp @@ -0,0 +1,40 @@ +#pragma once +#include + +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { +using namespace Ra::Dataflow::Core; + +/** + * \brief Allow to manage connections in the RadiumNodeEditor external + */ +class RA_DATAFLOW_API ConnectionStatusData : public QtNodes::NodeData +{ + public: + ConnectionStatusData( std::shared_ptr node, const std::string& outputName ) : + m_node { node }, m_outputName { outputName } {} + + QtNodes::NodeDataType type() const override { + return QtNodes::NodeDataType { "connection", "connectionStatus" }; + } + + std::shared_ptr getNode() { return m_node; } + std::string getOutputName() { return m_outputName; } + + private: + std::shared_ptr m_node { nullptr }; + std::string m_outputName; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditor.qrc b/src/Dataflow/QtGui/GraphEditor/GraphEditor.qrc new file mode 100644 index 00000000000..b34fa0ffaa2 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditor.qrc @@ -0,0 +1,10 @@ + + + images/copy.png + images/cut.png + images/new.png + images/open.png + images/paste.png + images/save.png + + diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorView.cpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.cpp new file mode 100644 index 00000000000..67890e0f7b6 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.cpp @@ -0,0 +1,166 @@ +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +GraphEditorView::GraphEditorView( QWidget* parent ) : QWidget( parent, Qt::Window ) { + QtNodes::ConnectionStyle::setConnectionStyle( + R"( + { + "ConnectionStyle": { + "UseDataDefinedColors": true + } + } + )" ); + + QVBoxLayout* l = new QVBoxLayout( this ); + + scene = new QtNodes::FlowScene( l ); + view = new QtNodes::FlowView( scene ); + + l->addWidget( view ); + l->setContentsMargins( 0, 0, 0, 0 ); + l->setSpacing( 0 ); + + // Create widgets + WidgetFactory::initializeWidgetFactory(); + + connectAll(); +} + +GraphEditorView::~GraphEditorView() { + disconnectAll(); +} + +void GraphEditorView::disconnectAll() { + for ( size_t i = 0; i < connections.size(); i++ ) { + QObject::disconnect( connections[i] ); + } + connections.clear(); +} + +void GraphEditorView::connectAll() { + + // This one is only here to allow modifying node parameters through the embedded widget + // TODO, find another way to do this. + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodeHoverLeft, [this]() { emit needUpdate(); } ) ); + + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodePlaced, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodeDeleted, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::connectionCreated, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::connectionDeleted, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodeMoved, []( QtNodes::Node& n, const QPointF& ) { + QJsonObject obj; + obj["x"] = n.nodeGraphicsObject().pos().x(); + obj["y"] = n.nodeGraphicsObject().pos().y(); + QJsonObject nodeJson; + nodeJson["position"] = obj; + n.nodeDataModel()->addMetaData( nodeJson ); + } ) ); +} + +void GraphEditorView::buildAdapterRegistry( const NodeFactorySet& factories ) { + m_editorRegistry.reset( new QtNodes::DataModelRegistry ); + for ( const auto& [factoryName, factory] : factories ) { + for ( const auto& [typeName, creator] : factory->getFactoryMap() ) { + auto f = creator.first; + auto creatorFactory = factory; + m_editorRegistry->registerModel( + typeName.c_str(), + [f, this, creatorFactory]() -> std::unique_ptr { + nlohmann::json jsondata; + auto node = f( jsondata ); + this->m_dataflowGraph->addNode( node ); + this->m_dataflowGraph->addFactory( creatorFactory ); + return std::make_unique( this->m_dataflowGraph, node ); + }, + factoryName.c_str(), + creator.second.c_str() ); + } + } + scene->setRegistry( m_editorRegistry ); +} + +DataflowGraph* GraphEditorView::editedGraph() { + return m_dataflowGraph.get(); +} + +void GraphEditorView::editGraph( std::shared_ptr g ) { + // Disconnect all event and clear the previous graph + disconnectAll(); + scene->clearScene(); + + // Setup the graph to edit + m_dataflowGraph = g; + if ( m_dataflowGraph ) { + buildAdapterRegistry( NodeFactoriesManager::getFactoryManager() ); + const auto& nodes = m_dataflowGraph->getNodes(); + // NodeToUuid mapping + std::map nodeToUuid; + // inserting nodes + for ( const auto& n : nodes ) { + auto nodeAdapter = std::make_unique( m_dataflowGraph, n ); + nodeToUuid.insert( { n.get(), nodeAdapter->uuid() } ); + scene->importNode( std::move( nodeAdapter ) ); + } + // inserting connections + for ( const auto& n : nodes ) { + int numPort = 0; + for ( const auto& input : n->getInputs() ) { + if ( input->isLinked() ) { + auto portOut = input->getLink(); + auto nodeOut = portOut->getNode(); + int outPortIndex = 0; + for ( const auto& p : nodeOut->getOutputs() ) { + if ( p.get() == portOut ) { break; } + outPortIndex++; + } + scene->importConnection( + nodeToUuid[nodeOut], outPortIndex, nodeToUuid[n.get()], numPort ); + } + numPort++; + } + } + scene->setSceneName( m_dataflowGraph->getInstanceName().c_str() ); + } + else { scene->setSceneName( "untitled" ); } + scene->iterateOverNodes( []( QtNodes::Node* n ) { n->onNodeSizeUpdated(); } ); + + // view->fitInView( view->sceneRect(), Qt::KeepAspectRatio); + // view->resetTransform(); + view->ensureVisible( view->sceneRect() ); + view->centerOn( view->sceneRect().center() ); + // Re-connect events + connectAll(); +} + +// Find the way to see all the scene in the editor (or, at leas 75% of the scene) +void GraphEditorView::resizeEvent( QResizeEvent* ) { + // view->resetTransform(); + view->ensureVisible( view->sceneRect() ); + view->centerOn( view->sceneRect().center() ); +} + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorView.hpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.hpp new file mode 100644 index 00000000000..0cbbf179e62 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.hpp @@ -0,0 +1,62 @@ +#pragma once +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +class DataflowGraph; +class NodeFactorySet; +} // namespace Core + +/** + * + */ +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API GraphEditorView : public QWidget +{ + Q_OBJECT + public: + explicit GraphEditorView( QWidget* parent ); + ~GraphEditorView(); + void disconnectAll(); + void connectAll(); + + /// Fill the editor with the existing nodes in the graph + void editGraph( std::shared_ptr g ); + + DataflowGraph* editedGraph(); + + Q_SIGNALS: + void needUpdate(); + + protected: + void resizeEvent( QResizeEvent* event ) override; + + private: + std::vector connections; + + QtNodes::FlowScene* scene { nullptr }; + QtNodes::FlowView* view { nullptr }; + + /// Build the registries from the graph's NodeFactory + void buildAdapterRegistry( const NodeFactorySet& factory ); + + std::shared_ptr m_dataflowGraph { nullptr }; + + /// Node creator registry to be used when creating a node in the editor + std::shared_ptr m_editorRegistry; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.cpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.cpp new file mode 100644 index 00000000000..2679764bf84 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.cpp @@ -0,0 +1,289 @@ +#include "Dataflow/Core/NodeFactory.hpp" +#include +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +GraphEditorWindow::~GraphEditorWindow() { + delete m_graphEdit; +} + +GraphEditorWindow::GraphEditorWindow( std::shared_ptr graph ) : + m_graphEdit { new GraphEditorView( nullptr ) }, m_graph { graph } { + + setCentralWidget( m_graphEdit ); + m_graphEdit->setFocusPolicy( Qt::StrongFocus ); + + createActions(); + createStatusBar(); + + readSettings(); + + connect( + m_graphEdit, &GraphEditorView::needUpdate, this, &GraphEditorWindow::documentWasModified ); + + setCurrentFile( QString() ); + setUnifiedTitleAndToolBarOnMac( true ); + if ( !m_graph ) { + m_graph = std::make_shared( "unititled.flow" ); + newFile(); + } + + m_graphEdit->editGraph( m_graph ); + m_graphEdit->show(); +} + +#if 0 +void GraphEditorWindow::resetGraph( std::shared_ptr graph ) { + m_graph = graph; + m_graphEdit->editGraph( m_graph ); + setCurrentFile( "" ); +} +#endif + +void GraphEditorWindow::closeEvent( QCloseEvent* event ) { + if ( maybeSave() ) { + writeSettings(); + event->accept(); + } + else { event->ignore(); } +} + +void GraphEditorWindow::newFile() { + if ( maybeSave() ) { + // Currently edited graph must be deleted only after it is no more used by the editor + m_graph->destroy(); + setCurrentFile( "" ); + } +} + +void GraphEditorWindow::open() { + if ( maybeSave() ) { + QString fileName = QFileDialog::getOpenFileName( this ); + if ( !fileName.isEmpty() ) loadFile( fileName ); + } +} + +bool GraphEditorWindow::save() { + if ( m_curFile.isEmpty() ) { return saveAs(); } + else { return saveFile( m_curFile ); } +} + +bool GraphEditorWindow::saveAs() { + QFileDialog dialog( this ); + dialog.setWindowModality( Qt::WindowModal ); + dialog.setAcceptMode( QFileDialog::AcceptSave ); + dialog.setDefaultSuffix( "json" ); + if ( dialog.exec() != QDialog::Accepted ) return false; + return saveFile( dialog.selectedFiles().first() ); +} + +void GraphEditorWindow::about() { + QMessageBox::about( this, + tr( "About Node Editor" ), + tr( "This is NodeGraph Editor widget from Radium::Dataflow::QtGui." ) ); +} + +void GraphEditorWindow::documentWasModified() { + setWindowModified( m_graph->shouldBeSaved() ); + emit needUpdate(); +} + +void GraphEditorWindow::createActions() { + + auto fileMenu = menuBar()->addMenu( tr( "&File" ) ); + auto fileToolBar = addToolBar( tr( "File" ) ); + const QIcon newIcon = QIcon::fromTheme( "document-new", QIcon( ":/images/new.png" ) ); + auto newAct = new QAction( newIcon, tr( "&New" ), this ); + newAct->setShortcuts( QKeySequence::New ); + newAct->setStatusTip( tr( "Create a new file" ) ); + connect( newAct, &QAction::triggered, this, &GraphEditorWindow::newFile ); + fileMenu->addAction( newAct ); + fileToolBar->addAction( newAct ); + + const QIcon openIcon = QIcon::fromTheme( "document-open", QIcon( ":/images/open.png" ) ); + auto openAct = new QAction( openIcon, tr( "&Open..." ), this ); + openAct->setShortcuts( QKeySequence::Open ); + openAct->setStatusTip( tr( "Open an existing file" ) ); + connect( openAct, &QAction::triggered, this, &GraphEditorWindow::open ); + fileMenu->addAction( openAct ); + fileToolBar->addAction( openAct ); + + const QIcon saveIcon = QIcon::fromTheme( "document-save", QIcon( ":/images/save.png" ) ); + auto saveAct = new QAction( saveIcon, tr( "&Save" ), this ); + saveAct->setShortcuts( QKeySequence::Save ); + saveAct->setStatusTip( tr( "Save the document to disk" ) ); + connect( saveAct, &QAction::triggered, this, &GraphEditorWindow::save ); + fileMenu->addAction( saveAct ); + fileToolBar->addAction( saveAct ); + + const auto saveAsIcon = QIcon::fromTheme( "document-save-as" ); + auto saveAsAct = + fileMenu->addAction( saveAsIcon, tr( "Save &As..." ), this, &GraphEditorWindow::saveAs ); + saveAsAct->setShortcuts( QKeySequence::SaveAs ); + saveAsAct->setStatusTip( tr( "Save the document under a new name" ) ); + + fileMenu->addSeparator(); + + const QIcon exitIcon = QIcon::fromTheme( "application-exit" ); + auto exitAct = fileMenu->addAction( exitIcon, tr( "E&xit" ), this, &QWidget::close ); + exitAct->setShortcuts( QKeySequence::Quit ); + exitAct->setStatusTip( tr( "Exit the application" ) ); + +#if 0 + // Activite this section when editMenu might be filled + auto editMenu = menuBar()->addMenu( tr( "&Edit" ) ); + auto editToolBar = addToolBar( tr( "Edit" ) ); +#endif + + auto helpMenu = menuBar()->addMenu( tr( "&Help" ) ); + auto aboutAct = helpMenu->addAction( tr( "&About" ), this, &GraphEditorWindow::about ); + aboutAct->setStatusTip( tr( "Show the application's About box" ) ); + + auto aboutQtAct = helpMenu->addAction( tr( "About &Qt" ), qApp, &QApplication::aboutQt ); + aboutQtAct->setStatusTip( tr( "Show the Qt library's About box" ) ); + // if ( !m_ownGraph ) { menuBar()->hide(); } +} + +void GraphEditorWindow::createStatusBar() { + statusBar()->showMessage( tr( "Ready" ) ); +} + +void GraphEditorWindow::readSettings() { + QSettings settings( QCoreApplication::organizationName(), QCoreApplication::applicationName() ); + settings.beginGroup( "nodegraph editor" ); + const QByteArray geometry = settings.value( "geometry", QByteArray() ).toByteArray(); + if ( geometry.isEmpty() ) { + const QRect availableGeometry = screen()->availableGeometry(); + resize( availableGeometry.width() / 3, availableGeometry.height() / 2 ); + move( ( availableGeometry.width() - width() ) / 2, + ( availableGeometry.height() - height() ) / 2 ); + } + else { restoreGeometry( geometry ); } + const QByteArray graphGeometry = settings.value( "graph", QByteArray() ).toByteArray(); + if ( graphGeometry.isEmpty() ) { m_graphEdit->resize( 800, 600 ); } + else { m_graphEdit->restoreGeometry( graphGeometry ); } + settings.endGroup(); +} + +void GraphEditorWindow::writeSettings() { + QSettings settings( QCoreApplication::organizationName(), QCoreApplication::applicationName() ); + settings.beginGroup( "nodegraph editor" ); + settings.setValue( "geometry", saveGeometry() ); + settings.setValue( "graph", m_graphEdit->saveGeometry() ); + settings.endGroup(); +} + +bool GraphEditorWindow::maybeSave() { + if ( m_graph == nullptr ) { return true; } + if ( !m_graph->shouldBeSaved() ) { return true; } + const QMessageBox::StandardButton ret = + QMessageBox::warning( this, + tr( "Application" ), + tr( "The document has been modified.\n" + "Do you want to save your changes?" ), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel ); + switch ( ret ) { + case QMessageBox::Save: + return save(); + case QMessageBox::Cancel: + return false; + default: + break; + } + + return true; +} + +void GraphEditorWindow::loadFile( const QString& fileName ) { + QGuiApplication::setOverrideCursor( Qt::WaitCursor ); + { + QFile file( fileName ); + if ( !file.open( QFile::ReadOnly | QFile::Text ) ) { + QMessageBox::warning( + this, + tr( "Application" ), + tr( "Cannot read file %1:\n%2." ) + .arg( QDir::toNativeSeparators( fileName ), file.errorString() ) ); + return; + } + } + + bool loaded( true ); + m_graph->destroy(); + loaded = m_graph->loadFromJson( fileName.toStdString() ); + + if ( !loaded ) { + QMessageBox::warning( + this, + tr( "Application" ), + tr( "Can't load graph from file %1.\n" ).arg( QDir::toNativeSeparators( fileName ) ) ); + } + + QGuiApplication::restoreOverrideCursor(); + if ( loaded ) { + m_graphEdit->editGraph( m_graph ); + setCurrentFile( fileName ); + statusBar()->showMessage( tr( "File loaded" ), 2000 ); + emit needUpdate(); + } +} + +bool GraphEditorWindow::saveFile( const QString& fileName ) { + QString errorMessage; + + QGuiApplication::setOverrideCursor( Qt::WaitCursor ); + // TODO, if graph do not compile, tell it to the user ? + // m_graph->compile(); + + m_graph->saveToJson( fileName.toStdString() ); +#if 0 + QSaveFile file(fileName); + + if (file.open(QFile::WriteOnly | QFile::Text)) { + QTextStream out(&file); + out << textEdit->toPlainText(); + if (!file.commit()) { + errorMessage = tr("Cannot write file %1:\n%2.") + .arg(QDir::toNativeSeparators(fileName), file.errorString()); + } + } else { + errorMessage = tr("Cannot open file %1 for writing:\n%2.") + .arg(QDir::toNativeSeparators(fileName), file.errorString()); + } +#endif + QGuiApplication::restoreOverrideCursor(); + + if ( !errorMessage.isEmpty() ) { + QMessageBox::warning( this, tr( "Application" ), errorMessage ); + return false; + } + + setCurrentFile( fileName ); + statusBar()->showMessage( tr( "File saved" ), 2000 ); + return true; +} + +void GraphEditorWindow::setCurrentFile( const QString& fileName ) { + m_curFile = fileName; + // textEdit->document()->setModified(false); + setWindowModified( false ); + + QString shownName = m_curFile; + if ( m_curFile.isEmpty() ) shownName = "untitled.flow"; + setWindowFilePath( shownName ); +} + +QString GraphEditorWindow::strippedName( const QString& fullFileName ) { + return QFileInfo( fullFileName ).fileName(); +} + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.hpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.hpp new file mode 100644 index 00000000000..ca2f385686a --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.hpp @@ -0,0 +1,73 @@ +#pragma once +#include + +#include +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +/** + * \brief Window widget to edit a Node Graph. + * This class just wrap a Ra::Dataflow::QtGui::GraphEditor::GraphEditorView in a main window with + * several services : + * - The window can be used as a standalone editor if no parameter is given to the constructor. + * - The window can be used to edit an existing graph by giving the graph to the constructor. + * + * connect the client to the needUpdate() signal to be notified of a change in the edited graph. + */ +class RA_DATAFLOW_API GraphEditorWindow : public QMainWindow +{ + Q_OBJECT + public: + explicit GraphEditorWindow( std::shared_ptr graph = nullptr ); + ~GraphEditorWindow(); + + void loadFile( const QString& fileName ); +#if 0 + // not sure to neeed this + void resetGraph( std::shared_ptr graph = nullptr ); +#endif + + signals: + void needUpdate(); + + protected: + void closeEvent( QCloseEvent* event ) override; + + private slots: + void newFile(); + void open(); + bool save(); + bool saveAs(); + void about(); + void documentWasModified(); + + private: + void createActions(); + void createStatusBar(); + void readSettings(); + void writeSettings(); + bool maybeSave(); + bool saveFile( const QString& fileName ); + void setCurrentFile( const QString& fileName ); + QString strippedName( const QString& fullFileName ); + + GraphEditorView* m_graphEdit { nullptr }; + QString m_curFile; + + std::shared_ptr m_graph { nullptr }; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.cpp b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.cpp new file mode 100644 index 00000000000..a3d5da04ab6 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.cpp @@ -0,0 +1,337 @@ +#include + +#include + +#include + +#include +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +namespace NodeDataModelTools { +/** + * Construct the widget needed to edit all the EditableParameter of the given node + * @param node The node to decorate + * @return the widget (of type RadiumAddons::Gui::Widgets::ControlPanel) exposing the parameters + */ +QWidget* getWidget( Node* node ); + +/** + * Update the state of the widget to reflect the ones of the node. + * @param node + * @param widget + */ +void updateWidget( Node* node, QWidget* widget ); + +/** + * Convert a QJsonObject to a nlohmann::json + */ +void QJsonObjectToNlohmannObject( const QJsonObject& p, nlohmann::json& data ); + +/** + * Convert a nlohmann::json to a QJsonObject + */ +void NlohmannObjectToQJsonObject( const nlohmann::json& data, QJsonObject& p ); +} // namespace NodeDataModelTools + +using namespace Ra::Dataflow::Core; +using namespace Ra::Gui::Widgets; + +NodeAdapterModel::NodeAdapterModel( std::shared_ptr graph, + std::shared_ptr n ) : + m_node { n }, m_dataflowGraph { graph } { + m_uuid = QUuid::createUuid(); + m_inputsConnected.resize( m_node->getInputs().size() ); + m_widget = NodeDataModelTools::getWidget( m_node.get() ); + NodeDataModelTools::updateWidget( m_node.get(), m_widget ); + checkConnections(); +} + +void NodeAdapterModel::checkConnections() const { + int errors = 0; + for ( size_t i = 0; i < m_inputsConnected.size(); i++ ) { + if ( !m_inputsConnected[i] && m_node->getInputs()[i]->isLinkMandatory() ) { errors++; } + } + + if ( errors == 0 ) { + m_validationState = QtNodes::NodeValidationState::Valid; + m_validationError = QString( "" ); + } + else { + if ( errors > 1 ) { + m_validationError = QString( + std::string( std::to_string( errors ) + " mandatory ports are not linked (*)." ) + .c_str() ); + } + else { m_validationError = "1 mandatory port is not linked (*)."; } + + m_validationState = QtNodes::NodeValidationState::Error; + } +} + +unsigned int NodeAdapterModel::nPorts( QtNodes::PortType portType ) const { + unsigned int result = 1; + + switch ( portType ) { + case QtNodes::PortType::In: { + result = m_node->getInputs().size(); + break; + } + + case QtNodes::PortType::Out: { + result = m_node->getOutputs().size(); + break; + } + + default: { + break; + } + } + + return result; +} + +QtNodes::NodeDataType NodeAdapterModel::dataType( QtNodes::PortType portType, + QtNodes::PortIndex portIndex ) const { + QtNodes::NodeDataType result { "incorrect", "incorrect" }; + + switch ( portType ) { + case QtNodes::PortType::In: { + std::string mandatory = ( m_node->getInputs()[portIndex]->isLinkMandatory() ) ? "*" : ""; + return IOToDataType( m_node->getInputs()[portIndex]->getTypeName(), + m_node->getInputs()[portIndex]->getName() + mandatory ); + } + + case QtNodes::PortType::Out: { + return IOToDataType( m_node->getOutputs()[portIndex]->getTypeName(), + m_node->getOutputs()[portIndex]->getName() ); + } + default: + break; + } + + checkConnections(); + + return result; +} + +std::shared_ptr NodeAdapterModel::outData( QtNodes::PortIndex port ) { + return std::make_shared( m_node, m_node->getOutputs()[port]->getName() ); +} + +void NodeAdapterModel::setInData( std::shared_ptr data, int port ) { + auto connectionData = dynamic_cast( data.get() ); + if ( connectionData ) { + // Add connection + m_dataflowGraph->addLink( connectionData->getNode(), + connectionData->getOutputName(), + m_node, + m_node->getInputs()[port]->getName() ); + m_inputsConnected[port] = true; + } + else { + // Remove connection + m_dataflowGraph->removeLink( m_node, m_node->getInputs()[port]->getName() ); + m_inputsConnected[port] = false; + } + checkConnections(); +} + +QtNodes::NodeDataType NodeAdapterModel::IOToDataType( const std::string& typeName, + const std::string& ioName ) const { + return QtNodes::NodeDataType { typeName.c_str(), ioName.c_str() }; +} + +void NodeAdapterModel::updateState() { + int i = 0; + for ( const auto& in : m_node->getInputs() ) { + if ( in->isLinked() ) { m_inputsConnected[i] = true; } + i++; + } + checkConnections(); +} + +void NodeAdapterModel::addMetaData( QJsonObject& json ) { + nlohmann::json data; + NodeDataModelTools::QJsonObjectToNlohmannObject( json, data ); + m_node->addJsonMetaData( data ); +} + +NodeAdapterModel::~NodeAdapterModel() { + m_dataflowGraph->removeNode( m_node ); +} + +QJsonObject NodeAdapterModel::save() const { + QJsonObject o; // = QtNodes::NodeDataModel::save(); + nlohmann::json nodeData; + m_node->toJson( nodeData ); + NodeDataModelTools::NlohmannObjectToQJsonObject( nodeData, o ); + return o; +} + +void NodeAdapterModel::restore( QJsonObject const& p ) { + // QtNodes::NodeDataModel::restore( p ); + // 1 - convert the QJsonObject to nlohmann::json + nlohmann::json nodeData; + NodeDataModelTools::QJsonObjectToNlohmannObject( p, nodeData ); + // 2 - call fromjson on the node using this json object + m_node->fromJson( nodeData ); + // 3 - update the widget according to the editable parameters + NodeDataModelTools::updateWidget( m_node.get(), m_widget ); +} + +namespace NodeDataModelTools { + +QWidget* getWidget( Node* node ) { + if ( node->getEditableParameters().size() == 0 ) { return nullptr; } + + auto controlPanel = new ControlPanel( "Editable parameters", false, nullptr ); + controlPanel->beginLayout( QBoxLayout::TopToBottom ); + for ( size_t i = 0; i < node->getEditableParameters().size(); i++ ) { + auto edtParam = node->getEditableParameters()[i].get(); + QWidget* newWidget = WidgetFactory::createWidget( edtParam ); + if ( newWidget ) { + newWidget->setParent( controlPanel ); + newWidget->setObjectName( edtParam->getName().c_str() ); + controlPanel->addLabel( edtParam->getName() ); + controlPanel->addWidget( newWidget ); + + if ( i != node->getEditableParameters().size() - 1 ) { controlPanel->addSeparator(); } + } + } + controlPanel->endLayout(); + controlPanel->setVisible( true ); + + return controlPanel; +} + +void updateWidget( Node* node, QWidget* widget ) { + if ( node->getEditableParameters().size() == 0 ) { return; } + for ( size_t i = 0; i < node->getEditableParameters().size(); i++ ) { + auto edtParam = node->getEditableParameters()[i].get(); + if ( !WidgetFactory::updateWidget( widget, edtParam ) ) { + LOG( Ra::Core::Utils::logWARNING ) + << "NodeAdapterModel : unable to update parameter " << edtParam->getName() + << " on node " << node->getInstanceName() << " (" << node->getTypeName() << ")"; + } + } +} + +/** Convert from Qt::Json to nlohmann::json */ +void QJsonEntryToNlohmannEntry( const QString& key, + const QJsonValue& value, + nlohmann::json& data ) { + switch ( value.type() ) { + case QJsonValue::Bool: + data[key.toStdString()] = value.toBool(); + break; + case QJsonValue::Double: + // as there is no QJsonValue::Int, manage explicitely keys with int value :( + // TODO find a better way to do that ... + // type is a specific entry of envmapdatasource + if ( key.compare( "type" ) == 0 ) { data[key.toStdString()] = int( value.toDouble() ); } + else { data[key.toStdString()] = Scalar( value.toDouble() ); } + + break; + case QJsonValue::String: + data[key.toStdString()] = value.toString().toStdString(); + break; + case QJsonValue::Array: { + auto jsArray = value.toArray(); + switch ( jsArray.first().type() ) { + case QJsonValue::Double: { + std::vector array; + for ( auto v : jsArray ) { + array.push_back( Scalar( v.toDouble() ) ); + } + data[key.toStdString()] = array; + break; + } + default: + LOG( Ra::Core::Utils::logERROR ) + << "Only Scalar arrays are supported for Json conversion."; + } + } break; + default: + LOG( Ra::Core::Utils::logERROR ) << "QJson to nlohmann::json : QtJson value type " + << value.type() << " is not suported."; + } +} + +void NlohmannEntryToQJsonEntry( const nlohmann::json& data, QJsonValue& value ) { + switch ( data.type() ) { + case nlohmann::detail::value_t::boolean: { + auto v = data.get(); + value = v; + } break; + case nlohmann::detail::value_t::number_float: { + auto v = double( data.get() ); + value = v; + } break; + case nlohmann::detail::value_t::number_integer: { + auto v = data.get(); + value = v; + } break; + case nlohmann::detail::value_t::number_unsigned: { + auto v = data.get(); + value = int( v ); + } break; + case nlohmann::detail::value_t::string: { + auto v = data.get(); + value = v.c_str(); + } break; + case nlohmann::detail::value_t::array: { + QJsonArray a; + QJsonValue v; + for ( auto& x : data.items() ) { + NlohmannEntryToQJsonEntry( x.value(), v ); + a.append( v ); + } + value = a; + } break; + default: + LOG( Ra::Core::Utils::logERROR ) << "nlohmann::json to QJson : nlohmann json type " + << int( data.type() ) << " is not supported."; + } +} + +void QJsonObjectToNlohmannObject( const QJsonObject& p, nlohmann::json& data ) { + for ( const auto& key : p.keys() ) { + auto value = p.value( key ); + if ( value.isObject() ) { + nlohmann::json j; + QJsonObjectToNlohmannObject( value.toObject(), j ); + data[key.toStdString()] = j; + } + else { QJsonEntryToNlohmannEntry( key, value, data ); } + } +} + +void NlohmannObjectToQJsonObject( const nlohmann::json& data, QJsonObject& p ) { + for ( const auto& [key, value] : data.items() ) { + if ( value.is_object() ) { + QJsonObject o; + NlohmannObjectToQJsonObject( data[key], o ); + p.insert( key.c_str(), o ); + } + else { + QJsonValue v; + NlohmannEntryToQJsonEntry( value, v ); + p.insert( key.c_str(), v ); + } + } +} + +} // namespace NodeDataModelTools + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.hpp b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.hpp new file mode 100644 index 00000000000..e338775e402 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.hpp @@ -0,0 +1,87 @@ +#pragma once +#include + +#include + +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API NodeAdapterModel : public QtNodes::NodeDataModel +{ + public: + NodeAdapterModel( std::shared_ptr graph, std::shared_ptr n ); + NodeAdapterModel() = delete; + NodeAdapterModel( const NodeAdapterModel& ) = delete; + NodeAdapterModel( NodeAdapterModel&& ) = delete; + NodeAdapterModel& operator=( const NodeAdapterModel& ) = delete; + NodeAdapterModel& operator=( NodeAdapterModel&& ) = delete; + ~NodeAdapterModel() override; + + public: + QString caption() const override { return m_node->getTypeName().c_str(); } + + bool captionVisible() const override { return true; } + + QString name() const override { return m_node->getTypeName().c_str(); } + + QString uuid() const override { return m_uuid.toString(); } + + bool isDeletable() override { return true; } // Assume all nodes belong to the graph + + void updateState() override; + + void addMetaData( QJsonObject& json ) override; + + private: + QtNodes::NodeDataType IOToDataType( const std::string& typeName, + const std::string& ioName ) const; + + void checkConnections() const; + + public: + unsigned int nPorts( QtNodes::PortType portType ) const override; + + QtNodes::NodeDataType dataType( QtNodes::PortType portType, + QtNodes::PortIndex portIndex ) const override; + + std::shared_ptr outData( QtNodes::PortIndex port ) override; + + void setInData( std::shared_ptr data, int port ) override; + + QtNodes::NodeValidationState validationState() const override { return m_validationState; } + + QString validationMessage() const override { return m_validationError; } + + QWidget* embeddedWidget() override { return m_widget; } + + private: + std::shared_ptr m_node; + std::shared_ptr m_dataflowGraph { nullptr }; + + QWidget* m_widget { nullptr }; + + std::vector m_inputsConnected; + mutable QtNodes::NodeValidationState m_validationState = QtNodes::NodeValidationState::Valid; + mutable QString m_validationError = QString( "" ); + + QUuid m_uuid; + + public: + QJsonObject save() const override; + void restore( QJsonObject const& p ) override; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/WidgetFactory.cpp b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.cpp new file mode 100644 index 00000000000..cac3d862345 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.cpp @@ -0,0 +1,373 @@ +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; +using namespace Ra::Gui::Widgets; + +using namespace Ra::Engine::Data; + +namespace WidgetFactory { +using WidgetFunctionPair = std::pair; + +std::unordered_map widgetsfunctions; + +void registerWidgetInternal( std::type_index typeIdx, + WidgetCreatorFunc widgetCreator, + WidgetUpdaterFunc widgetUpdater ) { + if ( widgetsfunctions.find( typeIdx ) == widgetsfunctions.end() ) { + widgetsfunctions[typeIdx] = { std::move( widgetCreator ), std::move( widgetUpdater ) }; + } + else { + LOG( Ra::Core::Utils::logWARNING ) + << "WidgetFactory: trying to add an already existing widget builder for type " + << simplifiedDemangledType( typeIdx ) << "."; + } +} + +QWidget* createWidget( EditableParameterBase* editableParameter ) { + if ( widgetsfunctions.find( editableParameter->getType() ) != widgetsfunctions.end() ) { + return widgetsfunctions[editableParameter->getType()].first( editableParameter ); + } + else { + // TODO, when PR #1027 will be merged, demangle the type name + LOG( Ra::Core::Utils::logWARNING ) + << "WidgetFactory : no defined widget builder for type " + << simplifiedDemangledType( editableParameter->getType() ) << "."; + } + return nullptr; +} + +bool updateWidget( QWidget* widget, EditableParameterBase* editableParameter ) { + if ( widgetsfunctions.find( editableParameter->getType() ) != widgetsfunctions.end() ) { + return widgetsfunctions[editableParameter->getType()].second( widget, editableParameter ); + } + else { + LOG( Ra::Core::Utils::logWARNING ) + << "WidgetFactory: no defined widget updater for type " + << simplifiedDemangledType( editableParameter->getType() ) << "."; + } + return false; +} + +void initializeWidgetFactory() { + /* + * Environment map "edition" widget + */ + WidgetFactory::registerWidget>( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast>*>( + editableParameter ); + + auto controlPanel = new ControlPanel( editable->getName(), false ); + auto envmpClbck = [editable, controlPanel]( const std::string& files ) { + if ( files.empty() ) { editable->m_data = nullptr; } + else { + // for now, only skyboxes are managed + editable->m_data = std::make_shared( files, true ); + auto slider = controlPanel->findChild( "strength" ); + if ( slider ) { editable->m_data->setStrength( slider->value() / 100. ); } + } + }; + // TODO : display the name of the envmap image somewhere + controlPanel->addFileInput( + "files", envmpClbck, "../", "Images (*.png *.jpg *.pfm *.exr *hdr)" ); + + auto strengthClbk = [editable]( double v ) { + if ( editable->m_data ) { + auto* env = editable->m_data.get(); + if ( env ) { env->setStrength( v / 100. ); } + } + }; + float s_init = 100.; + if ( editable->m_data ) { s_init = editable->m_data->getStrength() * 100.; } + controlPanel->addPowerSliderInput( "strength", strengthClbk, s_init, 0., 100 ); + controlPanel->setVisible( true ); + return controlPanel; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast>*>( + editableParameter ); + auto slider = widget->findChild( "strength" ); + if ( slider && editable->m_data ) { + slider->setValue( editable->m_data->getStrength() * 100. ); + return true; + } + else { return slider != nullptr; } + } ); + + /* + * Scalar edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto powerSlider = new PowerSlider(); + powerSlider->setObjectName( editable->getName().c_str() ); + // editable->m_data = 0.0_ra; + powerSlider->setValue( editable->m_data ); + const auto& constraints = editable->getConstraints(); + Scalar minValue = 0_ra; + Scalar maxValue = 1000_ra; + if ( constraints.contains( "min" ) ) { minValue = Scalar( constraints["min"] ); } + if ( constraints.contains( "max" ) ) { maxValue = Scalar( constraints["max"] ); } + powerSlider->setRange( minValue, maxValue ); + PowerSlider::connect( powerSlider, + &PowerSlider::valueChanged, + [editable]( Scalar value ) { editable->m_data = value; } ); + return powerSlider; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto slider = widget->findChild( editableParameter->getName().c_str() ); + if ( slider ) { + slider->setValue( editable->m_data ); + return true; + } + else { return false; } + } ); + /* + * int edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + const auto& constraints = editable->getConstraints(); + Scalar minValue = 0_ra; + Scalar maxValue = 1000_ra; + if ( constraints.contains( "min" ) ) { minValue = Scalar( constraints["min"] ); } + if ( constraints.contains( "max" ) ) { maxValue = Scalar( constraints["max"] ); } + + auto powerSlider = new PowerSlider(); + powerSlider->setObjectName( editable->getName().c_str() ); + // editable->m_data = 0.0_ra; + powerSlider->setValue( editable->m_data ); + powerSlider->setRange( minValue, maxValue ); + powerSlider->setSingleStep( 1 ); + PowerSlider::connect( powerSlider, + &PowerSlider::valueChanged, + [editable]( Scalar value ) { editable->m_data = int( value ); } ); + return powerSlider; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto slider = widget->findChild( editableParameter->getName().c_str() ); + if ( slider ) { + slider->setValue( editable->m_data ); + return true; + } + else { return false; } + } ); + /* + * Boolean edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto checkBox = new QCheckBox(); + checkBox->setObjectName( editable->getName().c_str() ); + checkBox->setCheckState( editable->m_data ? Qt::CheckState::Checked + : Qt::CheckState::Unchecked ); + QCheckBox::connect( checkBox, &QCheckBox::stateChanged, [editable]( int state ) { + if ( state == Qt::Unchecked ) { editable->m_data = false; } + else if ( state == Qt::Checked ) { editable->m_data = true; } + } ); + return checkBox; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto checkBox = widget->findChild( editableParameter->getName().c_str() ); + if ( checkBox ) { + checkBox->setCheckState( editable->m_data ? Qt::Checked : Qt::Unchecked ); + return true; + } + else { return false; } + } ); + + /* + * Color edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = + dynamic_cast*>( editableParameter ); + auto controlPanel = new ControlPanel( editable->getName(), false ); + auto clrCbk = [editable]( const Ra::Core::Utils::Color& clr ) { + editable->m_data = clr; + }; + controlPanel->addColorInput( editable->getName(), clrCbk, editable->m_data, true ); + controlPanel->setVisible( true ); + return controlPanel; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = + dynamic_cast*>( editableParameter ); + auto button = widget->findChild( editable->getName().c_str() ); + if ( button ) { + // todo, update the color on the button and make the dialog to take its + // initial color from the button. Once dont, return true ... + auto srgbColor = Ra::Core::Utils::Color::linearRGBTosRGB( editable->m_data ); + auto clrBttn = + QColor::fromRgbF( srgbColor[0], srgbColor[1], srgbColor[2], srgbColor[3] ); + + auto lum = 0.2126_ra * Scalar( clrBttn.redF() ) + + 0.7151_ra * Scalar( clrBttn.greenF() ) + + 0.0721_ra * Scalar( clrBttn.blueF() ); + QString qss = QString( "background-color: %1" ).arg( clrBttn.name() ); + if ( lum > 1_ra / 3_ra ) { qss += QString( "; color: #000000" ); } + else { qss += QString( "; color: #FFFFFF" ); } + button->setStyleSheet( qss ); + return true; + } + LOG( Ra::Core::Utils::logWARNING ) + << " Unable to find the button \"Choose color\" for \"" << editable->getName() + << "\" "; + return false; + } ); + + /* + * Button edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto button = new QPushButton( "Show graph" ); + button->setObjectName( editableParameter->getName().c_str() ); + QPushButton::connect( button, &QPushButton::clicked, [editable]() { + // Display a window to see the graph + /*auto graph = reinterpret_cast( editable ); + auto nodeEditor = new GraphEditorView( nullptr ); + + nodeEditor->editGraph( nullptr ); + nodeEditor->editGraph( graph ); + + nodeEditor->resize( 900, 500 ); + nodeEditor->move( 450, 260 ); + nodeEditor->show(); */ + } ); + + return button; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto button = widget->findChild( editable->getName().c_str() ); + if ( button ) { return true; } + else { return false; } + } ); + +#if HAS_TRANSFER_FUNCTION + /* + * Transfer function + */ + WidgetFactory::registerWidget>( + []( EditableParameterBase* editableParameter ) { + auto editableTransferFunction = + dynamic_cast>*>( editableParameter ); + // TODO, give a name to the widget so that they can be updated automatically when + // loading a node + auto button = new QPushButton( "Open widget" ); + auto transferEditor = new TransferEditor(); + TransferEditor::connect( + transferEditor, + &TransferEditor::propertiesChanged, + [editableTransferFunction, transferEditor]() { + int pos = 0; + for ( int i = 0; i < 256; i++ ) { + unsigned int color = transferEditor->colorAt( i ); + editableTransferFunction->m_data.at( pos ) = + (unsigned char)( ( 0x00ff0000 & color ) >> 16 ) / 255.f; + editableTransferFunction->m_data.at( pos + 1 ) = + (unsigned char)( ( 0x0000ff00 & color ) >> 8 ) / 255.f; + editableTransferFunction->m_data.at( pos + 2 ) = + (unsigned char)( ( 0x000000ff & color ) ) / 255.f; + editableTransferFunction->m_data.at( pos + 3 ) = + (unsigned char)( ( 0xff000000 & color ) >> 24 ) / 255.f; + pos = pos + 4; + } + } ); + QPushButton::connect( + button, &QPushButton::clicked, [transferEditor]() { transferEditor->show(); } ); + return button; + }, + []( QWidget*, EditableParameterBase* ) -> bool { return false; } ); +#endif + /* + * String enumerator + */ + WidgetFactory::registerWidget>( + []( EditableParameterBase* editableParameter ) { + auto editable = + dynamic_cast>*>( editableParameter ); + auto selector = new QComboBox(); + selector->setObjectName( editable->getName().c_str() ); + for ( const auto& e : editable->m_data ) { + selector->addItem( e.c_str() ); + } + QComboBox::connect( + selector, &QComboBox::currentTextChanged, [editable]( const QString& string ) { + editable->m_data.set( string.toStdString() ); + } ); + return selector; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = + dynamic_cast>*>( editableParameter ); + auto comboBox = widget->findChild( editable->getName().c_str() ); + if ( comboBox ) { + comboBox->setCurrentText( editable->m_data.get().c_str() ); + return true; + } + else { return false; } + } ); + + /* + * String + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto line = new QLineEdit(); + line->setObjectName( editable->getName().c_str() ); + QLineEdit::connect( line, &QLineEdit::textEdited, [editable]( const QString& string ) { + editable->m_data = string.toStdString(); + } ); + return line; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto line = widget->findChild( editable->getName().c_str() ); + if ( line ) { + line->setText( editable->m_data.c_str() ); + return true; + } + else { return false; } + } ); + /* + * Ra::Engine::Data::ShaderConfiguration --> Code Editor + */ +} + +} // namespace WidgetFactory + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/WidgetFactory.hpp b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.hpp new file mode 100644 index 00000000000..e6ad3d52668 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.hpp @@ -0,0 +1,82 @@ +#pragma once +#include + +#include + +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { +using namespace Ra::Dataflow::Core; + +// TODO : instead of a namespace, make it a class ? +/** + * Set of functions to manage automatic widget creation and update for editable parameters in the Qt + * Node Editor. + * \todo use Ra::Gui::ParameterSetEditor ? + */ +namespace WidgetFactory { +/** Type of the function that creates a widget. + * @param the editable parameter that defines the widget content + * @return the created widget + */ +using WidgetCreatorFunc = std::function; +/** Type of the function that creates a widget. + * @param the widget whose content must be updated + * @param the editable parameter that defines the widget content + * @return true if update is done, false if not + */ +using WidgetUpdaterFunc = std::function; + +/** private method to manage the factory + */ +RA_DATAFLOW_API void registerWidgetInternal( std::type_index typeIdx, + WidgetCreatorFunc widgetCreator, + WidgetUpdaterFunc widgetUpdater ); + +/** Register a widget builder and updater in the factory given the type of the editable parameter + * @tparam T The concrete type of the editable parameter + * @param widgetCreator a function that build a widget to edit the given parameter. + * @param widgetUpdater a function to update the widget state according to the given parameter. + */ +template +void registerWidget( WidgetCreatorFunc widgetCreator, WidgetUpdaterFunc widgetUpdater ) { + registerWidgetInternal( typeid( T ), std::move( widgetCreator ), std::move( widgetUpdater ) ); +} + +/** + * Create a widget from an editable parameter using the widget factory + * @param editableParameter the data whose type will define the widget + * @return the created widget, nullptr if no widget creator is associated with the editable + * parameter type. + */ +RA_DATAFLOW_API QWidget* createWidget( EditableParameterBase* editableParameter ); + +/** + * Update a widget from an editable parameter using the widget factory + * + * @param widget the widget to update + * @param editableParameter the data whose content will be transfered to the widget + * @return true if the update is done, false if thereis no updater associated with the + * editableParrameter type. + */ +RA_DATAFLOW_API bool updateWidget( QWidget* widget, EditableParameterBase* editableParameter ); + +/** + * Initialize the factory with pre-defined widgets according to the Radium NodeGraph predefined + * nodes. + */ +RA_DATAFLOW_API void initializeWidgetFactory(); + +} // namespace WidgetFactory + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/images/copy.png b/src/Dataflow/QtGui/GraphEditor/images/copy.png new file mode 100644 index 00000000000..2aeb28288f5 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/copy.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/cut.png b/src/Dataflow/QtGui/GraphEditor/images/cut.png new file mode 100644 index 00000000000..54638e9386d Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/cut.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/new.png b/src/Dataflow/QtGui/GraphEditor/images/new.png new file mode 100644 index 00000000000..12131b01008 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/new.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/open.png b/src/Dataflow/QtGui/GraphEditor/images/open.png new file mode 100644 index 00000000000..45fa2883a71 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/open.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/paste.png b/src/Dataflow/QtGui/GraphEditor/images/paste.png new file mode 100644 index 00000000000..c14425cad1f Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/paste.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/save.png b/src/Dataflow/QtGui/GraphEditor/images/save.png new file mode 100644 index 00000000000..e65a29d5f17 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/save.png differ diff --git a/src/Dataflow/QtGui/filelist.cmake b/src/Dataflow/QtGui/filelist.cmake new file mode 100644 index 00000000000..ac4590ae925 --- /dev/null +++ b/src/Dataflow/QtGui/filelist.cmake @@ -0,0 +1,15 @@ +# ---------------------------------------------------- +# This file should not be generated by the radium script +# ---------------------------------------------------- + +set(dataflow_qtgui_sources GraphEditor/GraphEditorView.cpp GraphEditor/NodeAdapterModel.cpp + GraphEditor/GraphEditorWindow.cpp GraphEditor/WidgetFactory.cpp +) + +set(dataflow_qtgui_headers + GraphEditor/ConnectionStatusData.hpp GraphEditor/GraphEditorView.hpp + GraphEditor/GraphEditorWindow.hpp GraphEditor/NodeAdapterModel.hpp + GraphEditor/WidgetFactory.hpp +) + +set(dataflow_qtgui_resources GraphEditor/GraphEditor.qrc) diff --git a/src/Dataflow/QtGui/pch.hpp b/src/Dataflow/QtGui/pch.hpp new file mode 100644 index 00000000000..6f70f09beec --- /dev/null +++ b/src/Dataflow/QtGui/pch.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/Dataflow/RaDataflow.hpp b/src/Dataflow/RaDataflow.hpp new file mode 100644 index 00000000000..046321b7ce9 --- /dev/null +++ b/src/Dataflow/RaDataflow.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +/// Defines the correct macro to export dll symbols. +#if defined RA_DATAFLOW_EXPORTS +# define RA_DATAFLOW_API DLL_EXPORT +#elif defined RA_DATAFLOW_STATIC +# define RA_DATAFLOW_API +#else +# define RA_DATAFLOW_API DLL_IMPORT +#endif + +/// Allow to define initializers for modules that need to be initialized transparently +#define DATAFLOW_LIBRARY_INITIALIZER_DECL( f ) void f##__Initializer() + +#define DATAFLOW_LIBRARY_INITIALIZER_IMPL( f ) \ + struct f##__Initializer_t_ { \ + f##__Initializer_t_() { ::f##__Initializer(); } \ + }; \ + static f##__Initializer_t_ f##__Initializer__; \ + void f##__Initializer() diff --git a/src/Dataflow/Rendering/CMakeLists.txt b/src/Dataflow/Rendering/CMakeLists.txt new file mode 100644 index 00000000000..c5e2b13f0e1 --- /dev/null +++ b/src/Dataflow/Rendering/CMakeLists.txt @@ -0,0 +1,41 @@ +set(ra_dataflowrendering_target DataflowRendering) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflowrendering_target}] ") + +project(${ra_dataflowrendering_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +include(filelist.cmake) + +# configure library +add_library( + ${ra_dataflowrendering_target} SHARED ${dataflow_rendering_sources} + ${dataflow_rendering_headers} +) + +# This one should be extracted directly from parent project properties. +target_compile_definitions(${ra_dataflowrendering_target} PRIVATE RA_DATAFLOW_EXPORTS) + +target_compile_options(${ra_dataflowrendering_target} PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) + +add_dependencies(${ra_dataflowrendering_target} DataflowCore Engine) +target_link_libraries(${ra_dataflowrendering_target} PUBLIC DataflowCore Engine) + +message(STATUS "Configuring library ${ra_dataflowrendering_target} with standard settings") +configure_radium_target(${ra_dataflowrendering_target}) +# configure the library only. The package is a sub-package and should be configured independently +configure_radium_library( + TARGET ${ra_dataflowrendering_target} COMPONENT TARGET_DIR "Dataflow/Rendering" + FILES "${dataflow_rendering_headers}" +) +# Generate cmake package +configure_radium_package( + NAME ${ra_dataflowrendering_target} PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + PACKAGE_DIR "lib/cmake/Radium/${ra_dataflowrendering_target}" NAME_PREFIX "Radium" +) + +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflowrendering_target} PARENT_SCOPE) + +if(RADIUM_ENABLE_PCH) + target_precompile_headers(${ra_dataflowrendering_target} PRIVATE pch.hpp) +endif() + +list(REMOVE_AT CMAKE_MESSAGE_INDENT -1) diff --git a/src/Dataflow/Rendering/Config.cmake.in b/src/Dataflow/Rendering/Config.cmake.in new file mode 100644 index 00000000000..23bd179f1ab --- /dev/null +++ b/src/Dataflow/Rendering/Config.cmake.in @@ -0,0 +1,38 @@ +# -------------- Configuration of the Radium DataflowCore targets and definitions ----------------------- +# Setup Engine and check for dependencies + +if (DataflowRendering_FOUND AND NOT TARGET DataflowRendering) + set(Configure_DataflowRendering ON) + # verify dependencies + if(NOT DataflowCore_FOUND) + # if in source dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake) + else() + # if in install dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowQtGui: dependency DataflowCore not found") + set(Configure_DataflowRendering OFF) + endif() + endif() + endif() + if(NOT Engine_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Engine/RadiumEngineConfig.cmake") + set(Gui_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Engine/RadiumEngineConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowRendering: dependency Engine not found") + set(Configure_DataflowRendering OFF) + endif() + endif() +endif() + +if(Configure_DataflowRendering) + include("${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Rendering/DataflowRenderingTargets.cmake") +endif() diff --git a/src/Dataflow/Rendering/Nodes/RenderingNode.hpp b/src/Dataflow/Rendering/Nodes/RenderingNode.hpp new file mode 100644 index 00000000000..9c893c8cfc9 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderingNode.hpp @@ -0,0 +1,75 @@ +#pragma once +#include + +#include + +#include +#include +#include +#include +#include + +namespace Ra::Engine::Data { +class ShaderProgramManager; +} + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +/** + * Defining some useful aliases for data type + * + */ + +using RenderObjectType = std::shared_ptr; +using LightType = const Ra::Engine::Scene::Light*; +using CameraType = Ra::Engine::Data::ViewingParameters; +using ColorType = Ra::Core::Utils::Color; +using TextureType = Ra::Engine::Data::Texture; + +/** + * Base class for Rendering nodes. + * Rendering nodes are nodes with some interface needed to render the scene. + */ +class RA_DATAFLOW_API RenderingNode : public Dataflow::Core::Node, + public Ra::Core::Utils::IndexedObject +{ + public: + using Dataflow::Core::Node::Node; + + /// The resize(uint32_t width, uint32_t height) function is called when the application gets + /// resized. Its goal is to resize the potential internal textures if needed. + /// @param width The new width of the surface. + /// @param height The new height of the surface. + virtual void resize( uint32_t width, uint32_t height ) = 0; + + /// Build a render technic per material. + /// @param ro The render object to get the material from + /// @param rt The render technic to build + virtual void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject*, + Ra::Engine::Rendering::RenderTechnique& ) const {}; + + /// Indicate if the nod needs to setup a rendertechnique on RenderObjects + virtual bool hasRenderTechnique() { return false; } + + /// Sets the shader program manager + void setShaderProgramManager( Ra::Engine::Data::ShaderProgramManager* shaderMngr ) { + m_shaderMngr = shaderMngr; + } + + static const std::string getTypename() { return "RenderingNode"; } + + protected: + void toJsonInternal( nlohmann::json& ) const override {} + void fromJsonInternal( const nlohmann::json& ) override {} + + /// The renderer's shader program manager + Ra::Engine::Data::ShaderProgramManager* m_shaderMngr; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Renderer/DataflowRenderer.cpp b/src/Dataflow/Rendering/Renderer/DataflowRenderer.cpp new file mode 100644 index 00000000000..87957cfac71 --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/DataflowRenderer.cpp @@ -0,0 +1,636 @@ +#include + +#include +#include +#include + +#include + +#include +#include + +#include + +using namespace Ra::Engine; +using namespace Ra::Engine::Scene; +using namespace Ra::Engine::Data; +using namespace Ra::Engine::Rendering; +using namespace gl; + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { +using namespace Ra::Dataflow::Core; + +DataflowRenderer::RenderGraphController::RenderGraphController() : + Ra::Core::Resources::ObservableVoid() {} + +void DataflowRenderer::RenderGraphController::configure( DataflowRenderer* renderer, + int w, + int h ) { + m_shaderMngr = renderer->m_shaderProgramManager; + m_width = w; + m_height = h; + if ( !m_graphToLoad.empty() ) { + loadGraph( m_graphToLoad ); + m_graphToLoad = ""; + } +} + +void DataflowRenderer::RenderGraphController::resize( int w, int h ) { + m_width = w; + m_height = h; + if ( m_renderGraph ) { m_renderGraph->resize( m_width, m_height ); } +} + +void DataflowRenderer::RenderGraphController::update( const Ra::Engine::Data::ViewingParameters& ) { + + if ( m_renderGraph && m_renderGraph->m_recompile ) { + // compile the model + m_renderGraph->init(); + // notify the view the model changes + notify(); + // notify the model the view may have changed + m_renderGraph->resize( m_width, m_height ); + } +} + +void DataflowRenderer::RenderGraphController::loadGraph( const std::string filename ) { + m_renderGraph.release(); + auto graphName = filename.substr( filename.find_last_of( '/' ) + 1 ); + m_renderGraph = std::make_unique( graphName ); + m_renderGraph->setShaderProgramManager( m_shaderMngr ); + m_renderGraph->loadFromJson( filename ); + notify(); +} + +void DataflowRenderer::RenderGraphController::defferedLoadGraph( const std::string filename ) { + m_graphToLoad = filename; +} + +void DataflowRenderer::RenderGraphController::saveGraph( const std::string filename ) { + if ( m_renderGraph ) { + auto graphName = filename.substr( filename.find_last_of( '/' ) + 1 ); + m_renderGraph->saveToJson( filename ); + m_renderGraph->setInstanceName( graphName ); + } +} + +void DataflowRenderer::RenderGraphController::resetGraph() { + m_renderGraph.release(); + m_renderGraph = std::make_unique( "untitled" ); + m_renderGraph->setShaderProgramManager( m_shaderMngr ); +} + +// ------------------------------------------------------------------------------------------ +// Interface with Radium renderer ... +// ------------------------------------------------------------------------------------------ + +DataflowRenderer::DataflowRenderer( RenderGraphController& controller ) : + Renderer(), m_controller { controller }, m_name { m_controller.getRendererName() } { + m_controller.attachMember( this, &DataflowRenderer::graphChanged ); +} + +DataflowRenderer::~DataflowRenderer() = default; + +void DataflowRenderer::graphChanged() { + m_graphChanged = true; +} + +bool DataflowRenderer::buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const { + if ( m_controller.m_renderGraph ) { + if ( m_controller.m_renderGraph->m_recompile ) { + m_controller.m_renderGraph->init(); + m_controller.resize( m_width, m_height ); + } + m_controller.m_renderGraph->buildRenderTechnique( ro ); + return true; + } + else { return false; } +} + +void DataflowRenderer::initResources() { + // uses several resources from the Radium engine + auto resourcesRootDir { RadiumEngine::getInstance()->getResourcesDir() + "Shaders/" }; + + m_shaderProgramManager->addShaderProgram( + { { "Hdr2Ldr" }, + resourcesRootDir + "2DShaders/Basic2D.vert.glsl", + resourcesRootDir + "2DShaders/Hdr2Ldr.frag.glsl" } ); + + m_postprocessFbo = std::make_unique(); +} + +void DataflowRenderer::initializeInternal() { + auto cmngr = dynamic_cast( + Ra::Engine::RadiumEngine::getInstance()->getSystem( "DefaultCameraManager" ) ); + if ( cmngr == nullptr ) { + cmngr = new DefaultCameraManager(); + Ra::Engine::RadiumEngine::getInstance()->registerSystem( "DefaultCameraManager", cmngr ); + } + auto lmngr = dynamic_cast( + Ra::Engine::RadiumEngine::getInstance()->getSystem( "DefaultLightManager" ) ); + if ( lmngr == nullptr ) { + lmngr = new DefaultLightManager(); + Ra::Engine::RadiumEngine::getInstance()->registerSystem( "DefaultLightManager", lmngr ); + } + m_lightmanagers.push_back( lmngr ); + + // Initialize renderer resources + initResources(); + m_controller.configure( this, m_width, m_height ); + + // TODO update shared textures as the rengerGraphe modify its output : observe the renderGraph + for ( const auto& t : m_sharedTextures ) { + m_secondaryTextures.insert( { t.first, t.second.get() } ); + } +} + +void DataflowRenderer::resizeInternal() { + // Resize the graph resources + m_controller.resize( m_width, m_height ); + + // Resize the internal resources + m_postprocessFbo->bind(); + m_postprocessFbo->attachTexture( GL_COLOR_ATTACHMENT0, m_fancyTexture->texture() ); +#ifdef PASSES_LOG + if ( m_postprocessFbo->checkStatus() != GL_FRAMEBUFFER_COMPLETE ) { + LOG( Ra::Core::Utils::logERROR ) << "FBO Error (NodeBasedRenderer::m_postprocessFbo) : " + << m_postprocessFbo->statusString(); + } +#endif + // finished with fbo, unbind to bind default + globjects::Framebuffer::unbind(); +} + +void DataflowRenderer::updateStepInternal( const Ra::Engine::Data::ViewingParameters& renderData ) { + // std::cout << "DataflowRenderer::updateStepInternal() : calling update on graph controller." + // << std::endl; + m_controller.update( renderData ); + if ( m_controller.m_renderGraph ) { + // Update renderTechnique if needed + if ( m_graphChanged ) { + buildAllRenderTechniques(); + m_graphChanged = false; + } + // TODO, improve light and camera management to prevent multiple alloc/copy ... + auto lights = getLights(); + lights->clear(); + lights->reserve( getLightManager()->count() ); + for ( size_t i = 0; i < getLightManager()->count(); i++ ) { + lights->push_back( getLightManager()->getLight( i ) ); + } + // The graph will take ownership of the light pointer ... + m_controller.m_renderGraph->setDataSources( allRenderObjects(), lights ); + } +} + +void DataflowRenderer::renderInternal( const Ra::Engine::Data::ViewingParameters& renderData ) { + // std::cout << "DataflowRenderer::renderInternal() : executing the graph." << std::endl; + // TODO, replace this kind of test by a call to a controller method + if ( m_controller.m_renderGraph && m_controller.m_renderGraph->m_ready ) { + // Cameras + // set input data + m_cameras.clear(); + m_cameras.push_back( renderData ); + m_controller.m_renderGraph->setCameras( &m_cameras ); + // execute the graph + m_controller.m_renderGraph->execute(); + // TODO : get all the resulting images (not only the "Beauty" channel + const auto& images = m_controller.m_renderGraph->getImagesOutput(); + // The first image is the "beauty" channel, set the color texture to this + m_colorTexture = images[0]; + } + else { m_colorTexture = nullptr; } +} + +void DataflowRenderer::postProcessInternal( const Ra::Engine::Data::ViewingParameters& ) { + if ( m_colorTexture ) { + m_postprocessFbo->bind(); + + // GL_ASSERT( glDrawBuffers( 1, buffers ) ); + GL_ASSERT( glDrawBuffer( GL_COLOR_ATTACHMENT0 ) ); + GL_ASSERT( glDisable( GL_DEPTH_TEST ) ); + GL_ASSERT( glDepthMask( GL_FALSE ) ); + + auto shader = m_postProcessEnabled + ? m_shaderProgramManager->getShaderProgram( "Hdr2Ldr" ) + : m_shaderProgramManager->getShaderProgram( "DrawScreen" ); + shader->bind(); + shader->setUniform( "screenTexture", m_colorTexture, 0 ); + m_quadMesh->render( shader ); + + GL_ASSERT( glDepthMask( GL_TRUE ) ); + GL_ASSERT( glEnable( GL_DEPTH_TEST ) ); + + m_postprocessFbo->unbind(); + } +} + +void DataflowRenderer::debugInternal( const Ra::Engine::Data::ViewingParameters& ) {} + +void DataflowRenderer::uiInternal( const Ra::Engine::Data::ViewingParameters& ) {} + +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra + +#if 0 +# include + +# include + +# ifdef PASSES_LOG +# include +using namespace Ra::Core::Utils; // log +# endif + +# include +# include +# include + +# include +# include +# include + +# include + +# include +# include + +using namespace Ra::Engine; +using namespace Ra::Engine::Scene; +using namespace Ra::Engine::Data; +using namespace Ra::Engine::Rendering; +namespace RadiumNBR { +using namespace gl; + +int NodeBasedRendererMagic = 0xFF0F00F0; + +static const GLenum buffers[] = { GL_COLOR_ATTACHMENT0 }; + +static NodeBasedRenderer::RenderControlFunctor noOpController; + +NodeBasedRenderer::NodeBasedRenderer() : Renderer(), m_controller{ noOpController } {} + +NodeBasedRenderer::NodeBasedRenderer( NodeBasedRenderer::RenderControlFunctor& controller ) : + Renderer(), m_controller{ controller }, m_name{ m_controller.getRendererName() } { + setDisplayNode( m_originalRenderGraph.getDisplayNode() ); +} + +NodeBasedRenderer::~NodeBasedRenderer() { + m_displaySinkNode->detach( m_displayObserverId ); + m_originalRenderGraph.destroy(); +} + +bool NodeBasedRenderer::buildRenderTechnique( RenderObject* ro ) const { + auto rt = Ra::Core::make_shared(); + for ( size_t level = 0; level < m_originalRenderGraph.getNodesByLevel()->size(); level++ ) + { + for ( size_t node = 0; node < m_originalRenderGraph.getNodesByLevel()->at( level ).size(); + node++ ) + { + m_originalRenderGraph.getNodesByLevel()->at( level ).at( node )->buildRenderTechnique( + ro, *rt ); + } + } + rt->updateGL(); + ro->setRenderTechnique( rt ); + return true; +} + +void NodeBasedRenderer::initResources() { + // uses several resources from the Radium engine + auto resourcesRootDir{ RadiumEngine::getInstance()->getResourcesDir() + "Shaders/" }; + + m_shaderProgramManager->addShaderProgram( + { { "Hdr2Ldr" }, + resourcesRootDir + "2DShaders/Basic2D.vert.glsl", + resourcesRootDir + "2DShaders/Hdr2Ldr.frag.glsl" } ); + + m_postprocessFbo = std::make_unique(); +} + +void NodeBasedRenderer::loadFromJson( const std::string& jsonFilePath ) { + m_jsonFilePath = jsonFilePath; + + if ( m_jsonFilePath != "" ) + { + m_originalRenderGraph.loadFromJson( jsonFilePath ); + m_originalRenderGraph.init(); + } + else + { std::cerr << "No Json was given to load a render graph." << std::endl; } +} + +void NodeBasedRenderer::compileRenderGraph() { + m_originalRenderGraph.init(); + m_originalRenderGraph.resize( m_width, m_height ); + buildAllRenderTechniques(); + m_displayedTexture = m_fancyTexture.get(); +} + +void NodeBasedRenderer::reloadRenderGraphFromJson() { + if ( m_jsonFilePath != "" ) + { + std::cout << "Reloading Render Graph from Json..." << std::endl; + // Destroy the resources used by the nodes + m_originalRenderGraph.destroy(); + + // Clear the nodes + m_originalRenderGraph.clearNodes(); + + // Reload + m_originalRenderGraph.loadFromJson( m_jsonFilePath ); + m_originalRenderGraph.init(); + m_originalRenderGraph.resize( m_width, m_height ); + buildAllRenderTechniques(); + + // Reset displayed texture + m_displayedTexture = m_fancyTexture.get(); + + std::cout << "Render Graph Reloaded!" << std::endl; + } + else + { std::cerr << "No Json was given to reload a render graph." << std::endl; } +} + +void NodeBasedRenderer::initializeInternal() { + + // TODO : this must be done only once, see register system ... + auto cmngr = dynamic_cast( + Ra::Engine::RadiumEngine::getInstance()->getSystem( "DefaultCameraManager" ) ); + if ( cmngr == nullptr ) + { + cmngr = new DefaultCameraManager(); + Ra::Engine::RadiumEngine::getInstance()->registerSystem( "DefaultCameraManager", cmngr ); + } + auto lmngr = dynamic_cast( + Ra::Engine::RadiumEngine::getInstance()->getSystem( "DefaultLightManager" ) ); + if ( lmngr == nullptr ) + { + lmngr = new DefaultLightManager(); + Ra::Engine::RadiumEngine::getInstance()->registerSystem( "DefaultLightManager", lmngr ); + } + m_lightmanagers.push_back( lmngr ); + + // Initialize renderer resources + initResources(); + m_controller.configure( this, m_width, m_height ); + + for ( const auto& t : m_sharedTextures ) + { m_secondaryTextures.insert( { t.first, t.second.get() } ); } + + // Todo cache this in an attribute ? + auto resourcesCheck = Ra::Core::Resources::getResourcesPath( + reinterpret_cast( &RadiumNBR::NodeBasedRendererMagic ), { "Resources/RadiumNBR" } ); + if ( !resourcesCheck ) + { + LOG( Ra::Core::Utils::logERROR ) << "Unable to find resources for NodeBasedRenderer!"; + return; + } + auto resourcesPath{ *resourcesCheck }; +} + +void NodeBasedRenderer::resizeInternal() { + // Resize each internal resources + m_controller.resize( m_width, m_height ); + m_originalRenderGraph.resize( m_width, m_height ); + + m_postprocessFbo->bind(); + m_postprocessFbo->attachTexture( GL_COLOR_ATTACHMENT0, m_fancyTexture->texture() ); +# ifdef PASSES_LOG + if ( m_postprocessFbo->checkStatus() != GL_FRAMEBUFFER_COMPLETE ) + { + LOG( Ra::Core::Utils::logERROR ) << "FBO Error (NodeBasedRenderer::m_postprocessFbo) : " + << m_postprocessFbo->checkStatus(); + } +# endif + // finished with fbo, unbind to bind default + globjects::Framebuffer::unbind(); +} + +void NodeBasedRenderer::renderInternal( const ViewingParameters& renderData ) { + // Run the render graph + m_originalRenderGraph.execute(); +} + +// Draw debug stuff, do not overwrite depth map but do depth testing +void NodeBasedRenderer::debugInternal( const ViewingParameters& renderData ) { +# if 0 + if ( m_drawDebug ) + { + const ShaderProgram* shader; + + m_postprocessFbo->bind(); + GL_ASSERT( glDisable( GL_BLEND ) ); + GL_ASSERT( glEnable( GL_DEPTH_TEST ) ); + GL_ASSERT( glDepthMask( GL_FALSE ) ); + GL_ASSERT( glDepthFunc( GL_LESS ) ); + + glDrawBuffers( 1, buffers ); + + for ( const auto& ro : m_debugRenderObjects ) + { + ro->render( RenderParameters{}, renderData ); + } + + DebugRender::getInstance()->render( renderData.viewMatrix, renderData.projMatrix ); + + m_postprocessFbo->unbind(); + + m_uiXrayFbo->bind(); + // Draw X rayed objects always on top of normal objects + GL_ASSERT( glDepthMask( GL_TRUE ) ); + GL_ASSERT( glClear( GL_DEPTH_BUFFER_BIT ) ); + for ( const auto& ro : m_xrayRenderObjects ) + { + if ( ro->isVisible() ) + { + shader = ro->getRenderTechnique()->getShader(); + + // bind data + shader->bind(); + // lighting for Xray : fixed + shader->setUniform( "light.color", Ra::Core::Utils::Color::Grey( 5.0 ) ); + shader->setUniform( "light.type", Light::LightType::DIRECTIONAL ); + shader->setUniform( "light.directional.direction", Ra::Core::Vector3( 0, -1, 0 ) ); + + Ra::Core::Matrix4 M = ro->getTransformAsMatrix(); + shader->setUniform( "transform.proj", renderData.projMatrix ); + shader->setUniform( "transform.view", renderData.viewMatrix ); + shader->setUniform( "transform.model", M ); + + ro->getRenderTechnique()->getMaterial()->bind( shader ); + + // render + ro->getMesh()->render(); + } + } + m_uiXrayFbo->unbind(); + } +# endif +} + +// Draw UI stuff, always drawn on top of everything else + clear ZMask +// TODO: NODEGRAPH! Unused ? +void NodeBasedRenderer::uiInternal( const ViewingParameters& renderData ) { +# if 0 + const ShaderProgram* shader; + + m_uiXrayFbo->bind(); + glDrawBuffers( 1, buffers ); + // Enable z-test + GL_ASSERT( glDepthMask( GL_TRUE ) ); + GL_ASSERT( glEnable( GL_DEPTH_TEST ) ); + GL_ASSERT( glDepthFunc( GL_LESS ) ); + GL_ASSERT( glClear( GL_DEPTH_BUFFER_BIT ) ); + for ( const auto& ro : m_uiRenderObjects ) + { + if ( ro->isVisible() ) + { + shader = ro->getRenderTechnique()->getShader(); + + // bind data + shader->bind(); + + Ra::Core::Matrix4 M = ro->getTransformAsMatrix(); + Ra::Core::Matrix4 MV = renderData.viewMatrix * M; + Ra::Core::Vector3 V = MV.block<3, 1>( 0, 3 ); + Scalar d = V.norm(); + + Ra::Core::Matrix4 S = Ra::Core::Matrix4::Identity(); + S.coeffRef( 0, 0 ) = S.coeffRef( 1, 1 ) = S.coeffRef( 2, 2 ) = d; + + M = M * S; + + shader->setUniform( "transform.proj", renderData.projMatrix ); + shader->setUniform( "transform.view", renderData.viewMatrix ); + shader->setUniform( "transform.model", M ); + + ro->getRenderTechnique()->getMaterial()->bind( shader ); + + // render + ro->getMesh()->render(); + } + } + m_uiXrayFbo->unbind(); +# endif +} + +void NodeBasedRenderer::postProcessInternal( const ViewingParameters& /* renderData */ ) { + if ( m_colorTexture ) + { + m_postprocessFbo->bind(); + + // GL_ASSERT( glDrawBuffers( 1, buffers ) ); + GL_ASSERT( glDrawBuffer( GL_COLOR_ATTACHMENT0 ) ); + GL_ASSERT( glDisable( GL_DEPTH_TEST ) ); + GL_ASSERT( glDepthMask( GL_FALSE ) ); + + auto shader = m_postProcessEnabled + ? m_shaderProgramManager->getShaderProgram( "Hdr2Ldr" ) + : m_shaderProgramManager->getShaderProgram( "DrawScreen" ); + shader->bind(); + shader->setUniform( "screenTexture", m_colorTexture, 0 ); + m_quadMesh->render( shader ); + + GL_ASSERT( glDepthMask( GL_TRUE ) ); + GL_ASSERT( glEnable( GL_DEPTH_TEST ) ); + + m_postprocessFbo->unbind(); + } +} + +void NodeBasedRenderer::updateStepInternal( const ViewingParameters& renderData ) { + if ( m_reloadJson ) + { + reloadRenderGraphFromJson(); + if ( m_resetPath ) + { + m_jsonFilePath = m_jsonFilePath.substr( 0, m_jsonFilePath.rfind( '/' ) + 1 ); + m_resetPath = false; + } + m_reloadJson = false; + } + + if ( m_originalRenderGraph.m_recompile ) + { + std::cerr << "NodeBasedRenderer::updateStepInternal :Recompiling Graph\n"; + compileRenderGraph(); + m_originalRenderGraph.m_recompile = false; + } + + // Render objects + m_originalRenderGraph.getDataNode()->setElements( *allRenderObjects() ); + // Lights + std::vector lights; + for ( size_t i = 0; i < getLightManager()->count(); i++ ) + { lights.push_back( getLightManager()->getLight( i ) ); } + m_originalRenderGraph.getDataNode()->setElements( lights ); + // Cameras + std::vector cameras; + cameras.push_back( renderData ); + m_originalRenderGraph.getDataNode()->setElements( cameras ); + // Update the render graph + + m_originalRenderGraph.update(); +} + +void NodeBasedRenderer::setDisplayNode( DisplaySinkNode* displayNode ) { + m_displaySinkNode = displayNode; + m_displayObserverId = + m_displaySinkNode->attachMember( this, &NodeBasedRenderer::observeDisplaySink ); +} + +void NodeBasedRenderer::observeDisplaySink( const std::vector& graphOutput ) { + // TODO : find a way to make the renderer observable to manage ouput texture in the applicaiton + // gui if needed + std::cout << "NodeBasedRenderer::observeDisplaySink - connected textures (" + << graphOutput.size() << ") : \n"; + /* + for ( const auto t : graphOutput ) + { + if ( t ) { std::cout << "\t" << t->getName() << "\n"; } + else + { std::cout << "\tName not available yet. Must run the graph a first time\n"; } + } + */ + // Add display nodes' linked textures to secondary textures + m_secondaryTextures.clear(); + for ( const auto& t : m_sharedTextures ) + { m_secondaryTextures.insert( { t.first, t.second.get() } ); } + + bool colorTextureSet = false; + if ( m_displaySinkNode ) + { + auto textures = m_displaySinkNode->getTextures(); + for ( const auto t : textures ) + { +# ifdef GRAPH_CALL_TRACE + std::cout << t->getName() << std::endl; +# endif + if ( t ) + { + if ( !colorTextureSet ) + { + m_colorTexture = t; + colorTextureSet = true; + } + else + { m_secondaryTextures.insert( { t->getName(), t } ); } + } + } + } + + if ( !colorTextureSet ) + { + m_colorTexture = m_fancyTexture.get(); + colorTextureSet = true; + } +} + +} // namespace RadiumNBR +#endif diff --git a/src/Dataflow/Rendering/Renderer/DataflowRenderer.hpp b/src/Dataflow/Rendering/Renderer/DataflowRenderer.hpp new file mode 100644 index 00000000000..929a192d82a --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/DataflowRenderer.hpp @@ -0,0 +1,202 @@ +#pragma once +#include + +/** + * Right now a renderer own a graph with some specific nodes available that gives acces to the + * renderobjects, the camera and the lights. + * These nodes are put in a specific factory SceneAccessors which contains nodes to access the + * renderobject, the camera and the light of the scene. + * + */ + +#include +#include + +#include + +namespace globjects { +class Framebuffer; +} + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { + +/// Todo, put this somewhere else. This is needed to locate resources by client applications +/// Todo (bis), remove this requirement +extern int DataflowRendererMagic; + +/** Dataflow renderer for the Radium Engine + * This Renderer is fully configurable, either dynamically or programmatically. + * It implements the Ra::Engine::Rendering/Renderer interface. + * + * A NodeBasedRenderer is configured by using the RenderControlFunctor given at construction. + * A RenderControlFunctor offers the following services : + * - configure() : add to the renderer as many RadiumNBR::RenderPass as needed. This method is + * called once when initializing the renderer. This method could also initialize internal + * resources into the controller that could be used to control the rendering. + * - resize() : called each time the renderer output is resized. This will allow modify controller + * resources that depends on the size of the output (e.g. internal textures ...) + * - update() : Called once before each frame to update the internal state of the renderer. + * + * A NodeBasedRenderer defines two textures that might be shared between passes : + * - a depth buffer attachable texture, stored with the key "Depth (RadiumNBR)" into the shared + * textures collection + * - a Linear space RGBA color texture, stored with the key "Linear RGB (RadiumNBR)" into the + * shared textures collection + * + * If requested on the base Ra::Engine::Rendering::Renderer, a NodeBasedRenderer apply a + * post-process step on the "Linear RGB (RadiumNBR)" that convert colors from linearRGB to sRGB + * color space before displaying the image. + * + * + * @see rendering.md for description of the renderer + */ +class RA_DATAFLOW_API DataflowRenderer : public Ra::Engine::Rendering::Renderer +{ + + public: + /** + * RenderGraph controller + */ + struct RA_DATAFLOW_API RenderGraphController : Ra::Core::Resources::ObservableVoid { + + RenderGraphController(); + virtual ~RenderGraphController() = default; + RenderGraphController( const RenderGraphController& ) = delete; + RenderGraphController( const RenderGraphController&& ) = delete; + RenderGraphController& operator=( RenderGraphController&& ) = delete; + RenderGraphController& operator=( const RenderGraphController& ) = delete; + + /// Configuration function. + /// Called once at the configuration of the renderer + virtual void configure( DataflowRenderer* renderer, int w, int h ); + + /// Resize function + /// Called each time the renderer is resized + virtual void resize( int w, int h ); + + /// Update function + /// Called once before each frame to update the internal state of the renderer + virtual void update( const Ra::Engine::Data::ViewingParameters& renderData ); + + [[nodiscard]] virtual std::string getRendererName() const { return "Dataflow Renderer"; } + + void loadGraph( const std::string& filename ); + void saveGraph( const std::string& filename ); + void resetGraph(); + /// Call this to set a graph to load before OpenGL is OK + void defferedLoadGraph( const std::string& filename ); + + /// The controlled graph. + /// The controller own the graph and manage loading/saving of the renderer + std::unique_ptr m_renderGraph { nullptr }; + + private: + int m_width { -1 }; + int m_height { -1 }; + Ra::Engine::Data::ShaderProgramManager* m_shaderMngr; + std::string m_graphToLoad; + }; + + /// Construct a renderer configured and managed through the controller + explicit DataflowRenderer( RenderGraphController& controller ); + + /// The destructor is used to destroy the render graph + ~DataflowRenderer() override; + + [[nodiscard]] std::string getRendererName() const override { return m_name; } + + bool buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const override; + + /// Access the default light manager + Ra::Engine::Scene::LightManager* getLightManager() { return m_lightmanagers[0]; } + + /// Access the controller + RenderGraphController& getController() { return m_controller; } + + /// Sets the display sink node + // void setDisplayNode( DisplaySinkNode* displayNode ); + + /// Loads the render graph from a Json file. + // void loadFromJson( const std::string& jsonFilePath ); + + /// Gets the Json file path + // const std::string& getJsonFilePath() { return m_jsonFilePath; } + + /// Sets the Json file path + // void setJsonFilePath( const std::string& jsonFilePath ) { m_jsonFilePath = jsonFilePath; } + + /// Reloads the render graph + // void compileRenderGraph(); + + /// Reloads the render graph according to the current Json file + // void reloadRenderGraphFromJson(); + + /// Raises the flag to reload the Json + /* + void signalReloadJson( bool resetPath = false ) { + m_reloadJson = true; + m_resetPath = resetPath; + } + */ + + protected: + void initializeInternal() override; + void resizeInternal() override; + void updateStepInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void renderInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void postProcessInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void debugInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void uiInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + + /** Initialize internal resources for the renderer. + * The base function creates the depth and final color texture to be shared by the rendering + * passes that need them. + */ + virtual void initResources(); + + public: + inline std::map>& sharedTextures() { + return m_sharedTextures; + } + + inline globjects::Framebuffer* postprocessFbo() { return m_postprocessFbo.get(); } + + inline std::vector* allRenderObjects() { return &m_fancyRenderObjects; } + + inline std::vector* getLights() { return &m_lights; } + + private: + /// Controler observer method + void graphChanged(); + + bool m_graphChanged { false }; + + /// textures own by the Renderer but shared across passes + std::map> m_sharedTextures; + + /// internal FBO used for post-processing + std::unique_ptr m_postprocessFbo; + + /// The configurator functor to use + RenderGraphController& m_controller; + + /// The name of the renderer + std::string m_name { "RenderGraph renderer" }; + + /// Texture to be read for postprocess according to the display node + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + + /// Vector of lights ... + std::vector m_lights; + + /// Vector of camera + std::vector m_cameras; +}; + +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Renderer/RenderingGraph.cpp b/src/Dataflow/Rendering/Renderer/RenderingGraph.cpp new file mode 100644 index 00000000000..5f21dd94eb4 --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/RenderingGraph.cpp @@ -0,0 +1,124 @@ +#include + +#include +#include + +using namespace Ra::Engine::Rendering; + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { +using namespace Ra::Dataflow::Rendering::Nodes; +using namespace Ra::Dataflow::Core; + +void RenderingGraph::init() { + DataflowGraph::init(); +} + +bool RenderingGraph::addNode( Node* newNode ) { + auto added = DataflowGraph::addNode( newNode ); + if ( added ) { + // Todo : is there something to do ? + } + return added; +} + +bool RenderingGraph::removeNode( Node* node ) { + auto removed = DataflowGraph::removeNode( node ); + if ( removed ) { + // Todo : is there something to do ? + } + return removed; +} + +/* +bool RenderingGraph::postCompilationOperation() { +#if 0 + m_renderingNodes.clear(); + m_rtIndexedNodes.clear(); + m_dataProviders.clear(); + if ( m_displaySinkNode && m_displayObserverId != -1 ) { + m_displaySinkNode->detach( m_displayObserverId ); + m_displayObserverId = -1; + m_displaySinkNode = nullptr; + } + const auto& compiledNodes = getNodesByLevel(); + int idx = 1; // The renderTechnique id = 0 is reserved for ui/debug objects + for ( const auto& lvl : *compiledNodes ) { + for ( auto n : lvl ) { + auto renderNode = dynamic_cast( n ); + if ( renderNode != nullptr ) { + m_renderingNodes.push_back( renderNode ); + if ( renderNode->hasRenderTechnique() ) { + renderNode->setIndex( idx++ ); + m_rtIndexedNodes.push_back( renderNode ); + } + renderNode->setShaderProgramManager( m_shaderMngr ); + } + else { + auto sceneNode = dynamic_cast( n ); + if ( sceneNode ) { m_dataProviders.push_back( sceneNode ); } + else { + // Manage all sinks ... + auto displaySink = dynamic_cast( n ); + if ( displaySink ) { + m_displaySinkNode = displaySink; + // observe the displaySink + m_displayObserverId = + displaySink->attachMember( this, &RenderingGraph::observeSinks ); + } + } + } + } + } +#if 0 + std::cout << "RenderingGraph::postCompilationOperation : got " << + m_renderingNodes.size() << " compiled rendering nodes with " << + m_rtIndexedNodes.size() << " render-passes, " << + m_dataProviders.size() << " scene nodes. \n"; +#endif +#endif +return true; +} +*/ +#if 0 +void RenderingGraph::observeSinks( const std::vector& graphOutput ) { + m_outputTextures = graphOutput; + /* + std::cout << "Available output textures are :" << std::endl; + int i = 0; + for (auto t : m_outputTextures ) { + std::cout << "\t tex " << i++ << " : "; + if (t) { + std::cout << t->getName() << std::endl; + } else { + std::cout << "nullptr" << std::endl; + } + } + */ +} +#endif + +const std::vector& RenderingGraph::getImagesOutput() const { + return m_outputTextures; +} + +void RenderingGraph::clearNodes() { + DataflowGraph::clearNodes(); + m_renderingNodes.clear(); + m_rtIndexedNodes.clear(); +} + +void RenderingGraph::buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const { + auto rt = std::make_shared(); + for ( const auto& rn : m_rtIndexedNodes ) { + rn->buildRenderTechnique( ro, *rt ); + } + rt->updateGL(); + ro->setRenderTechnique( rt ); +} +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Renderer/RenderingGraph.hpp b/src/Dataflow/Rendering/Renderer/RenderingGraph.hpp new file mode 100644 index 00000000000..06fbab96038 --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/RenderingGraph.hpp @@ -0,0 +1,113 @@ +#pragma once +#include + +#include + +#include + +#if 0 +# include +# include +#endif + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { +using namespace Ra::Dataflow::Rendering::Nodes; +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API RenderingGraph : public DataflowGraph +{ + public: + explicit RenderingGraph( const std::string& name ); + ~RenderingGraph() override = default; + + void init() override; + bool addNode( Node* newNode ) override; + bool removeNode( Node* node ) override; + void clearNodes() override; + + /// Sets the shader program manager + void setShaderProgramManager( Ra::Engine::Data::ShaderProgramManager* shaderMngr ) { + m_shaderMngr = shaderMngr; + } + + /// Resize all the rendering output + void resize( uint32_t width, uint32_t height ); + + /// Set the scene accessors on the graph + void setDataSources( std::vector* ros, std::vector* lights ); + /// Set the viewpoint on the graph + void setCameras( std::vector* cameras ); + + /// get the computed texture vector + const std::vector& getImagesOutput() const; + + /// Set render techniques needed by the rendering nodes + void buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const; + /// Return the typename of the Graph + static const std::string& getTypename(); + + protected: + void fromJsonInternal( const nlohmann::json& ) override; + void toJsonInternal( nlohmann::json& ) const override; + + private: + /// The renderer's shader program manager + Ra::Engine::Data::ShaderProgramManager* m_shaderMngr { nullptr }; + /// List of nodes that requires some particular processing + std::vector m_renderingNodes; // to resize + std::vector m_rtIndexedNodes; // associate an index and buildRenderTechnique +#if 0 + /// List of nodes that serve as data provider + std::vector m_dataProviders; + + // DisplaySink observerMethod : right now, observe only displaySink node + void observeSinks( const std::vector& graphOutput ); + /// The display sink node used to get rendered images + DisplaySinkNode* m_displaySinkNode { nullptr }; +#endif + /// ObserverId for displaySink; + int m_displayObserverId { -1 }; + std::vector m_outputTextures; +}; + +inline RenderingGraph::RenderingGraph( const std::string& name ) : + DataflowGraph( name, getTypename() ) {} + +inline const std::string& RenderingGraph::getTypename() { + static std::string demangledTypeName { "Rendering Graph" }; + return demangledTypeName; +} + +inline void RenderingGraph::resize( uint32_t width, uint32_t height ) { + for ( auto rn : m_renderingNodes ) { + rn->resize( width, height ); + } +} + +inline void RenderingGraph::setDataSources( std::vector* ros, + std::vector* lights ) { +#if 0 + for(auto sn : m_dataProviders) { + sn->setScene(ros, lights); + } +#endif +} + +inline void RenderingGraph::setCameras( std::vector* cameras ) { +#if 0 + for(auto sn : m_dataProviders) { + sn->setCameras(cameras); + } +#endif +} + +inline void RenderingGraph::fromJsonInternal( const nlohmann::json& ) {} +inline void RenderingGraph::toJsonInternal( nlohmann::json& ) const {} + +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/filelist.cmake b/src/Dataflow/Rendering/filelist.cmake new file mode 100644 index 00000000000..7c05dd525ed --- /dev/null +++ b/src/Dataflow/Rendering/filelist.cmake @@ -0,0 +1,9 @@ +# ---------------------------------------------------- +# This file should not be generated by the radium script +# ---------------------------------------------------- + +set(dataflow_rendering_sources Renderer/DataflowRenderer.cpp Renderer/RenderingGraph.cpp) + +set(dataflow_rendering_headers Nodes/RenderingNode.hpp Renderer/DataflowRenderer.hpp + Renderer/RenderingGraph.hpp +) diff --git a/src/Dataflow/Rendering/pch.hpp b/src/Dataflow/Rendering/pch.hpp new file mode 100644 index 00000000000..6f70f09beec --- /dev/null +++ b/src/Dataflow/Rendering/pch.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/Engine/Data/EnvironmentTexture.cpp b/src/Engine/Data/EnvironmentTexture.cpp index 3fda194fe83..d542b2ceb6a 100644 --- a/src/Engine/Data/EnvironmentTexture.cpp +++ b/src/Engine/Data/EnvironmentTexture.cpp @@ -425,13 +425,14 @@ void EnvironmentTexture::setupTexturesFromSphericalEquiRectangular() { Scalar v = -1 + j * duv; Vector3 d = bases[imgIdx][0] + u * bases[imgIdx][1] + v * bases[imgIdx][2]; d = d.normalized(); - Vector2 st { w * sphericalPhi( d ) / ( 2 * M_PI ), h * sphericalTheta( d ) / M_PI }; + Vector2 st { w * sphericalPhi( d ) / ( 2_ra * M_PI ), + h * sphericalTheta( d ) / M_PI }; // TODO : use st to access and filter the original envmap // for now, no filtering is done. (eq to GL_NEAREST) - int s = int( st.x() ); - int t = int( st.y() ); - int cu = int( ( u / 2 + 0.5 ) * textureSize ); - int cv = int( ( v / 2 + 0.5 ) * textureSize ); + int s = std::min( int( st.x() ), w - 1 ); + int t = std::min( int( st.y() ), h - 1 ); + int cu = int( ( u / 2_ra + 0.5_ra ) * textureSize ); + int cv = int( ( v / 2_ra + 0.5_ra ) * textureSize ); m_skyData[imgIdx][4 * ( cv * textureSize + cu ) + 0] = latlonPix[4 * ( t * w + s ) + 0]; diff --git a/src/Gui/Widgets/ControlPanel.cpp b/src/Gui/Widgets/ControlPanel.cpp index cca0783f5fd..63fd127068e 100644 --- a/src/Gui/Widgets/ControlPanel.cpp +++ b/src/Gui/Widgets/ControlPanel.cpp @@ -158,7 +158,8 @@ void ControlPanel::addColorInput( Ra::Core::Utils::Color color, bool withAlpha, const std::string& tooltip ) { - auto button = new QPushButton( name.c_str(), this ); + auto button = new QPushButton( name.c_str(), this ); + button->setObjectName( name.c_str() ); auto srgbColor = Ra::Core::Utils::Color::linearRGBTosRGB( color ); auto clrBttn = QColor::fromRgbF( srgbColor[0], srgbColor[1], srgbColor[2], srgbColor[3] ); auto clrDlg = [callback, clrBttn, withAlpha, button, name]() mutable { diff --git a/tests/unittest/CMakeLists.txt b/tests/unittest/CMakeLists.txt index aa26cd009d9..a538a8a6b8e 100644 --- a/tests/unittest/CMakeLists.txt +++ b/tests/unittest/CMakeLists.txt @@ -12,7 +12,7 @@ set(test_src Core/camera.cpp Core/color.cpp Core/containers.cpp - Core/demangle.cpp + Core/typeutils.cpp Core/distance.cpp Core/enumconverter.cpp Core/geometryData.cpp @@ -22,6 +22,7 @@ set(test_src Core/obb.cpp Core/observer.cpp Core/polyline.cpp + Core/random.cpp Core/raycast.cpp Core/resources.cpp Core/string.cpp @@ -30,6 +31,11 @@ set(test_src Core/topomesh.cpp Core/variableset.cpp Core/vectorarray.cpp + Dataflow/customnodes.cpp + Dataflow/graph.cpp + Dataflow/nodes.cpp + Dataflow/serialization.cpp + Dataflow/sourcesandsinks.cpp Engine/environmentmap.cpp Engine/renderparameters.cpp Engine/signalmanager.cpp @@ -54,7 +60,7 @@ target_include_directories(unittests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_options(unittests PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) target_compile_definitions(unittests PRIVATE UNIT_TESTS) # add -DUNIT_TESTS define -target_link_libraries(unittests PRIVATE Catch2::Catch2 Core Engine Gui) +target_link_libraries(unittests PRIVATE Catch2::Catch2 Core Engine Gui Dataflow) add_dependencies(unittests Catch2 Core Engine Gui) find_package(Filesystem COMPONENTS Final Experimental REQUIRED) diff --git a/tests/unittest/Core/demangle.cpp b/tests/unittest/Core/demangle.cpp deleted file mode 100644 index da3cc5c2073..00000000000 --- a/tests/unittest/Core/demangle.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include -#include - -#include -#include - -#include - -namespace TypeTests { -struct TypeName_struct {}; -} // namespace TypeTests -TEST_CASE( "Core/Utils/TypesUtils", "[Core][Core/Utils][TypesUtils]" ) { - SECTION( "Demangle from typename" ) { - using Ra::Core::Utils::demangleType; - - REQUIRE( std::string( demangleType() ) == "int" ); - REQUIRE( std::string( demangleType() ) == "float" ); - REQUIRE( std::string( demangleType() ) == "unsigned int" ); - // TODO, verify type demangling on windows -#ifndef _WIN32 - REQUIRE( std::string( demangleType() ) == "unsigned long" ); -#else - REQUIRE( std::string( demangleType() ) == "unsigned __int64" ); -#endif - auto demangledName = std::string( demangleType>() ); - REQUIRE( demangledName == "std::vector>" ); - - demangledName = std::string( demangleType() ); - REQUIRE( demangledName == "TypeTests::TypeName_struct" ); - } - - SECTION( "Demangle from instance" ) { - using Ra::Core::Utils::demangleType; - - int i { 1 }; - float f { 2 }; - unsigned int u { 3 }; - size_t s { 4 }; - - REQUIRE( std::string( demangleType( i ) ) == "int" ); - REQUIRE( std::string( demangleType( f ) ) == "float" ); - REQUIRE( std::string( demangleType( u ) ) == "unsigned int" ); - // TODO, verify type demangling on windows -#ifndef _WIN32 - REQUIRE( std::string( demangleType( s ) ) == "unsigned long" ); -#else - REQUIRE( std::string( demangleType( s ) ) == "unsigned __int64" ); -#endif - std::vector v; - auto demangledName = std::string( demangleType( v ) ); - REQUIRE( demangledName == "std::vector>" ); - - TypeTests::TypeName_struct tns; - demangledName = std::string( demangleType( tns ) ); - REQUIRE( demangledName == "TypeTests::TypeName_struct" ); - } -} diff --git a/tests/unittest/Core/random.cpp b/tests/unittest/Core/random.cpp new file mode 100644 index 00000000000..2c302457f8e --- /dev/null +++ b/tests/unittest/Core/random.cpp @@ -0,0 +1,106 @@ +#include +#include + +#include +#include + +#include +using namespace Ra::Core::Random; + +TEST_CASE( "Core/Random/RandomPointSet", "[Core][Core/Random][PointSet]" ) { + SECTION( "Fibonacci sequence" ) { + std::array fib_verif { 0_ra, + 0.61803398874989479150343640867504_ra, + 1.2360679774997895830068728173501_ra, + 1.8541019662496844855326116885408_ra, + 2.4721359549995791660137456347002_ra }; + FibonacciSequence fib { 2 }; + // Our fibonacci sequence is only defined for more than 5 points + REQUIRE( fib.range() == 5 ); + for ( size_t i = 0; i < 5; ++i ) { + REQUIRE( isApprox( fib( i ), fib_verif[i] ) ); + } + } + + SECTION( "VanDerCorput sequence" ) { + std::array vdc_verif { 0_ra, 0.5_ra, 0.25_ra, 0.75_ra, 0.125_ra }; + VanDerCorputSequence vdc; + for ( size_t i = 0; i < 5; ++i ) { + REQUIRE( isApprox( vdc( i ), vdc_verif[i] ) ); + } + } + + SECTION( "Fibonacci point set" ) { + std::array, 5> fibseq_verif { + std::pair { 0_ra, 0_ra / 5_ra }, + { 0.61803398874989479150343640867504_ra, 1_ra / 5_ra }, + { 1.2360679774997895830068728173501_ra, 2_ra / 5_ra }, + { 1.8541019662496844855326116885408_ra, 3_ra / 5_ra }, + { 2.4721359549995791660137456347002_ra, 4_ra / 5_ra } }; + FibonacciPointSet fibs { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = fibs( i ); + REQUIRE( isApprox( v[0], fibseq_verif[i].first ) ); + REQUIRE( isApprox( v[1], fibseq_verif[i].second ) ); + } + } + + SECTION( "Hammersley point set" ) { + std::array, 5> seq_verif { + std::pair { 0_ra, 0_ra / 5_ra }, + { 1_ra / 5_ra, 0.5_ra }, + { 2_ra / 5_ra, 0.25_ra }, + { 3_ra / 5_ra, 0.75_ra }, + { 4_ra / 5_ra, 0.125_ra } }; + HammersleyPointSet seq { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = seq( i ); + REQUIRE( isApprox( v[0], seq_verif[i].first ) ); + REQUIRE( isApprox( v[1], seq_verif[i].second ) ); + } + } + + // todo, verify if the sequence is always the same (it should) on any systems/run + SECTION( "MersenneTwister point set" ) { +#ifdef CORE_USE_DOUBLE + // Sequence valid only when Scalar == double + std::array, 5> seq_verif { + std::pair { 0.59284461651668263204584263803554_ra, + 0.84426574425659828282419994138763_ra }, + { 0.85794561998982987738315841852454_ra, 0.84725173738433123826752080276492_ra }, + { 0.62356369649610832173181051985011_ra, 0.38438170837375662536317122430773_ra }, + { 0.29753460535723419422282631785492_ra, 0.056712975933163663200264892338964_ra }, + { 0.27265629474158931122573790162278_ra, 0.47766511174464632016878340436961_ra } }; +#else + // Sequence valid only when Scalar == float + std::array, 5> seq_verif { + std::pair { 0.548813521862030029296875_ra, + 0.59284460544586181640625_ra }, + { 0.71518933773040771484375_ra, 0.844265758991241455078125_ra }, + { 0.602763354778289794921875_ra, 0.857945621013641357421875_ra }, + { 0.544883191585540771484375_ra, 0.847251713275909423828125_ra }, + { 0.4236547946929931640625_ra, 0.623563706874847412109375_ra } }; +#endif + MersenneTwisterPointSet seq { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = seq( i ); + REQUIRE( isApprox( v[0], seq_verif[i].first ) ); + REQUIRE( isApprox( v[1], seq_verif[i].second ) ); + } + } + + SECTION( "SphericalPointSet point set (Hammersley)" ) { + std::array, 5> seq_verif { + std::pair { 1.2246467991473532071737640294584e-16_ra, 0_ra }, + { 0.30901699437494745126286943559535_ra, 0.95105651629515353118193843329209_ra }, + { -0.70062926922203661028731858095853_ra, 0.50903696045512725198989301134134_ra }, + { -0.70062926922203672130962104347418_ra, -0.50903696045512702994528808631003_ra }, + { 0.20439552950218897731105016646325_ra, -0.62906475622110624712490789534058_ra } }; + SphericalPointSet seq { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = seq( i ); + REQUIRE( isApprox( v[0], seq_verif[i].first ) ); + REQUIRE( isApprox( v[1], seq_verif[i].second ) ); + } + } +} diff --git a/tests/unittest/Core/typeutils.cpp b/tests/unittest/Core/typeutils.cpp new file mode 100644 index 00000000000..12e4633fdda --- /dev/null +++ b/tests/unittest/Core/typeutils.cpp @@ -0,0 +1,68 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace TypeTests { +struct TypeName_struct {}; +} // namespace TypeTests +TEST_CASE( "Core/Utils/TypesUtils", "[Core][Utils][TypesUtils]" ) { + SECTION( "Demangle from typename" ) { + using Ra::Core::Utils::demangleType; + + REQUIRE( demangleType() == "int" ); + REQUIRE( demangleType() == "float" ); + REQUIRE( demangleType() == "unsigned int" ); + REQUIRE( demangleType() == "unsigned long" ); + + auto demangledName = demangleType>(); + REQUIRE( demangledName == "std::vector>" ); + + demangledName = demangleType(); + REQUIRE( demangledName == "TypeTests::TypeName_struct" ); + + demangledName = demangleType( std::type_index( typeid( std::vector ) ) ); + REQUIRE( demangledName == "std::vector>" ); + } + + SECTION( "Demangle from instance" ) { + using Ra::Core::Utils::demangleType; + + int i { 1 }; + float f { 2 }; + unsigned int u { 3 }; + size_t s { 4 }; + + REQUIRE( demangleType( i ) == "int" ); + REQUIRE( demangleType( f ) == "float" ); + REQUIRE( demangleType( u ) == "unsigned int" ); + REQUIRE( demangleType( s ) == "unsigned long" ); + +#ifndef _WIN32 + // this segfault on windows due to out_of_bound exception. why ??? + std::vector v; + auto demangledName = demangleType( v ); + REQUIRE( demangledName == "std::vector>" ); +#endif + TypeTests::TypeName_struct tns; + auto demangledNameFromStruct = demangleType( tns ); + REQUIRE( demangledNameFromStruct == "TypeTests::TypeName_struct" ); + } + + SECTION( "Type traits" ) { + using namespace Ra::Core::Utils; + REQUIRE( is_container::value == false ); + REQUIRE( is_container::value == false ); + REQUIRE( is_container::value == false ); + REQUIRE( is_container>::value == true ); + REQUIRE( is_container>::value == true ); + REQUIRE( is_container>::value == true ); + REQUIRE( is_container>::value == true ); + } +} diff --git a/tests/unittest/Dataflow/customnodes.cpp b/tests/unittest/Dataflow/customnodes.cpp new file mode 100644 index 00000000000..c943d20166d --- /dev/null +++ b/tests/unittest/Dataflow/customnodes.cpp @@ -0,0 +1,337 @@ +/** + * Demonstrate how to define custom nodes anduse factory to serialize graphs with custom nodes + */ +#include + +#include +#include + +#include + +#include + +#include +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +namespace Customs { +using CustomStringSource = Sources::SingleDataSourceNode; +using CustomStringSink = Sinks::SinkNode; + +//! [Develop a custom node] +/** + * \brief generate a predicate that compare a value wrt a threshold. + * The name of the operator is fetched from input port "name" or the internal data set using + * setFunctionName. Available operator are "=", ">", ">=", "<", "<=", "!=", "true", "false". + * + * The threshold is fetched from input port "threshold" or the internal data set using setThreshold. + * + * The operator is sent on the output port "f". + * + * \tparam T the type of the parameter to evaluate + */ +template +class FilterSelector final : public Node +{ + public: + using function_type = std::function; + + explicit FilterSelector( const std::string& name ) : FilterSelector( name, getTypename() ) {} + + bool execute() override { + // get operator parameters + if ( m_portName->isLinked() ) { m_operatorName = m_portName->getData(); } + if ( m_portThreshold->isLinked() ) { m_threshold = m_portThreshold->getData(); } + // compute the result associated to the output port + m_currentFunction = m_functions[m_operatorName]; + return true; + } + + /** \brief Set the function name used to select the function to deliver. + * this name will be used if the input port "name" is not linked + * @param name + */ + void setOperatorName( const std::string& name ) { m_operatorName = name; } + /** + * \brief Get the delivered data + * @return The non owning pointer (alias) to the delivered data. + */ + function_type* getOperator() const { return m_functions[m_operatorName]; } + + /** \brief Set the threshold - will copy the value into the node + * @param name + */ + void setThreshold( const T& t ) { m_threshold = t; } + /** \brief Get the threshold + */ + T getThreshold() const { return m_threshold; } + + protected: + bool fromJsonInternal( const nlohmann::json& data ) override { + if ( data.contains( "operator" ) ) { m_operatorName = data["operator"]; } + else { m_operatorName = "true"; } + if ( data.contains( "threshold" ) ) { m_threshold = data["threshold"]; } + else { m_threshold = T {}; } + return true; + } + + void toJsonInternal( nlohmann::json& data ) const override { + data["operator"] = m_operatorName; + data["threshold"] = m_threshold; + } + + public: + static const std::string& getTypename() { + static std::string demangledTypeName = std::string { "FilterSelector<" } + + Ra::Dataflow::Core::simplifiedDemangledType() + + ">"; + return demangledTypeName; + } + + private: + FilterSelector( const std::string& instanceName, const std::string& typeName ) : + Node( instanceName, typeName ) { + // Adding ports to node + addInput( m_portName ); + addInput( m_portThreshold ); + addOutput( m_operatourOut, &m_currentFunction ); + addOutput( m_nameOut, &m_operatorName ); + } + + /// Alias to the output port + PortOut* m_operatourOut { new PortOut( "f", this ) }; + PortOut* m_nameOut { new PortOut( "name", this ) }; + /// Alias for the input ports + PortIn* m_portName { new PortIn( "name", this ) }; + PortIn* m_portThreshold { new PortIn( "threshold", this ) }; + + /// The data provided by the node + std::map m_functions { + { "true", []( const T& ) { return true; } }, + { "false", []( const T& ) { return false; } }, + { "<", [this]( const T& v ) { return v < this->m_threshold; } }, + { ">", [this]( const T& v ) { return v > this->m_threshold; } } }; + + std::string m_operatorName { "true" }; + function_type m_currentFunction = m_functions[m_operatorName]; + T m_threshold {}; +}; +//! [Develop a custom node] +} // namespace Customs + +// Reusable function to create a graph +template +DataflowGraph* buildgraph( const std::string& name ) { + auto g = new DataflowGraph( name ); + + auto ds = + std::make_shared>>( "ds" ); + REQUIRE( g->addNode( ds ) ); + + auto rs = std::make_shared>>( "rs" ); + REQUIRE( g->addNode( rs ) ); + + auto ts = std::make_shared>( "ts" ); + REQUIRE( g->addNode( ts ) ); + + auto ss = std::make_shared( "ss" ); + REQUIRE( g->addNode( ss ) ); + + auto nm = std::make_shared( "nm" ); + REQUIRE( g->addNode( nm ) ); + + auto fs = std::make_shared>( "fs" ); + REQUIRE( g->addNode( fs ) ); + + auto fl = std::make_shared>>( "fl" ); + REQUIRE( g->addNode( fl ) ); + + bool ok; + ok = g->addLink( ds, "to", fl, "in" ); + REQUIRE( ok ); + ok = g->addLink( fl, "out", rs, "from" ); + REQUIRE( ok ); + ok = g->addLink( ss, "to", fs, "name" ); + REQUIRE( ok ); + ok = g->addLink( ts, "to", fs, "threshold" ); + REQUIRE( ok ); + ok = g->addLink( fs, "f", fl, "f" ); + REQUIRE( ok ); + ok = g->addLink( fs, "name", nm, "from" ); + REQUIRE( ok ); + return g; +} + +// test sections +TEST_CASE( "Dataflow/Core/Custom nodes", "[Dataflow][Core][Custom nodes]" ) { + SECTION( "Build graph with custom nodes" ) { + // build a graph + auto g = buildgraph( "testCustomNodes" ); + + // get input and ouput of the graph + auto inputCollection = g->getDataSetter( "ds_to" ); + REQUIRE( inputCollection != nullptr ); + auto inputOpName = g->getDataSetter( "ss_to" ); + REQUIRE( inputOpName != nullptr ); + auto inputThreshold = g->getDataSetter( "ts_to" ); + REQUIRE( inputThreshold != nullptr ); + + auto filteredCollection = g->getDataGetter( "rs_from" ); + REQUIRE( filteredCollection != nullptr ); + auto generatedOperator = g->getDataGetter( "nm_from" ); + REQUIRE( generatedOperator != nullptr ); + + // parameterize the graph + using CollectionType = Ra::Core::VectorArray; + CollectionType testVector; + testVector.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution dis( 0.0_ra, 1.0_ra ); + // Fill the vector with random numbers between 0 and 1 + for ( size_t n = 0; n < testVector.capacity(); ++n ) { + testVector.push_back( dis( gen ) ); + } + inputCollection->setData( &testVector ); + + Scalar threshold { 0.5_ra }; + inputThreshold->setData( &threshold ); + + std::string op { "true" }; + inputOpName->setData( &op ); + + std::cout << "Data sent to graph : \n\toperator " << op << " : \n\t"; + for ( auto ord : testVector ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + + // execute the graph that filter out nothing + // execute + auto r = g->execute(); + REQUIRE( r ); + + // Getters are usable only after successful compilation/execution of the graph + // Get results as references (no need to get them again later if the graph does not change) + auto& vres = filteredCollection->getData(); + auto& vop = generatedOperator->getData(); + + REQUIRE( vop == "true" ); + REQUIRE( vres.size() == testVector.size() ); + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + + // change operator to filter out everything + op = "false"; + r = g->execute(); + REQUIRE( r ); + REQUIRE( vop == "false" ); + REQUIRE( vres.size() == 0 ); + + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + // Change operator to keep element less than threshold + op = "<"; + r = g->execute(); + REQUIRE( r ); + + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + REQUIRE( *( std::max_element( vres.begin(), vres.end() ) ) < threshold ); + + // Change operator to keep element greater than threshold + op = ">"; + r = g->execute(); + REQUIRE( r ); + + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + REQUIRE( *( std::max_element( vres.begin(), vres.end() ) ) > threshold ); + } + SECTION( "Serialization of a custom graph" ) { + // Create and fill the factory for the custom nodes + auto customFactory = NodeFactoriesManager::createFactory( "CustomNodesUnitTests" ); + + // add node creators to the factory + bool registered; + registered = customFactory->registerNodeCreator( + Customs::CustomStringSource::getTypename() + "_", "Custom" ); + REQUIRE( registered == true ); + registered = customFactory->registerNodeCreator( + Customs::CustomStringSink::getTypename() + "_", "Custom" ); + REQUIRE( registered == true ); + registered = customFactory->registerNodeCreator>( + Customs::FilterSelector::getTypename() + "_", "Custom" ); + REQUIRE( registered == true ); + // The same node can't be register twice in the same factory + registered = customFactory->registerNodeCreator>( + Customs::FilterSelector::getTypename() + "_", "Custom" ); + REQUIRE( registered == false ); + + std::cout << "Building the following custom nodes with the factory " + << customFactory->getName() << "\n"; + for ( auto [name, functor] : customFactory->getFactoryMap() ) { + std::cout << name << ", "; + } + std::cout << "\n"; + + nlohmann::json emptyData; + auto customSource = customFactory->createNode( + Customs::CustomStringSource::getTypename(), emptyData, nullptr ); + REQUIRE( customSource != nullptr ); + + std::cout << "Created node " << customSource->getInstanceName() << " with type " + << customSource->getTypeName() << " // " + << Customs::CustomStringSource::getTypename() << "\n"; + + // build a graph + auto g = buildgraph( "testCustomNodes" ); + g->addFactory( customFactory ); + + std::string tmpdir { "customGraphExport/" }; + std::filesystem::create_directories( tmpdir ); + + // save the graph without factory + // save the graph with factory + g->saveToJson( tmpdir + "customGraph.json" ); + + g->destroy(); + delete g; + g = new DataflowGraph( "" ); + + bool loaded = g->loadFromJson( tmpdir + "customGraph.json" ); + + REQUIRE( loaded == true ); + g->destroy(); + delete g; + + /// try to load the graph without custom factory + auto unregistered = NodeFactoriesManager::unregisterFactory( customFactory->getName() ); + REQUIRE( unregistered == true ); + + g = new DataflowGraph( "" ); + loaded = g->loadFromJson( tmpdir + "customGraph.json" ); + REQUIRE( loaded == false ); + delete g; + + std::filesystem::remove_all( tmpdir ); + } +} diff --git a/tests/unittest/Dataflow/graph.cpp b/tests/unittest/Dataflow/graph.cpp new file mode 100644 index 00000000000..fddf441b844 --- /dev/null +++ b/tests/unittest/Dataflow/graph.cpp @@ -0,0 +1,416 @@ +#include + +#include + +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +void inspectGraph( const DataflowGraph& g ) { + // Factories used by the graph + auto factories = g.getNodeFactories(); + std::cout << "Used factories by the graph \"" << g.getInstanceName() << "\" with type \"" + << g.getTypeName() << "\" :\n"; + for ( const auto& f : *( factories.get() ) ) { + std::cout << "\t" << f.first << "\n"; + } + + // Nodes of the graph + const auto& nodes = g.getNodes(); + std::cout << "Nodes of the graph " << g.getInstanceName() << " (" << nodes.size() << ") :\n"; + for ( const auto& n : nodes ) { + std::cout << "\t\"" << n->getInstanceName() << "\" of type \"" << n->getTypeName() + << "\"\n"; + // Inspect input, output and interfaces of the node + std::cout << "\t\tInput ports :\n"; + for ( const auto& p : n->getInputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() << "\n"; + } + std::cout << "\t\tOutput ports :\n"; + for ( const auto& p : n->getOutputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() << "\n"; + } + std::cout << "\t\tInterface ports :\n"; + for ( const auto& p : n->getInterfaces() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() << "\n"; + } + } + + // Nodes by level after the compilation + if ( g.isCompiled() ) { + auto& cn = g.getNodesByLevel(); + std::cout << "Nodes of the graph, sorted by level after compiling the graph :\n"; + for ( size_t i = 0; i < cn.size(); ++i ) { + std::cout << "\tLevel " << i << " :\n"; + for ( const auto n : cn[i] ) { + std::cout << "\t\t\"" << n->getInstanceName() << "\"\n"; + } + } + } + + // describe the graph interface : inputs and outputs port of the whole graph (not of the + // nodes) + std::cout << "Inputs and output ports of the graph " << g.getInstanceName() << " :\n"; + const auto& inputs = g.getInputs(); + std::cout << "\tInput ports (" << inputs.size() << ") are :\n"; + for ( const auto& inp : inputs ) { + std::cout << "\t\t\"" << inp->getName() << "\" accepting type \"" << inp->getTypeName() + << "\"\n"; + } + const auto& outputs = g.getOutputs(); + std::cout << "\tOutput ports (" << outputs.size() << ") are :\n"; + for ( const auto& outp : outputs ) { + std::cout << "\t\t\"" << outp->getName() << "\" accepting type \"" << outp->getTypeName() + << "\"\n"; + } + + std::cout << "DataSetters and DataGetters port of the graph " << g.getInstanceName() << " :\n"; + auto setters = g.getAllDataSetters(); + std::cout << "\tSetters ports (" << setters.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : setters ) { + std::cout << "\t\t\"" << portName << "\" accepting type \"" << portType << "\"\n"; + } + auto getters = g.getAllDataGetters(); + std::cout << "\tGetters ports (" << getters.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : getters ) { + std::cout << "\t\t\"" << portName << "\" generating type \"" << portType << "\"\n"; + } +} + +TEST_CASE( "Dataflow/Core/Graph", "[Dataflow][Core][Graph]" ) { + SECTION( "Creation of a graph" ) { + DataflowGraph g( "Test Graph" ); + // Test not a json error detection + auto result = g.loadFromJson( "data/Dataflow/NotAJsonFile.json" ); + REQUIRE( !result ); + // Test loading empty graph + nlohmann::json emptyJson = {}; + result = g.fromJson( emptyJson ); + REQUIRE( result ); + + // missing identification of the graph (either id or instance) + nlohmann::json noId = { { "model", { "name", "Core DataflowGraph" } } }; + result = g.fromJson( noId ); + REQUIRE( !result ); + g.destroy(); + + // missing model of the graph + nlohmann::json noModel = { { "instance", "No model in this node" } }; + result = g.fromJson( noModel ); + REQUIRE( !result ); + g.destroy(); + + // missing instance data --> loads an empty graph + nlohmann::json noGraph = { { "instance", "Missing instance data for model" }, + { "model", { "name", "Core DataflowGraph" } } }; + result = g.fromJson( noGraph ); + REQUIRE( result ); + g.destroy(); + + // Requesting unknown factory + nlohmann::json wrongFactory = { + { "instance", "unknown factory" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", { { "factories", { "NotAFactory" } } } } } } }; + result = g.fromJson( wrongFactory ); + REQUIRE( !result ); + g.destroy(); + + // trying to instance an unknown node type + nlohmann::json NotANode = { + { "instance", "graph with unknown node" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "NotANode" }, + { "model", { { "name", "NotANode" } } } } } } } } } } }; + result = g.fromJson( NotANode ); + REQUIRE( !result ); + g.destroy(); + + // trying to instance an unknown node type + nlohmann::json NoModelName = { + { "instance", "graph with missing node model information" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "Unknown model" }, + { "model", { { "extra", "NotaTypeName" } } } } } } } } } } }; + result = g.fromJson( NoModelName ); + REQUIRE( !result ); + g.destroy(); + + // trying to instance an unknown node type + nlohmann::json noInstanceIdentification = { + { "instance", "graph with missing node model information" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", { { { "model", { { "name", "Source" } } } } } } } } } } }; + result = g.fromJson( noInstanceIdentification ); + REQUIRE( !result ); + g.destroy(); + + // errors in the connection description + nlohmann::json reusingNodeIdentification = { + { "instance", "graph with wrong connection" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "Source" }, { "model", { { "name", "Source" } } } }, + { { "instance", "Source" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", { { { "out_node", "wrongId" } } } } } } } } }; + result = g.fromJson( reusingNodeIdentification ); + REQUIRE( !result ); + g.destroy(); + reusingNodeIdentification = { + { "instance", "graph with wrong connection" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "model", { { "name", "Source" } } } }, + { { "model", { { "name", "Sink" } } } } } }, + { "connections", { { { "out_node", "wrongId" } } } } } } } } }; + result = g.fromJson( reusingNodeIdentification ); + REQUIRE( !result ); + g.destroy(); + + // errors in the connection description + nlohmann::json wrongConnection = { + { "instance", "graph with wrong connection" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", { { { "out_node", "wrongId" } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + wrongConnection = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, { "out_index", 2 } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + wrongConnection = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, + { "out_index", 0 }, + { "in_node", "Sink" }, + { "in_port", "from" } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + wrongConnection = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, + { "out_index", 0 }, + { "in_node", "SinkInt" }, + { "in_port", "from" } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + // Constructed a correct graph + nlohmann::json goodSimpleGraph = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkFloat" }, + { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, + { "out_port", "to" }, + { "in_node", "SinkFloat" }, + { "in_index", 0 } } } } } } } } }; + result = g.fromJson( goodSimpleGraph ); + REQUIRE( result ); + + // trying to add a duplicated node + auto duplicatedNodeName = + std::make_shared>( "SourceFloat" ); + auto r = g.addNode( duplicatedNodeName ); + REQUIRE( !r ); + + auto sinkFloatNode = g.getNode( "Sink" ); + REQUIRE( sinkFloatNode == nullptr ); + sinkFloatNode = g.getNode( "SinkFloat" ); + REQUIRE( sinkFloatNode != nullptr ); + auto sourceFloatNode = g.getNode( "SourceFloat" ); + REQUIRE( sourceFloatNode != nullptr ); + + auto sourceIntNode = std::make_shared( "SourceInt" ); + auto sinkIntNode = std::make_shared( "SinkInt" ); + // node not found + REQUIRE( !g.removeLink( sinkIntNode, "from" ) ); + + // "from" node not found + REQUIRE( !g.addLink( sourceIntNode, "out", sinkIntNode, "in" ) ); + + REQUIRE( g.addNode( sourceIntNode ) ); + // "to" node not found + REQUIRE( !g.addLink( sourceIntNode, "out", sinkIntNode, "in" ) ); + + REQUIRE( g.addNode( sinkIntNode ) ); + // output port of "from" node not found + // output port of "from" node not found + REQUIRE( !g.addLink( sourceIntNode, "out", sinkIntNode, "in" ) ); + + // input port of "to" node not found + result = g.addLink( sourceIntNode, "to", sinkIntNode, "in" ); + REQUIRE( !result ); + + // link OK + result = g.addLink( sourceIntNode, "to", sinkIntNode, "from" ); + REQUIRE( result ); + + // from port of "to" node already linked + result = g.addLink( sourceIntNode, "to", sinkIntNode, "from" ); + REQUIRE( !result ); + + // type mismatch + result = g.addLink( sourceIntNode, "to", sinkFloatNode, "from" ); + REQUIRE( !result ); + + // protect the graph to prevent link removal + g.setNodesAndLinksProtection( true ); + REQUIRE( g.getNodesAndLinksProtection() ); + // unable to remove links from protected graph ... + result = g.removeLink( sinkIntNode, "from" ); + REQUIRE( !result ); + g.setNodesAndLinksProtection( false ); + REQUIRE( !g.getNodesAndLinksProtection() ); + // remove link OK + result = g.removeLink( sinkIntNode, "from" ); + REQUIRE( result ); + + // input port not found to remove its link + result = g.removeLink( sinkIntNode, "in" ); + REQUIRE( !result ); + + // compile the graph + result = g.compile(); + REQUIRE( result ); + REQUIRE( g.isCompiled() ); + + // clear the graph + g.clearNodes(); + + // Nodes can't be found + auto nullNode = g.getNode( "SourceInt" ); + REQUIRE( nullNode == nullptr ); + nullNode = g.getNode( "SinkInt" ); + REQUIRE( nullNode == nullptr ); + // Nodes can't be found + nullNode = g.getNode( "SourceFloat" ); + REQUIRE( nullNode == nullptr ); + nullNode = g.getNode( "SinkFloat" ); + REQUIRE( nullNode == nullptr ); + + // destroy everything + g.destroy(); + } + + SECTION( "Inspection of a graph" ) { + std::cout << "Loading graph data/Dataflow/ExampleGraph.json\n"; + auto g = DataflowGraph::loadGraphFromJsonFile( "data/Dataflow/ExampleGraph.json" ); + + // Factories used by the graph + auto factories = g->getNodeFactories(); + REQUIRE( factories != nullptr ); + const auto& nodes = g->getNodes(); + REQUIRE( nodes.size() == g->getNodesCount() ); + auto c = g->compile(); + REQUIRE( c == true ); + REQUIRE( g->isCompiled() ); + // Prints the graph content + inspectGraph( *g ); + g->needsRecompile(); + REQUIRE( !g->isCompiled() ); + + // removing the boolean sink from the graph + auto n = g->getNode( "validation value" ); + auto useCount = n.use_count(); + REQUIRE( n->getInstanceName() == "validation value" ); + c = g->removeNode( n ); + REQUIRE( c == true ); + REQUIRE( n ); + REQUIRE( n.use_count() == useCount - 1 ); + c = g->compile(); + REQUIRE( c == true ); + + // Simplified graph after compilation + auto& cn = g->getNodesByLevel(); + // the source "Validator" is no more in level 0 as it is not reachable from a sink in the + // graph. + auto found = std::find_if( cn[0].begin(), cn[0].end(), []( const auto& nn ) { + return nn->getInstanceName() == "Validator"; + } ); + REQUIRE( found == cn[0].end() ); + + // removing the source "Validator" + n = g->getNode( "Validator" ); + REQUIRE( n->getInstanceName() == "Validator" ); + // protect the graph to prevent node removal + g->setNodesAndLinksProtection( true ); + c = g->removeNode( n ); + REQUIRE( !c ); + g->setNodesAndLinksProtection( false ); + c = g->removeNode( n ); + REQUIRE( c ); + + std::cout << "####### Graph after sink and source removal\n"; + inspectGraph( *g ); + } +} diff --git a/tests/unittest/Dataflow/nodes.cpp b/tests/unittest/Dataflow/nodes.cpp new file mode 100644 index 00000000000..030134169b1 --- /dev/null +++ b/tests/unittest/Dataflow/nodes.cpp @@ -0,0 +1,423 @@ +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +//! [Create a source to sink graph for type T] +using namespace Ra::Dataflow::Core; +template +std::tuple, std::shared_ptr, PortBase*> +createGraph( + const std::string& name, + typename Functionals::BinaryOpNode::BinaryOperator f ) { + using TestNode = Functionals::BinaryOpNode; + auto g = new DataflowGraph { name }; + + auto source_a = std::make_shared>( "a" ); + g->addNode( source_a ); + auto a = g->getDataSetter( "a_to" ); + REQUIRE( a->getNode() == g ); + + auto source_b = std::make_shared>( "b" ); + g->addNode( source_b ); + auto b = g->getDataSetter( "b_to" ); + REQUIRE( b->getNode() == g ); + + auto sink = std::make_shared>( "r" ); + g->addNode( sink ); + auto r = g->getDataGetter( "r_from" ); + REQUIRE( r->getNode() == g ); + + auto op = std::make_shared( "operator", f ); + // op->setOperator( f ); + g->addNode( op ); + + REQUIRE( g->addLink( source_a, "to", op, "a" ) ); + REQUIRE( g->addLink( op, "r", sink, "from" ) ); + REQUIRE( !g->compile() ); + // this will not execute the graph as it does not compile + g->execute(); + REQUIRE( !g->isCompiled() ); + // add missing link + REQUIRE( g->addLink( source_b, "to", op, "b" ) ); + + return { g, a, b, r }; +} + +TEST_CASE( "Dataflow/Core/Nodes", "[Dataflow][Core][Nodes]" ) { + SECTION( "Operations on Scalar" ) { + using DataType = Scalar; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator add = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a + b; }; + + auto [g, a, b, r] = createGraph( "test scalar binary op", add ); + + DataType x { 1_ra }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType y { 2_ra }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + // As graph was modified since last compilation, this will recompile the graph + g->execute(); + + auto& z = r->getData(); + REQUIRE( z == x + y ); + + std::cout << x << " + " << y << " == " << z << "\n"; + + g->destroy(); + delete g; + } + + SECTION( "Operations on Vectors" ) { + using DataType = Ra::Core::Vector3; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator add = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a + b; }; + + auto [g, a, b, r] = createGraph( "test Vector3 binary op", add ); + + DataType x { 1_ra, 2_ra, 3_ra }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType y { 3_ra, 2_ra, 1_ra }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + REQUIRE( z == x + y ); + + std::cout << "[" << x.transpose() << "] + [" << y.transpose() << "] == [" << z.transpose() + << "]\n"; + + g->destroy(); + delete g; + } + + SECTION( "Operations on VectorArrays" ) { + using DataType = Ra::Core::VectorArray; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator add = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a + b; }; + + auto [g, a, b, r] = createGraph( "test Vector3 binary op", add ); + + DataType x { { 1_ra, 2_ra }, { 3_ra, 4_ra } }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType y { { 5_ra, 6_ra }, { 7_ra, 8_ra } }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x[i] + y[i] ); + } + + std::cout << "{ "; + for ( const auto& t : x ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} + { "; + for ( const auto& t : y ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + + g->destroy(); + delete g; + } + + SECTION( "Operations between VectorArray and Scalar" ) { + using DataType_a = Ra::Core::VectorArray; + using DataType_b = Scalar; + // How to do this ? Eigen generates an error due to align allocation + // using DataType_r = Ra::Core::VectorArray< decltype( std::declval() * + // std::declval() ) >; + using DataType_r = Ra::Core::VectorArray; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator op = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a * b; }; + auto [g, a, b, r] = createGraph( + "test Vector2 x Scalar binary op", op ); + + DataType_a x { { 1_ra, 2_ra }, { 3_ra, 4_ra } }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType_b y { 5_ra }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x[i] * y ); + } + + std::cout << "{ "; + for ( const auto& t : x ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} * " << y << " = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + + // change operator + auto opNode = std::dynamic_pointer_cast( g->getNode( "operator" ) ); + REQUIRE( opNode != nullptr ); + if ( opNode ) { + typename TestNode::BinaryOperator f = []( typename TestNode::Arg1_type arg1, + typename TestNode::Arg2_type arg2 ) -> + typename TestNode::Res_type { return arg1 / arg2; }; + opNode->setOperator( f ); + } + g->execute(); + + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x[i] / y ); + } + + std::cout << "{ "; + for ( const auto& t : x ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} / " << y << " = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + g->destroy(); + delete g; + } + + SECTION( "Operations between Scalar and VectorArray" ) { + using namespace Ra::Dataflow::Core; + using DataType_a = Scalar; + using DataType_b = Ra::Core::VectorArray; + using DataType_r = Ra::Core::VectorArray; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator op = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a * b; }; + auto [g, a, b, r] = createGraph( + "test Vector2 x Scalar binary op", op ); + + DataType_a x { 4_ra }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType_b y { { 1_ra, 2_ra }, { 3_ra, 4_ra } }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x * y[i] ); + } + + std::cout << x << " * { "; + for ( const auto& t : y ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + g->destroy(); + delete g; + } + + SECTION( "Transform/reduce/filter/test" ) { + //! [Create a complex transform/reduce graph] + auto g = new DataflowGraph( "Complex graph" ); + using VectorType = Ra::Core::VectorArray; + + // Source of a vector of Scalar : random vector + auto nodeS = std::make_shared( "s" ); + + // Source of an operator on scalars : f(x) = 2*x + using DoubleFunction = Sources::FunctionSourceNode::function_type; + DoubleFunction doubleMe = []( const Scalar& x ) -> Scalar { return 2_ra * x; }; + auto nodeD = std::make_shared>( "d" ); + nodeD->setData( &doubleMe ); + + // Source of a Scalar : mean neutral element 0_ra + auto nodeN = std::make_shared( "n" ); + nodeN->setData( 0_ra ); + + // Source of a reduction operator : compute the mean using Welford online algo + using ReduceOperator = Sources::FunctionSourceNode; + struct MeanOperator { + size_t n { 0 }; + Scalar operator()( const Scalar& m, const Scalar& x ) { + return m + ( ( x - m ) / ( ++n ) ); + } + }; + auto nodeM = std::make_shared( "m" ); + ReduceOperator::function_type m = MeanOperator(); + + // Reduce node : will compute the mean + using MeanCalculator = Functionals::ReduceNode; + auto meanCalculator = std::make_shared( "mean" ); + + // Sink for the mean + auto nodeR = std::make_shared( "r" ); + + // Transform operator, will double the vectors' values + auto nodeT = std::make_shared( "twice" ); + + // Will compute the mean on the doubled vector + auto doubleMeanCalculator = std::make_shared( "double mean" ); + + // Sink for the double mean + auto nodeRD = std::make_shared( "rd" ); + + // Source for a comparison functor , eg f(x, y) -> 2*x == y + auto nodePred = std::make_shared( "predicate" ); + Sources::ScalarBinaryPredicateSource::function_type predicate = + []( const Scalar& a, const Scalar& b ) -> bool { return 2_ra * a == b; }; + nodePred->setData( &predicate ); + + // Boolean sink for the validation result + auto sinkB = std::make_shared( "test" ); + + // Node for coparing the results of the computation graph + auto validator = + std::make_shared>( "validator" ); + + g->addNode( nodeS ); + g->addNode( nodeD ); + g->addNode( nodeN ); + g->addNode( nodeM ); + g->addNode( nodeR ); + g->addNode( meanCalculator ); + g->addNode( doubleMeanCalculator ); + g->addNode( nodeT ); + g->addNode( nodeRD ); + + bool linkAdded; + linkAdded = g->addLink( nodeS, "to", meanCalculator, "in" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeM, "f", meanCalculator, "f" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeN, "to", meanCalculator, "init" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( meanCalculator, "out", nodeR, "from" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeS, "to", nodeT, "in" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeD, "f", nodeT, "f" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeT, "out", doubleMeanCalculator, "in" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( doubleMeanCalculator, "out", nodeRD, "from" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeM, "f", doubleMeanCalculator, "f" ); + REQUIRE( linkAdded == true ); + + g->addNode( nodePred ); + g->addNode( sinkB ); + g->addNode( validator ); + linkAdded = g->addLink( meanCalculator, "out", validator, "a" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( doubleMeanCalculator, "out", validator, "b" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodePred, "f", validator, "f" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( validator, "r", sinkB, "from" ); + REQUIRE( linkAdded == true ); + + auto input = g->getDataSetter( "s_to" ); + auto output = g->getDataGetter( "r_from" ); + auto outputD = g->getDataGetter( "rd_from" ); + auto outputB = g->getDataGetter( "test_from" ); + auto inputR = g->getDataSetter( "m_f" ); + if ( inputR == nullptr ) { std::cout << "Failed to get the graph function input !!\n"; } + + // Inspect the graph interface : inputs and outputs port + auto inputs = g->getAllDataSetters(); + std::cout << "Input ports (" << inputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs ) { + std::cout << "\t\"" << portName << "\" accepting type " << portType << "\n"; + } + auto outputs = g->getAllDataGetters(); + std::cout << "Output ports (" << outputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs ) { + std::cout << "\t\"" << portName << "\" generating type " << portType << "\n"; + } + + if ( !g->compile() ) { std::cout << "Compilation error !!"; } + + // Set input/ouput data + VectorType test; + input->setData( &test ); + + test.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + for ( size_t n = 0; n < test.capacity(); ++n ) { + test.push_back( dis( gen ) ); + } + + // No need to do this as mean operator source has a copy of a functor + ReduceOperator::function_type m1 = MeanOperator(); + inputR->setData( &m1 ); + + g->execute(); + + auto& result = output->getData(); + auto& resultD = outputD->getData(); + auto& resultB = outputB->getData(); + + std::cout << "Computed mean ( ref ): " << result << "\n"; + std::cout << "Computed mean ( tra ): " << resultD << "\n"; + std::cout << std::boolalpha; + std::cout << "Ratio ( expected 2 ): " << resultD / result << " -- validator --> " + << resultB << "\n"; + + std::cout << '\n'; + + REQUIRE( resultD / result == 2_ra ); + REQUIRE( resultB ); + // uncomment this if you want to edit the generated graph with GraphEditor + // g->saveToJson( "Transform-reduce.json" ); + g->destroy(); + delete g; + //! [Create a complex transform/reduce graph] + } +} diff --git a/tests/unittest/Dataflow/serialization.cpp b/tests/unittest/Dataflow/serialization.cpp new file mode 100644 index 00000000000..fe53aabe0c9 --- /dev/null +++ b/tests/unittest/Dataflow/serialization.cpp @@ -0,0 +1,106 @@ +#include + +#include +#include + +#include + +#include + +#include +#include +#include +#include + +TEST_CASE( "Dataflow/Core/DataflowGraph", "[Dataflow][Core][DataflowGraph]" ) { + SECTION( "Execution and modification of a graph" ) { + using namespace Ra::Dataflow::Core; + using DataType = Scalar; + DataflowGraph g { "original graph" }; + g.addJsonMetaData( + { { "extra", { { "info", "missing operators on functional node" } } } } ); + auto source_a = std::make_shared>( "a" ); + g.addNode( source_a ); + auto a = g.getDataSetter( "a_to" ); + auto source_b = std::make_shared>( "b" ); + g.addNode( source_b ); + auto b = g.getDataSetter( "b_to" ); + auto sink = std::make_shared>( "r" ); + g.addNode( sink ); + auto r = g.getDataGetter( "r_from" ); + using TestNode = Functionals::BinaryOpNode; + TestNode::BinaryOperator add = []( TestNode::Arg1_type a, + TestNode::Arg2_type b ) -> TestNode::Res_type { + return a + b; + }; + auto op_unique = std::make_shared( "addition" ); + op_unique->setOperator( add ); + auto added = g.addNode( op_unique ); + REQUIRE( added ); + g.addLink( source_a, "to", op_unique, "a" ); + g.addLink( op_unique, "r", sink, "from" ); + g.addLink( source_b, "to", op_unique, "b" ); + + // execution of the original graph + DataType x { 1_ra }; + a->setData( &x ); + DataType y { 2_ra }; + b->setData( &y ); + // Execute initial graph"; + g.execute(); + auto z = r->getData(); + REQUIRE( z == x + y ); + + // Save the graph + std::string tmpdir { "tmpDir4Tests" }; + std::filesystem::create_directories( tmpdir ); + g.saveToJson( tmpdir + "/GraphSerializationTest.json" ); + g.destroy(); + // this does nothing as g was destroyed + g.execute(); + + // Create a new graph and load from the saved graph + DataflowGraph g1 { "loaded graph" }; + g1.loadFromJson( tmpdir + "/GraphSerializationTest.json" ); + + // Setting the unserializable data on nodes (functions) + auto addition = g1.getNode( "addition" ); + REQUIRE( addition != nullptr ); + REQUIRE( addition->getTypeName() == Functionals::BinaryOpScalar::getTypename() ); + auto typedAddition = std::dynamic_pointer_cast( addition ); + REQUIRE( typedAddition != nullptr ); + if ( typedAddition != nullptr ) { typedAddition->setOperator( add ); } + + // Execute loaded graph + // Data delivered by the source nodes are the one saved by the original graph + g1.execute(); + auto r_loaded = g1.getDataGetter( "r_from" ); + auto& z_loaded = r_loaded->getData(); + REQUIRE( z_loaded == z ); + + auto a_loaded = g1.getDataSetter( "a_to" ); + auto b_loaded = g1.getDataSetter( "b_to" ); + DataType xp { 2_ra }; + a_loaded->setData( &xp ); + DataType yp { 3_ra }; + b_loaded->setData( &yp ); + g1.execute(); + REQUIRE( z_loaded == 5 ); + + // Reset sources to use the loaded data loaded + g1.releaseDataSetter( "a_to" ); + g1.releaseDataSetter( "b_to" ); + g1.execute(); + REQUIRE( z_loaded == z ); + + // reactivate the dataSetter and change the data delivered by a (copy data into a) + g1.activateDataSetter( "b_to" ); + auto loadedSource_a = + std::dynamic_pointer_cast>( g1.getNode( "a" ) ); + Scalar newX = 3_ra; + loadedSource_a->setData( newX ); + g1.execute(); + REQUIRE( z_loaded == 6 ); + std::filesystem::remove_all( tmpdir ); + } +} diff --git a/tests/unittest/Dataflow/sourcesandsinks.cpp b/tests/unittest/Dataflow/sourcesandsinks.cpp new file mode 100644 index 00000000000..66f939ddfea --- /dev/null +++ b/tests/unittest/Dataflow/sourcesandsinks.cpp @@ -0,0 +1,159 @@ +#include + +#include +#include + +#include + +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +//! [Create a source to sink graph for type T] +template +void testGraph( const std::string& name, T in, T& out ) { + auto g = std::make_unique( name ); + auto source = std::make_shared>( "in" ); + auto sink = std::make_shared>( "out" ); + g->addNode( source ); + g->addNode( sink ); + auto linked = g->addLink( source, "to", sink, "from" ); + if ( !linked ) { std::cerr << "Error linking source and sink nodes.\n"; } + REQUIRE( linked ); + + auto input = g->getDataSetter( "in_to" ); + REQUIRE( input != nullptr ); + auto output = g->getDataGetter( "out_from" ); + REQUIRE( output != nullptr ); + + auto compiled = g->compile(); + if ( !compiled ) { std::cerr << "Error compiling graph.\n"; } + REQUIRE( compiled ); + + std::cout << "Setting " << simplifiedDemangledType() << " data on interface port ... "; + input->setData( &in ); + + g->execute(); + + T r = output->getData(); + std::cout << "Getting a " << simplifiedDemangledType( r ) << " from interface port ... "; + out = r; + + g->releaseDataSetter( "in_to" ); + source->setData( in ); + + nlohmann::json graphData; + g->toJson( graphData ); + g->destroy(); + + g = std::make_unique( name ); + g->fromJson( graphData ); + auto ok = g->execute(); + REQUIRE( ok ); +} +//! [Create a source to sink graph for type T] +TEST_CASE( "Dataflow/Core/Sources and Sinks", "[Dataflow][Core][Sources and Sinks]" ) { + SECTION( "Operations on base type : Scalar" ) { + using DataType = Scalar; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + + DataType x { 3.141592_ra }; + DataType y { 0_ra }; + testGraph( "Test on Scalar", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : float" ) { + using DataType = float; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 3.141592 }; + DataType y { 0 }; + testGraph( "Test on float", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : double" ) { + using DataType = double; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 3.141592 }; + DataType y { 0 }; + testGraph( "Test on float", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : int" ) { + using DataType = int; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { -3 }; + DataType y { 0 }; + testGraph( "Test on int", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : unsigned int" ) { + using DataType = unsigned int; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 3 }; + DataType y { 0 }; + testGraph( "Test on unsigned int", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : bool" ) { + using DataType = bool; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { true }; + DataType y { false }; + testGraph( "Test on bool", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Vector2" ) { + using DataType = Ra::Core::Vector2; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 1_ra, 2_ra }; + DataType y; + testGraph( "Test on Vector2", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Vector3" ) { + using DataType = Ra::Core::Vector3; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 1_ra, 2_ra, 3_ra }; + DataType y; + testGraph( "Test on Vector3", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Vector4" ) { + using DataType = Ra::Core::Vector4; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 1_ra, 2_ra, 3_ra, 4_ra }; + DataType y; + testGraph( "Test on Vector4", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Color" ) { + using DataType = Ra::Core::Utils::Color; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x = Ra::Core::Utils::Color::Skin(); + DataType y; + testGraph( "Test on Color", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } +} diff --git a/tests/unittest/data/Dataflow/ExampleGraph.json b/tests/unittest/data/Dataflow/ExampleGraph.json new file mode 100644 index 00000000000..96025e0cb30 --- /dev/null +++ b/tests/unittest/data/Dataflow/ExampleGraph.json @@ -0,0 +1,220 @@ +{ + "instance": "Example graph", + "model": { + "graph": { + "connections": [ + { + "in_node": "reduced vector", + "in_port": "from", + "out_node": "Collection reducer - original collection", + "out_port": "out" + }, + { + "in_node": "Collection reducer - original collection", + "in_port": "in", + "out_node": "Vector", + "out_port": "to" + }, + { + "in_node": "Collection reducer - original collection", + "in_port": "f", + "out_node": "Reducer", + "out_port": "f" + }, + { + "in_node": "Collection reducer - original collection", + "in_port": "init", + "out_node": "Neutral", + "out_port": "to" + }, + { + "in_node": "Collection reducer - transformed collection", + "in_port": "in", + "out_node": "Collection transformer", + "out_port": "out" + }, + { + "in_node": "Collection reducer - transformed collection", + "in_port": "f", + "out_node": "Reducer", + "out_port": "f" + }, + { + "in_node": "Collection transformer", + "in_port": "in", + "out_node": "Vector", + "out_port": "to" + }, + { + "in_node": "Collection transformer", + "in_port": "f", + "out_node": "Doubler", + "out_port": "f" + }, + { + "in_node": "transformed/reduced vector", + "in_port": "from", + "out_node": "Collection reducer - transformed collection", + "out_port": "out" + }, + { + "in_node": "validation value", + "in_port": "from", + "out_node": "Validator : evaluate the validation predicate", + "out_port": "r" + }, + { + "in_node": "Validator : evaluate the validation predicate", + "in_port": "a", + "out_node": "Collection reducer - original collection", + "out_port": "out" + }, + { + "in_node": "Validator : evaluate the validation predicate", + "in_port": "b", + "out_node": "Collection reducer - transformed collection", + "out_port": "out" + }, + { + "in_node": "Validator : evaluate the validation predicate", + "in_port": "f", + "out_node": "Validator", + "out_port": "f" + } + ], + "factories": [], + "nodes": [ + { + "instance": "Vector", + "model": { + "comment": "Unable to save data when serializing a SingleDataSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -288.0, + "y": 58.0 + } + }, + { + "instance": "Doubler", + "model": { + "comment": "Unable to save data when serializing a FunctionSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -290.0, + "y": 139.0 + } + }, + { + "instance": "Neutral", + "model": { + "name": "Source", + "number": 0.0 + }, + "position": { + "x": -289.0, + "y": -83.0 + } + }, + { + "instance": "Reducer", + "model": { + "comment": "Unable to save data when serializing a FunctionSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -292.0, + "y": 224.0 + } + }, + { + "instance": "reduced vector", + "model": { + "name": "Sink" + }, + "position": { + "x": 459.0, + "y": -140.0 + } + }, + { + "instance": "Collection reducer - original collection", + "model": { + "comment": "Reduce operator could not be serialized for Reduce>", + "name": "Reduce>" + }, + "position": { + "x": 159.0, + "y": -133.0 + } + }, + { + "instance": "Collection reducer - transformed collection", + "model": { + "comment": "Reduce operator could not be serialized for Reduce>", + "name": "Reduce>" + }, + "position": { + "x": 461.0, + "y": 79.0 + } + }, + { + "instance": "Collection transformer", + "model": { + "comment": "Transform operator could not be serialized for Transform>", + "name": "Transform>" + }, + "position": { + "x": 159.0, + "y": 53.0 + } + }, + { + "instance": "transformed/reduced vector", + "model": { + "name": "Sink" + }, + "position": { + "x": 720.0, + "y": -62.0 + } + }, + { + "instance": "Validator", + "model": { + "comment": "Unable to save data when serializing a FunctionSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -291.0, + "y": 309.0 + } + }, + { + "instance": "validation value", + "model": { + "name": "Sink" + }, + "position": { + "x": 969.0, + "y": 197.0 + } + }, + { + "instance": "Validator : evaluate the validation predicate", + "model": { + "comment": "Binary operator could not be serialized for BinaryOp bool>", + "name": "BinaryOp bool>" + }, + "position": { + "x": 705.0, + "y": 158.0 + } + } + ] + }, + "name": "Core DataflowGraph" + } +} diff --git a/tests/unittest/data/Dataflow/NotAJsonFile.json b/tests/unittest/data/Dataflow/NotAJsonFile.json new file mode 100644 index 00000000000..4fc8f92e8c1 --- /dev/null +++ b/tests/unittest/data/Dataflow/NotAJsonFile.json @@ -0,0 +1 @@ +# This file is not a valid json file