diff --git a/.gitignore b/.gitignore index 26bf0dcc869..0a4f7641d72 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ *.swp *.old \#*\# +compile_commands.json + # All dot files, if you wish to track one, just add it .* diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ae4e1e6940e..e2390ec615e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -25,6 +25,7 @@ foreach( SimpleAnimation SimpleSimulation TexturedQuad + CurveEditor ) add_subdirectory(${APP}) add_dependencies(${PROJECT_NAME} ${APP}) diff --git a/examples/CurveEditor/BezierUtils/CubicBezierApproximation.hpp b/examples/CurveEditor/BezierUtils/CubicBezierApproximation.hpp new file mode 100644 index 00000000000..47c1e5c92aa --- /dev/null +++ b/examples/CurveEditor/BezierUtils/CubicBezierApproximation.hpp @@ -0,0 +1,352 @@ +#pragma once + +#include "LeastSquareSystem.hpp" +#include +#include +#include + +#include + +using namespace Ra::Core; +using namespace Ra::Core::Geometry; + +class CubicBezierApproximation +{ + public: + CubicBezierApproximation() {} + + void init( const VectorArray& data, + int nb_min_bz = 1, + float epsilon = 0.1, + std::ofstream* logfile = nullptr ) { + if ( logfile != nullptr ) { + ( *logfile ) << "# Initialising optimization with epsilon " << epsilon << std::endl; + ( *logfile ) << "eps= " << epsilon << ";" << std::endl; + } + + m_data = data; + m_distThreshold = epsilon; + m_logfile = logfile; + + recomputeJunctions( nb_min_bz + 1 ); + if ( !data.empty() ) { m_dim = data[0].rows(); } + + updateParameters(); + + m_step = 0; + + m_isInitialized = true; + m_hasComputed = false; + } + + bool compute() { + using namespace Ra::Core::Utils; + + if ( !m_isInitialized ) { + LOG( logERROR ) << "CubicBezierApproximation is not initialized"; + return false; + } + + if ( m_hasComputed ) { + LOG( logERROR ) << "CubicBezierApproximation has already a solution"; + return false; + } + + if ( m_data.size() < 4 ) { + auto okflag = computeDegeneratedSolution(); + if ( !okflag ) { return false; } + m_hasComputed = true; + return true; + } + + ++m_step; + printPolygonMatlab( m_data, "P_" + std::to_string( m_step ) ); + + auto okflag = computeLeastSquareSolution(); + if ( !okflag ) { return false; } + + printPolygonMatlab( m_curSol.getCtrlPoints(), "B_" + std::to_string( m_step ) ); + + float err = evaluateSolution(); + + if ( err > m_distThreshold ) { + bool stopFlag { recomputeJunctions( m_bzjunctions.size() + 1 ) }; + if ( !stopFlag ) { return false; } + return compute(); + } + + m_hasComputed = true; + + return true; + } + + bool compute( int nbz ) { + using namespace Ra::Core::Utils; + + if ( !m_isInitialized ) { + LOG( logERROR ) << "CubicBezierApproximation is not initialized"; + return false; + } + + if ( m_hasComputed ) { + LOG( logERROR ) << "CubicBezierApproximation has already a solution"; + return false; + } + + if ( m_data.size() < 4 ) { + auto okflag = computeDegeneratedSolution(); + if ( !okflag ) { return false; } + m_hasComputed = true; + return true; + } + + ++m_step; + printPolygonMatlab( m_data, "P_" + std::to_string( m_step ) ); + + auto okflag = computeLeastSquareSolution(); + if ( !okflag ) { return false; } + + printPolygonMatlab( m_curSol.getCtrlPoints(), "B_" + std::to_string( m_step ) ); + + if ( m_curSol.getNbBezier() < nbz ) { + bool stopFlag { recomputeJunctions( m_bzjunctions.size() + 1 ) }; + + if ( !stopFlag ) { return false; } + + return compute( nbz ); + } + + m_hasComputed = true; + + return true; + } + + PiecewiseCubicBezier getSolution() const { return m_curSol; } + + int getNstep() const { return m_step; } + + private: + bool m_isInitialized { false }; + bool m_hasComputed { false }; + int m_dim { 0 }; + int m_step { 0 }; + float m_distThreshold { 0.f }; + VectorArray m_data; + std::vector m_params; + std::set m_bzjunctions; + PiecewiseCubicBezier m_curSol; + + std::ofstream* m_logfile { nullptr }; + + void updateParameters() { + if ( m_bzjunctions.size() == 0 ) return; + + m_params.resize( m_data.size() ); + + auto b { m_bzjunctions.cbegin() }; + int d1 { *b }; + int numseg { 0 }; + while ( ( ++b ) != m_bzjunctions.cend() ) { + int d0 { d1 }; + d1 = *b; + + float totalDist { 0. }; + m_params[d0] = 0; + for ( int i = d0 + 1; i <= d1; ++i ) { + float dist { ( m_data[i] - m_data[i - 1] ).norm() }; + m_params[i] = m_params[i - 1] + dist; + totalDist += dist; + } + + for ( int i = d0; i <= d1; ++i ) { + m_params[i] = ( m_params[i] / totalDist ) + numseg; + } + ++numseg; + } + } + + float errorAt( int i ) { return ( m_data[i] - m_curSol.f( m_params[i] ) ).squaredNorm(); } + + bool recomputeJunctions( int nb_junctions ) { + m_bzjunctions.clear(); + + int a { 0 }; + int b { (int)( m_data.size() - 1 ) }; + float h = ( b - a ) / (float)( nb_junctions - 1 ); + std::vector xs( nb_junctions ); + typename std::vector::iterator it; + float val; + for ( it = xs.begin(), val = a; it != xs.end(); ++it, val += h ) + *it = (int)val; + + for ( int x : xs ) { + m_bzjunctions.insert( x ); + } + + updateParameters(); + + return true; + } + + float evaluateSolution() { + float errMax { -1 }; + using namespace Ra::Core::Utils; + // LOG(logDEBUG) << "Evaluation"; + for ( int i = 0; i < (int)m_data.size(); ++i ) { + // LOG(logDEBUG) << i << " : " << errorAt(i); + errMax = std::max( errMax, errorAt( i ) ); + } + return errMax; + } + + void computeConstraints( LeastSquareSystem& lss ) { + int nbz { (int)( m_bzjunctions.size() ) - 1 }; + + auto computePointDistanceConstraint = [nbz]( float u ) { + std::map A_cstr; + auto locpar = PiecewiseCubicBezier::getLocalParameter( u, nbz ); + auto bcoefs = CubicBezier::bernsteinCoefsAt( locpar.second ); + int bi = locpar.first; + + for ( int i = 0; i < 4; ++i ) { + A_cstr[3 * bi + i] = bcoefs[i]; + } + + return A_cstr; + }; + + std::set::const_iterator bzit = m_bzjunctions.cbegin(); + for ( int b = 0; b < nbz; ++b ) { + int s0 { *bzit }; + int s1 { *( ++bzit ) }; + int nb_pts_in_bz { s1 - s0 + 1 }; + + if ( nb_pts_in_bz >= 4 ) { + // Bezier segment is constrained by at least 4 points => well-posed system + for ( int s = s0; s <= s1; ++s ) { + if ( ( s == s0 ) && ( s0 != 0 ) ) { continue; } + // The problem is over (or perfectly) constrained : for each sample in between + // the Bezier junction we minimize the distance with the optimized curve at the + // associated parameter + lss.addConstraint( computePointDistanceConstraint( m_params[s] ), m_data[s] ); + } + } + else { + // Bezier segment is constrained by less than 4 points => degenerated case + VectorArray data( m_data.cbegin() + s0, m_data.cbegin() + s1 + 1 ); + VectorArray cpts; + + // The problem is underconstrained : we compute one solution that perfectly fits the + // data And we constrain the control points of the solution to match this solution + computeDegeneratedSolution( data, cpts ); + + for ( int k = 0; k < 4; ++k ) { + if ( ( k == 0 ) && ( s0 != 0 ) ) { continue; } + lss.addConstraint( { { 3 * b + k, 1 } }, cpts[k] ); + } + } + } + } + + bool computeLeastSquareSolution() { + int nbz { (int)( m_bzjunctions.size() ) - 1 }; + int nvar { 3 * nbz + 1 }; + + if ( nbz < 1 ) { + using namespace Ra::Core::Utils; + LOG( logERROR ) << "Error while approximating stroke : not enough points"; + return false; + } + + LeastSquareSystem lss( nvar, m_dim ); + computeConstraints( lss ); + + Eigen::MatrixXf sol; + if ( !lss.solve( sol ) ) { return false; } + + VectorArray cpts( nvar ); + for ( int i = 0; i < nvar; ++i ) { + cpts[i] = Curve2D::Vector( sol.row( i ) ); + } + + m_curSol.setCtrlPoints( cpts ); + + return true; + } + + bool computeDegeneratedSolution( const VectorArray& data, + VectorArray& cpts ) { + + int npts { (int)data.size() }; + if ( ( npts == 0 ) || ( npts >= 4 ) ) { return false; } + + cpts.resize( 4 ); + if ( npts == 1 ) { + // One point in the stroke : the Bezier curve is degenerated + cpts[0] = data[0]; + cpts[1] = data[0]; + cpts[2] = data[0]; + cpts[3] = data[0]; + } + if ( npts == 2 ) { + // Two points in the stroke : straight line segment + cpts[0] = data[0]; + cpts[1] = ( 2. / 3. ) * data[0] + ( 1. / 3. ) * data[1]; + cpts[2] = ( 1. / 3. ) * data[0] + ( 2. / 3. ) * data[1]; + cpts[3] = data[1]; + } + if ( npts == 3 ) { + // Three points in the stroke : we find the quadratic bezier best fitting solution + // and raise the degree to 3 + float l0 { ( data[1] - data[0] ).norm() }; + float l1 { ( data[2] - data[1] ).norm() }; + float tau { l0 / ( l0 + l1 ) }; // parameter of the middle point in the curve + + /** === Details of the computation + * p0,p1,p2 the data to approximate + * t : parameter for p1 in the fitted bezier + * + * best fitting quadratic bezier has control points : + * { p0, (1/(2t(1-t))*(p1 - (t^2)p2 - ((1-t)^2)p0), p2 } + * + * we can raise degree of the bezier by applying : + * { b0, b0 + (2/3)*(b1-b0), b2 + (2/3)*(b1-b2), b2 } + * where {b0, b1, b2} are the control points of the quadratic + * + * hence the control points of the best fitting cubic bezier + * { p0, p0 + (2/3)*( (1/(2t(1-t))*(p1 - (t^2)p2 - ((1-t)^2)p0) - p0) + * , p2 + (2/3)*( (1/(2t(1-t))*(p1 - (t^2)p2 - ((1-t)^2)p0) - p0), p2} + * with some simplifications, we get + * { p0, (1/(3t(1-t))*(p1 - (t^2)p2 + (-1+2t)*(1-t)p0) + * , (1/(3t(1-t))*(p1 + t(1-2t)p2 - (1-t)^2)p0), p2} + * + **/ + + float omt { 1 - tau }; + float fact { 1.f / ( 3 * tau * omt ) }; + cpts[0] = data[0]; + cpts[1] = fact * ( ( -1 + 2 * tau ) * omt * data[0] + data[1] - tau * tau * data[2] ); + cpts[2] = fact * ( -omt * omt * data[0] + data[1] + ( 1 - 2 * tau ) * tau * data[2] ); + cpts[3] = data[2]; + } + + return true; + } + + bool computeDegeneratedSolution() { + VectorArray cpts; + if ( !computeDegeneratedSolution( m_data, cpts ) ) { return false; } + m_curSol.setCtrlPoints( cpts ); + return true; + } + + void printPolygonMatlab( const VectorArray& poly, + const std::string& varname ) { + if ( m_logfile == nullptr ) { return; } + ( *m_logfile ) << varname << "= ["; + for ( unsigned int i = 0; i < poly.size(); i++ ) { + ( *m_logfile ) << "[" << poly[i].x() << ";" << poly[i].y() << "] "; + } + ( *m_logfile ) << "];" << std::endl; + } +}; diff --git a/examples/CurveEditor/BezierUtils/LeastSquareSystem.hpp b/examples/CurveEditor/BezierUtils/LeastSquareSystem.hpp new file mode 100644 index 00000000000..c455c31e3ef --- /dev/null +++ b/examples/CurveEditor/BezierUtils/LeastSquareSystem.hpp @@ -0,0 +1,198 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +using Eigen::MatrixXf; +using Eigen::SparseMatrix; +using Eigen::Triplet; +using Eigen::VectorXf; + +/** + * @brief The LeastSquareSystem class allows + * setting up and solving an n-dimensional least square system + * of the form Ax=b. + */ +class LeastSquareSystem +{ + public: + using CoefTriplet = Triplet; + using SparseMat = SparseMatrix; + + /** + * @brief LeastSquareSystem of the dimension given by the parameters + * @param number of variables of the system + * @param dimension of the variables (optional) + * @param expected number of constraints (optional) + */ + explicit LeastSquareSystem( int nvar, int dim = 1, int exp_ncstr = 0 ) : + m_nvar( nvar ), m_dim( dim ) { + m_constrainedVar.resize( m_nvar, false ); + if ( exp_ncstr > 0 ) { m_bval.reserve( exp_ncstr ); } + } + + /** + * @brief Adding a constraint to the system + * @param coefs of the corresponding row in the matrix A, + * only non zero coefs will be stored in the sparse matrix + * coefs are 1D : same for all dimension of the variable + * @param value of the corresponding row in b (n-dimensional) + * @param weight of the constraint (optional) + * @return 0 if everything went ok, -1 otherwise (the constraint is not added in this case) + */ + int addConstraint( std::map coefs, VectorXf res, float w = 1 ) { + using namespace Ra::Core::Utils; + if ( coefs.empty() || ( res.rows() != m_dim ) ) { + LOG( logERROR ) << "Constraint has wrong dimension"; + return -1; + } + + bool anyNonZeroCoef { false }; + + for ( const auto& [id_var, coef] : coefs ) { + if ( w * fabs( coef ) < 1e-8 ) { continue; } + anyNonZeroCoef = true; + for ( int d = 0; d < m_dim; ++d ) { + m_tripletList.emplace_back( + CoefTriplet( m_dim * m_ncstr + d, m_dim * id_var + d, w * coef ) ); + } + m_constrainedVar[id_var] = true; + } + + if ( anyNonZeroCoef ) { + m_bval.push_back( w * res ); + ++m_ncstr; + return 0; + } + + return -1; + } + + /** + * @brief Call this once all constraints have been added to the system. + * Sets up and solves the LSS if it is solvable. + * @param writes the solution in a (nvar x ndim) matrix (a row = a variable) + * @return true if everything went ok, false if the system is unsolvable or badly built + */ + bool solve( MatrixXf& solution ) const { + using namespace Ra::Core::Utils; + + if ( !isSolvable() ) { + LOG( logERROR ) << "Least square system unsolvable"; + return false; + } + + SparseMat A; + VectorXf b; + bool setupFlag { this->setUpSystem( A, b ) }; + if ( !setupFlag ) { + LOG( logERROR ) << "Error while setting up least square system"; + return false; + } + + VectorXf flat_solution; + bool solveFlag { jacobiSVD( A, b, flat_solution ) }; + if ( !solveFlag ) { + LOG( logERROR ) << "Error while solving the least square system"; + return false; + } + + solution = flat_solution.reshaped( m_nvar, m_dim ); + + return true; + } + + /** + * @brief Prints details of the current state of the system + * @param output log level (optional) + */ + void log( Ra::Core::Utils::TLogLevel logL = Ra::Core::Utils::logINFO ) const { + using namespace Ra::Core::Utils; + + LOG( logL ) << "LSS with " << m_nvar << " variables of dimension " << m_dim << ", and " + << m_ncstr << " constraints"; + + LOG( logL ) << "Triplets (" << m_tripletList.size() << ") : "; + for ( const auto& t : m_tripletList ) { + LOG( logL ) << "(" << t.row() << "," << t.col() << ") = " << t.value(); + } + + LOG( logL ) << "Values (" << m_bval.size() << ")"; + for ( const auto& b : m_bval ) { + LOG( logL ) << b.transpose(); + } + + std::stringstream logss; + logss << "Constrained variables :"; + int v = -1; + for ( const auto& c : m_constrainedVar ) { + logss << "(" << ++v << ": " << c << ") "; + } + LOG( logL ) << logss.str(); + logss.str( "" ); + } + + private: + int m_nvar { 0 }; // number of variables in the system + int m_dim { 1 }; // dimension of the variables (n) + int m_ncstr { 0 }; // number of constraints in the system + + std::vector m_constrainedVar; // constrained variables + std::list m_tripletList; // coefs of the sparse A matrix + std::vector m_bval; // values of the b vector (each coef is n-dimensional) + + /** + * @brief Setting up the matrices of the system + * @param output A sparse (nconstr*ndim) x (nvar*ndim) matrix + * @param output b (nconstr*ndim) vector + * @return true if the system was built properly, false otherwise + */ + bool setUpSystem( SparseMat& A, VectorXf& b ) const { + if ( ( m_ncstr != (int)m_bval.size() ) ) { return false; } + + A.resize( m_ncstr * m_dim, m_nvar * m_dim ); + A.setFromTriplets( m_tripletList.begin(), m_tripletList.end() ); + A.makeCompressed(); + + b.resize( m_ncstr * m_dim ); + for ( int c = 0; c < m_ncstr; ++c ) { + for ( int d = 0; d < m_dim; ++d ) { + b( m_dim * c + d ) = m_bval[c]( d ); + } + } + return true; + } + + /** + * @brief Check if system is solvable. + * Only check if all variable are constrained and if more constraints than variables + * Does not guarantee the rank of the A matrix will be sufficient + * @return true if solvable, false otherwise + */ + bool isSolvable() const { + using namespace Ra::Core::Utils; + if ( m_ncstr < m_nvar ) { + LOG( logERROR ) << "LSS - Underconstrained problem"; + return false; + } + + auto unconstr = std::find( m_constrainedVar.cbegin(), m_constrainedVar.cend(), false ); + if ( unconstr != m_constrainedVar.cend() ) { + LOG( logERROR ) << "LSS - Unconstrained variable(s)"; + return false; + } + + return true; + } + + bool jacobiSVD( const SparseMat& A, const VectorXf& b, VectorXf& x ) const { + MatrixXf denseA( A ); + Eigen::JacobiSVD svd( denseA, Eigen::ComputeThinU | Eigen::ComputeThinV ); + x = svd.solve( b ); + return true; + } +}; diff --git a/examples/CurveEditor/CMakeLists.txt b/examples/CurveEditor/CMakeLists.txt new file mode 100644 index 00000000000..fb3ce82d5fe --- /dev/null +++ b/examples/CurveEditor/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.16) +cmake_policy(SET CMP0071 NEW) +cmake_policy(SET CMP0042 NEW) + +project(CurveEditor VERSION 1.0.0) + +# ------------------------------------------------------------------------------ +# set wanted application defaults for cmake settings +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("Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +# ------------------------------------------------------------------------------ +find_package(Radium REQUIRED Core Engine Gui IO) + +find_qt_package(COMPONENTS Core Widgets REQUIRED) +set(Qt_LIBRARIES Qt::Core Qt::Widgets) +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +# ------------------------------------------------------------------------------ + +set(app_sources main.cpp CurveComponent.cpp PolylineComponent.cpp PointComponent.cpp + Gui/MainWindow.cpp Gui/MyViewer.cpp CurveEditor.cpp +) + +set(app_headers + Gui/MainWindow.hpp + Gui/MyViewer.hpp + CurveComponent.hpp + PolylineComponent.hpp + CurveFactory.hpp + PointComponent.hpp + PointFactory.hpp + BezierUtils/CubicBezierApproximation.hpp + BezierUtils/LeastSquareSystem.hpp + CurveEditor.hpp +) + +set(app_uis) +qt_wrap_ui(app_uis_moc ${app_uis}) + +set(app_resources) + +# to install the app as a redistribuable bundle on macos, add MACOSX_BUNDLE when calling +# add_executable +add_executable( + ${PROJECT_NAME} MACOSX_BUNDLE ${app_sources} ${app_headers} ${app_uis} ${app_resources} +) + +if("${CMAKE_CXX_COMPILER_ID}" MATCHES "MSVC") + target_compile_options( + ${PROJECT_NAME} + PRIVATE /MP + /W4 + /wd4251 + /wd4592 + /wd4127 + /Zm200 + $<$: + /Gw + /GS- + /GL + /GF + > + PUBLIC + ) +endif() + +get_target_property(USE_ASSIMP Radium::IO IO_ASSIMP) +if(${USE_ASSIMP}) + target_compile_definitions(${PROJECT_NAME} PRIVATE "-DIO_USE_ASSIMP") +endif() + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) +target_include_directories( + ${PROJECT_NAME} PRIVATE ${RADIUM_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR} # Moc + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries( + ${PROJECT_NAME} PUBLIC Radium::Core Radium::Engine Radium::Gui Radium::IO ${Qt_LIBRARIES} +) + +# configure the application +configure_radium_app( + NAME ${PROJECT_NAME} PREFIX Examples RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Assets" +) diff --git a/examples/CurveEditor/CurveComponent.cpp b/examples/CurveEditor/CurveComponent.cpp new file mode 100644 index 00000000000..6a9146e9986 --- /dev/null +++ b/examples/CurveEditor/CurveComponent.cpp @@ -0,0 +1,92 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef IO_USE_ASSIMP +# include +#endif + +#include + +using namespace Ra; +using namespace Ra::Core; +using namespace Ra::Core::Utils; +using namespace Ra::Core::Geometry; +using namespace Ra::Engine; +using namespace Ra::Engine::Rendering; +using namespace Ra::Engine::Data; +using namespace Ra::Engine::Scene; + +/** + * This file contains a minimal radium/qt application which shows the geometrical primitives + * supported by Radium + */ + +CurveComponent::CurveComponent( Ra::Engine::Scene::Entity* entity, + Vector3Array ctrlPts, + const std::string& name ) : + Ra::Engine::Scene::Component( name, entity ), m_ctrlPts( ctrlPts ) {} + +/// This function is called when the component is properly +/// setup, i.e. it has an entity. +void CurveComponent::initialize() { + auto plainMaterial = make_shared( "Plain Material" ); + plainMaterial->m_perVertexColor = true; + + auto bezier = CubicBezier( Vector2( m_ctrlPts[0].x(), m_ctrlPts[0].z() ), + Vector2( m_ctrlPts[1].x(), m_ctrlPts[1].z() ), + Vector2( m_ctrlPts[2].x(), m_ctrlPts[2].z() ), + Vector2( m_ctrlPts[3].x(), m_ctrlPts[3].z() ) ); + + auto bezierVertices = Vector3Array(); + auto bezierColors = Vector4Array(); + for ( unsigned int i = 0; i <= 100; i++ ) { + float u = float( i ) / 100.f; + auto fu = bezier.f( u ); + bezierVertices.push_back( Vector3( fu.x(), 0, fu.y() ) ); + bezierColors.push_back( { 1, 0, 0, 1 } ); + } + + // Render mesh + auto renderObject1 = + RenderObject::createRenderObject( "PolyMesh", + this, + RenderObjectType::Geometry, + DrawPrimitives::LineStrip( bezierVertices, bezierColors ), + Ra::Engine::Rendering::RenderTechnique {} ); + renderObject1->setMaterial( plainMaterial ); + addRenderObject( renderObject1 ); + + auto renderObject2 = RenderObject::createRenderObject( + "PolyMesh1", + this, + RenderObjectType::Geometry, + DrawPrimitives::LineStrip( { m_ctrlPts[0], m_ctrlPts[1] }, { { 0, 0, 0, 1 } } ), + Ra::Engine::Rendering::RenderTechnique {} ); + renderObject2->setMaterial( plainMaterial ); + addRenderObject( renderObject2 ); + + auto renderObject3 = RenderObject::createRenderObject( + "PolyMesh1", + this, + RenderObjectType::Geometry, + DrawPrimitives::LineStrip( { m_ctrlPts[2], m_ctrlPts[3] }, { { 0, 0, 0, 1 } } ), + Ra::Engine::Rendering::RenderTechnique {} ); + renderObject3->setMaterial( plainMaterial ); + addRenderObject( renderObject3 ); +} diff --git a/examples/CurveEditor/CurveComponent.hpp b/examples/CurveEditor/CurveComponent.hpp new file mode 100644 index 00000000000..9a8365d608b --- /dev/null +++ b/examples/CurveEditor/CurveComponent.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +class CurveComponent : public Ra::Engine::Scene::Component +{ + + public: + CurveComponent( Ra::Engine::Scene::Entity* entity, + Ra::Core::Vector3Array ctrlPts, + const std::string& name ); + + /// This function is called when the component is properly + /// setup, i.e. it has an entity. + void initialize() override; + + Ra::Core::Vector3Array m_ctrlPts; +}; diff --git a/examples/CurveEditor/CurveEditor.cpp b/examples/CurveEditor/CurveEditor.cpp new file mode 100644 index 00000000000..90490f34e72 --- /dev/null +++ b/examples/CurveEditor/CurveEditor.cpp @@ -0,0 +1,528 @@ +#include +#include +#include +#include +#include +#include + +#include "BezierUtils/CubicBezierApproximation.hpp" +#include "CurveEditor.hpp" + +using namespace Ra::Core; +using namespace Ra::Core::Geometry; +using namespace Ra::Core::Utils; +using namespace Ra::Engine; +using namespace Ra::Engine::Scene; + +CurveEditor::CurveEditor( const VectorArray& polyline, MyViewer* viewer ) : + Entity( "Curve Editor" ), m_viewer( viewer ) { + + m_entityMgr = RadiumEngine::getInstance()->getEntityManager(); + m_roMgr = RadiumEngine::getInstance()->getRenderObjectManager(); + + // Approximate polyline with bezier + CubicBezierApproximation approximator; + approximator.init( polyline ); + approximator.compute(); + auto solution = approximator.getSolution(); + auto solutionPts = solution.getCtrlPoints(); + + // Create and render solution entities + CurveFactory factory; + std::vector allCtrlPts; + m_viewer->makeCurrent(); + for ( unsigned int i = 0; i < solutionPts.size() - 3; i += 3 ) { + Vector3Array ctrlPts { Vector3( solutionPts[i].x(), 0, solutionPts[i].y() ), + Vector3( solutionPts[i + 1].x(), 0, solutionPts[i + 1].y() ), + Vector3( solutionPts[i + 2].x(), 0, solutionPts[i + 2].y() ), + Vector3( solutionPts[i + 3].x(), 0, solutionPts[i + 3].y() ) }; + auto e = factory.createCurveComponent( this, ctrlPts, i / 3 ); + m_curveEntities.push_back( e ); + allCtrlPts.push_back( ctrlPts ); + } + + auto e = PointFactory::createPointComponent( this, allCtrlPts[0][0], { 0 }, 0, Color::Blue() ); + m_pointEntities.push_back( e ); + + int nameIndex = 1; + for ( unsigned int i = 0; i < allCtrlPts.size(); i++ ) { + for ( unsigned int j = 1; j < allCtrlPts[i].size() - 1; j++ ) { + e = PointFactory::createPointComponent( this, allCtrlPts[i][j], { i }, nameIndex ); + m_pointEntities.push_back( e ); + nameIndex++; + } + if ( i == allCtrlPts.size() - 1 ) + e = PointFactory::createPointComponent( + this, allCtrlPts[i][3], { i }, nameIndex, Color::Blue() ); + else + e = PointFactory::createPointComponent( + this, allCtrlPts[i][3], { i, i + 1 }, nameIndex, Color::Blue() ); + m_pointEntities.push_back( e ); + nameIndex++; + } + m_viewer->doneCurrent(); + m_viewer->getRenderer()->buildAllRenderTechniques(); + m_viewer->needUpdate(); +} + +CurveEditor::~CurveEditor() {} + +unsigned int CurveEditor::getPointIndex( unsigned int curveIdSize, + unsigned int currentPtIndex, + const Vector3& firstCtrlPt, + const Vector3& currentPt ) { + unsigned int pointIndex; + if ( curveIdSize > 1 || currentPtIndex == m_pointEntities.size() - 1 ) { + pointIndex = ( firstCtrlPt == currentPt ) ? 0 : 3; + } + else { + pointIndex = ( currentPtIndex % 3 ); + } + return pointIndex; +} + +void CurveEditor::updateCurve( unsigned int curveId, + unsigned int curveIdSize, + PointComponent* pointComponent, + const Vector3& oldPoint, + unsigned int currentPoint ) { + auto component = m_curveEntities[curveId]; + auto ctrlPts = component->m_ctrlPts; + int pointIndex = getPointIndex( curveIdSize, currentPoint, ctrlPts[0], oldPoint ); + + auto ro = m_roMgr->getRenderObject( m_pointEntities[currentPoint]->getRenderObjects()[0] ); + auto transform = ro->getTransform(); + ctrlPts[pointIndex] = transform * pointComponent->m_defaultPoint; + pointComponent->m_point = ctrlPts[pointIndex]; + + std::string name = m_curveEntities[curveId]->getName(); + removeComponent( name ); + auto e = CurveFactory::createCurveComponent( this, ctrlPts, name ); + m_curveEntities[curveId] = e; +} + +void CurveEditor::refreshPoint( unsigned int pointIndex, const Vector3& newPoint ) { + auto pointName = m_pointEntities[pointIndex]->getName(); + auto pointComponent = m_pointEntities[pointIndex]; + auto pointColor = pointComponent->m_color; + auto pointCurve = pointComponent->m_curveId; + auto pointState = pointComponent->m_state; + Vector3 point; + if ( newPoint == Vector3::Zero() ) + point = pointComponent->m_point; + else + point = newPoint; + removeComponent( m_pointEntities[pointIndex]->getName() ); + auto pointEntity = + PointFactory::createPointComponent( this, point, pointCurve, pointName, pointColor ); + pointEntity->m_state = pointState; + m_pointEntities[pointIndex] = pointEntity; +} + +void CurveEditor::updateCurves( bool onRelease ) { + + if ( m_currentPoint < 0 ) return; + + auto pointComponent = m_pointEntities[m_currentPoint]; + auto oldPoint = pointComponent->m_point; + unsigned int curveIdSize = pointComponent->m_curveId.size(); + + m_viewer->makeCurrent(); + + for ( unsigned int i = 0; i < curveIdSize; i++ ) { + auto curveId = pointComponent->m_curveId[i]; + updateCurve( curveId, curveIdSize, pointComponent, oldPoint, m_currentPoint ); + } + + for ( unsigned int i = 0; i < m_tangentPoints.size(); i++ ) { + auto tangentPtComponent = m_pointEntities[m_tangentPoints[i]]; + Vector3 tangentPt = tangentPtComponent->m_point; + auto symCurveId = tangentPtComponent->m_curveId[0]; + updateCurve( symCurveId, 1, tangentPtComponent, tangentPt, m_tangentPoints[i] ); + } + + // Necessary for transform re-initialization + if ( onRelease ) { + refreshPoint( m_currentPoint ); + for ( unsigned int i = 0; i < m_tangentPoints.size(); i++ ) { + refreshPoint( m_tangentPoints[i] ); + } + m_tangentPoints.clear(); + } + + m_viewer->doneCurrent(); + m_viewer->getRenderer()->buildAllRenderTechniques(); + m_viewer->needUpdate(); +} + +Transform CurveEditor::computePointTransform( PointComponent* pointCmp, + PointComponent* midPointCmp, + const Vector3& worldPos ) { + auto transform = Transform::Identity(); + auto midPoint = midPointCmp->m_point; + Vector3 diff = ( midPoint - worldPos ); + if ( midPointCmp->m_state == PointComponent::SYMETRIC ) { + transform.translate( ( midPoint + diff ) - pointCmp->m_defaultPoint ); + } + else { + diff.normalize(); + auto actualdiff = diff * ( midPoint - pointCmp->m_point ).norm(); + transform.translate( ( midPoint + actualdiff ) - pointCmp->m_defaultPoint ); + } + return transform; +} + +inline float CurveEditor::distanceSquared( const Vector3f& pointA, const Vector3f& pointB ) { + float dx = pointA.x() - pointB.x(); + float dy = pointA.y() - pointB.y(); + float dz = pointA.z() - pointB.z(); + return dx * dx + dy * dy + dz * dz; +} + +void CurveEditor::subdivisionBezier( int vertexIndex, + unsigned int curveIndex, + const Vector3Array& ctrlPts ) { + + auto bezier = Geometry::CubicBezier( Vector2( ctrlPts[0].x(), ctrlPts[0].z() ), + Vector2( ctrlPts[1].x(), ctrlPts[1].z() ), + Vector2( ctrlPts[2].x(), ctrlPts[2].z() ), + Vector2( ctrlPts[3].x(), ctrlPts[3].z() ) ); + float u = float( vertexIndex ) / 100.f; + Vector2 fu = bezier.f( u ); + auto clickedPoint = Vector3( fu.x(), 0, fu.y() ); + + // De Casteljau + Vector3 firstPoint = Math::linearInterpolate( ctrlPts[0], ctrlPts[1], u ); + Vector3 sndPoint = Math::linearInterpolate( ctrlPts[1], ctrlPts[2], u ); + Vector3 thirdPoint = Math::linearInterpolate( ctrlPts[2], ctrlPts[3], u ); + Vector3 fourthPoint = Math::linearInterpolate( firstPoint, sndPoint, u ); + Vector3 fifthPoint = Math::linearInterpolate( sndPoint, thirdPoint, u ); + + auto newCtrlPts = ctrlPts; + newCtrlPts[1] = firstPoint; + newCtrlPts[2] = fourthPoint; + newCtrlPts[3] = clickedPoint; + + auto nameCurve = m_curveEntities[curveIndex]->getName(); + removeComponent( nameCurve ); + m_curveEntities[curveIndex] = CurveFactory::createCurveComponent( this, newCtrlPts, nameCurve ); + + refreshPoint( curveIndex * 3 + 1, firstPoint ); + refreshPoint( curveIndex * 3 + 2, thirdPoint ); + + auto firstInsertionIdx = curveIndex * 3 + 2; + auto ptE = PointFactory::createPointComponent( + this, clickedPoint, { curveIndex, curveIndex + 1 }, m_pointEntities.size(), Color::Blue() ); + m_pointEntities.insert( m_pointEntities.begin() + firstInsertionIdx, ptE ); + + ptE = PointFactory::createPointComponent( + this, fourthPoint, { curveIndex }, m_pointEntities.size() ); + m_pointEntities.insert( m_pointEntities.begin() + firstInsertionIdx, ptE ); + + ptE = PointFactory::createPointComponent( + this, fifthPoint, { curveIndex + 1 }, m_pointEntities.size() ); + m_pointEntities.insert( m_pointEntities.begin() + ( ( curveIndex + 1 ) * 3 + 1 ), ptE ); + + Vector3Array newCtrlPts1; + newCtrlPts1.push_back( clickedPoint ); + newCtrlPts1.push_back( fifthPoint ); + newCtrlPts1.push_back( thirdPoint ); + newCtrlPts1.push_back( ctrlPts[3] ); + + auto curveE = CurveFactory::createCurveComponent( this, newCtrlPts1, m_curveEntities.size() ); + m_curveEntities.insert( m_curveEntities.begin() + curveIndex + 1, curveE ); + + for ( unsigned int i = ( ( curveIndex + 1 ) * 3 + 2 ); i < m_pointEntities.size(); i++ ) { + auto ptCmp = m_pointEntities[i]; + for ( unsigned int j = 0; j < ptCmp->m_curveId.size(); j++ ) { + ptCmp->m_curveId[j] += 1; + } + } +} + +void CurveEditor::addPointAtEnd( const Vector3& worldPos ) { + auto lastIndex = m_pointEntities.size() - 1; + auto beforeLast = m_pointEntities[lastIndex - 1]; + auto last = m_pointEntities[lastIndex]; + Vector3 diff = ( last->m_point - beforeLast->m_point ); + diff.normalize(); + + Vector3 secondPt = ( last->m_point + diff ); + Vector3 thirdDiff = ( last->m_point - worldPos ); + thirdDiff.normalize(); + Vector3 thirdPt = ( worldPos - diff ); + + Vector3Array ctrlPts { last->m_point, secondPt, thirdPt, worldPos }; + + auto e = CurveFactory::createCurveComponent( this, ctrlPts, m_curveEntities.size() ); + m_curveEntities.push_back( e ); + + unsigned int pointIndex = lastIndex + 1; + last->m_curveId.push_back( ( pointIndex / 3 ) ); + + auto ptC = + PointFactory::createPointComponent( this, ctrlPts[1], { ( pointIndex / 3 ) }, pointIndex ); + m_pointEntities.push_back( ptC ); + + ptC = PointFactory::createPointComponent( + this, ctrlPts[2], { ( pointIndex / 3 ) }, pointIndex + 1 ); + m_pointEntities.push_back( ptC ); + + ptC = PointFactory::createPointComponent( + this, ctrlPts[3], { ( pointIndex / 3 ) }, pointIndex + 2, Color::Blue() ); + m_pointEntities.push_back( ptC ); +} + +void CurveEditor::addPointInCurve( const Vector3& worldPos, int mouseX, int mouseY ) { + auto camera = m_viewer->getCameraManipulator()->getCamera(); + auto radius = int( m_viewer->getRenderer()->getBrushRadius() ); + bool found = false; + Rendering::Renderer::PickingResult pres; + + for ( int i = mouseX - radius; i < mouseX + radius && !found; i++ ) { + for ( int j = mouseY - radius; j < mouseY + radius; j++ ) { + + Rendering::Renderer::PickingQuery query { Vector2( i, m_viewer->height() - j ), + Rendering::Renderer::SELECTION, + Rendering::Renderer::RO }; + Data::ViewingParameters renderData { + camera->getViewMatrix(), camera->getProjMatrix(), 0 }; + pres = m_viewer->getRenderer()->doPickingNow( query, renderData ); + + if ( pres.getRoIdx().isValid() && m_roMgr->exists( pres.getRoIdx() ) ) { + auto ro = m_roMgr->getRenderObject( pres.getRoIdx() ); + if ( ro->getMesh()->getNumVertices() == 2 ) continue; + pres.removeDuplicatedIndices(); + auto e = ro->getComponent(); + + int curveIndex = -1; + for ( int z = 0; z < m_curveEntities.size(); z++ ) { + if ( m_curveEntities[z] == e ) { + curveIndex = z; + break; + } + } + if ( curveIndex < 0 ) continue; + + auto meshPtr = ro->getMesh().get(); + auto mesh = dynamic_cast( meshPtr ); + auto curveCmp = static_cast( ro->getComponent() ); + auto ctrlPts = curveCmp->m_ctrlPts; + + if ( mesh != nullptr ) { + float bestV = std::numeric_limits::max(); + int vIndex = -1; + for ( unsigned int w = 0; w < mesh->getNumVertices(); w++ ) { + auto dist = + distanceSquared( worldPos, mesh->getCoreGeometry().vertices()[w] ); + if ( dist < bestV ) { + bestV = dist; + vIndex = w; + } + } + + subdivisionBezier( vIndex, curveIndex, ctrlPts ); + found = true; + break; + } + } + } + } +} + +bool CurveEditor::processHover( std::shared_ptr ro ) { + auto e = ro->getComponent(); + + // if not a control point -> do nothing + for ( int i = 0; i < m_pointEntities.size(); i++ ) { + if ( e == m_pointEntities[i] ) { + m_currentPoint = i; + break; + } + } + if ( m_currentPoint < 0 ) return false; + + if ( m_currentPoint % 3 == 0 ) + m_savedPoint = m_currentPoint; + else if ( m_savedPoint >= 0 && m_savedPoint - 1 != m_currentPoint && + m_savedPoint + 1 != m_currentPoint ) + m_savedPoint = -1; + + ro->getMaterial()->getParameters().addParameter( "material.color", Color::Red() ); + m_selectedRo = ro; + return true; +} + +void CurveEditor::processUnhovering() { + auto pointCmp = static_cast( m_selectedRo->getComponent() ); + m_selectedRo->getMaterial()->getParameters().addParameter( "material.color", + pointCmp->m_color ); + m_selectedRo = nullptr; + m_currentPoint = -1; +} + +void CurveEditor::processPicking( const Vector3& worldPos ) { + if ( m_currentPoint < 0 ) return; + auto pointComponent = static_cast( m_selectedRo->getComponent() ); + auto point = pointComponent->m_point; + + auto transformTranslate = Transform::Identity(); + transformTranslate.translate( worldPos - point ); + + auto transform = m_selectedRo->getLocalTransform() * transformTranslate; + m_selectedRo->setLocalTransform( transform ); + + if ( !( m_currentPoint == 0 || m_currentPoint == 1 || + m_currentPoint == m_pointEntities.size() - 2 || + m_currentPoint == m_pointEntities.size() - 1 ) ) { + + if ( m_currentPoint % 3 == 0 ) { + auto leftRo = m_roMgr->getRenderObject( + m_pointEntities[m_currentPoint - 1]->getRenderObjects()[0] ); + auto leftTransform = leftRo->getTransform() * transformTranslate; + leftRo->setLocalTransform( leftTransform ); + + auto rightRo = m_roMgr->getRenderObject( + m_pointEntities[m_currentPoint + 1]->getRenderObjects()[0] ); + auto rightTransform = rightRo->getTransform() * transformTranslate; + rightRo->setLocalTransform( rightTransform ); + + if ( m_tangentPoints.empty() ) { + m_tangentPoints.push_back( m_currentPoint - 1 ); + m_tangentPoints.push_back( m_currentPoint + 1 ); + } + } + else if ( m_currentPoint % 3 == 2 ) { + auto pointMid = m_pointEntities[m_currentPoint + 1]; + if ( !( pointMid->m_state == PointComponent::DEFAULT ) ) { + pointComponent = m_pointEntities[m_currentPoint + 2]; + auto symTransform = computePointTransform( pointComponent, pointMid, worldPos ); + auto ro = m_roMgr->getRenderObject( pointComponent->getRenderObjects()[0] ); + ro->setLocalTransform( symTransform ); + + if ( m_tangentPoints.empty() ) { m_tangentPoints.push_back( m_currentPoint + 2 ); } + } + } + else if ( m_currentPoint % 3 == 1 ) { + auto pointMid = m_pointEntities[m_currentPoint - 1]; + if ( !( pointMid->m_state == PointComponent::DEFAULT ) ) { + pointComponent = m_pointEntities[m_currentPoint - 2]; + auto symTransform = computePointTransform( pointComponent, pointMid, worldPos ); + auto ro = m_roMgr->getRenderObject( pointComponent->getRenderObjects()[0] ); + ro->setLocalTransform( symTransform ); + + if ( m_tangentPoints.empty() ) { m_tangentPoints.push_back( m_currentPoint - 2 ); } + } + } + } + updateCurves(); +} + +void CurveEditor::setSmooth( bool smooth ) { + if ( m_savedPoint >= 0 ) { + if ( smooth ) { + m_pointEntities[m_savedPoint]->m_state = PointComponent::SMOOTH; + smoothify( m_savedPoint ); + } + else + m_pointEntities[m_savedPoint]->m_state = PointComponent::DEFAULT; + } +} + +void CurveEditor::setSymetry( bool symetry ) { + if ( m_savedPoint >= 0 ) { + if ( symetry ) { + m_pointEntities[m_savedPoint]->m_state = PointComponent::SYMETRIC; + symmetrize( m_savedPoint ); + } + else + m_pointEntities[m_savedPoint]->m_state = PointComponent::SMOOTH; + } +} + +void CurveEditor::smoothify( int pointId ) { + if ( ( pointId == 0 || pointId == 1 || pointId == m_pointEntities.size() - 2 || + pointId == m_pointEntities.size() - 1 ) ) + return; + + m_viewer->makeCurrent(); + Vector3 backPoint = m_pointEntities[pointId - 1]->m_point; + Vector3 back = backPoint - m_pointEntities[pointId]->m_point; + Vector3 frontPoint = m_pointEntities[pointId + 1]->m_point; + Vector3 front = frontPoint - m_pointEntities[pointId]->m_point; + + Vector3 newPointDir = back - front; + newPointDir.normalize(); + newPointDir = newPointDir * back.norm(); + Vector3 newBackPoint = m_pointEntities[pointId]->m_point + newPointDir; + + auto transform = Transform::Identity(); + transform.translate( newBackPoint - m_pointEntities[pointId - 1]->m_point ); + auto roIdx = m_pointEntities[pointId - 1]->getRenderObjects()[0]; + auto ro = m_roMgr->getRenderObject( roIdx ); + ro->setLocalTransform( transform ); + m_pointEntities[pointId - 1]->m_point = newBackPoint; + + newPointDir.normalize(); + newPointDir = newPointDir * front.norm(); + Vector3 newFrontPoint = m_pointEntities[pointId]->m_point - newPointDir; + + transform = Transform::Identity(); + transform.translate( newFrontPoint - m_pointEntities[pointId + 1]->m_point ); + roIdx = m_pointEntities[pointId + 1]->getRenderObjects()[0]; + ro = m_roMgr->getRenderObject( roIdx ); + ro->setLocalTransform( transform ); + m_pointEntities[pointId + 1]->m_point = newFrontPoint; + + m_currentPoint = pointId - 1; + updateCurves( true ); + m_currentPoint = pointId + 1; + updateCurves( true ); + m_currentPoint = -1; + + m_viewer->doneCurrent(); + m_viewer->getRenderer()->buildAllRenderTechniques(); + m_viewer->needUpdate(); +} + +void CurveEditor::symmetrize( int pointId ) { + if ( ( pointId == 0 || pointId == 1 || pointId == m_pointEntities.size() - 2 || + pointId == m_pointEntities.size() - 1 ) ) + return; + + m_viewer->makeCurrent(); + + Vector3 backPoint = m_pointEntities[pointId - 1]->m_point; + Vector3 back = backPoint - m_pointEntities[pointId]->m_point; + Vector3 frontPoint = m_pointEntities[pointId + 1]->m_point; + Vector3 front = frontPoint - m_pointEntities[pointId]->m_point; + + auto transform = Transform::Identity(); + Index roIdx = -1; + if ( std::min( back.norm(), front.norm() ) == back.norm() ) { + front.normalize(); + front = front * back.norm(); + Vector3 newPoint = m_pointEntities[pointId]->m_point + front; + transform.translate( newPoint - m_pointEntities[pointId + 1]->m_point ); + roIdx = m_pointEntities[pointId + 1]->getRenderObjects()[0]; + m_pointEntities[pointId + 1]->m_point = newPoint; + m_currentPoint = pointId + 1; + } + else { + back.normalize(); + back = back * front.norm(); + Vector3 newPoint = m_pointEntities[pointId]->m_point + back; + transform.translate( newPoint - m_pointEntities[pointId - 1]->m_point ); + roIdx = m_pointEntities[pointId - 1]->getRenderObjects()[0]; + m_pointEntities[pointId - 1]->m_point = newPoint; + m_currentPoint = pointId - 1; + } + auto ro = m_roMgr->getRenderObject( roIdx ); + ro->setLocalTransform( transform ); + updateCurves( true ); + m_currentPoint = -1; + + m_viewer->doneCurrent(); + m_viewer->getRenderer()->buildAllRenderTechniques(); + m_viewer->needUpdate(); +} diff --git a/examples/CurveEditor/CurveEditor.hpp b/examples/CurveEditor/CurveEditor.hpp new file mode 100644 index 00000000000..40142e09fb1 --- /dev/null +++ b/examples/CurveEditor/CurveEditor.hpp @@ -0,0 +1,119 @@ +#include "CurveFactory.hpp" +#include "Gui/MyViewer.hpp" +#include "PointFactory.hpp" +#include +#include +#include +#include + +class CurveEditor : public Ra::Engine::Scene::Entity +{ + + public: + CurveEditor( const Ra::Core::VectorArray& polyline, + MyViewer* viewer ); + ~CurveEditor(); + + /** + * @brief add a point at the end of the polyline + * @param the world position where to add the point + */ + void addPointAtEnd( const Ra::Core::Vector3& worldPos ); + + /** + * @brief add a point in the existing polyline + * @param the world position where to add the point + * @param the corresponding window x position + * @param the corresponding window y position + */ + void addPointInCurve( const Ra::Core::Vector3& worldPos, int mouseX, int mouseY ); + + /** + * @brief update all curves, if onRelease then it will clean the entities transform + * @param the world position where to add the point + */ + void updateCurves( bool onRelease = false ); + + /** + * @brief change the color of the corresponding ro and save it for future manipulation + * @param the renderObject that is currently being hovered + */ + bool processHover( std::shared_ptr ro ); + + /** + * @brief change saved ro color to their default and unsave the ro + */ + void processUnhovering(); + + /** + * @brief process the saved ro and move it to the given world position + * @param the world position of where to move the saved ro + */ + void processPicking( const Ra::Core::Vector3& worldPos ); + + std::shared_ptr getSelected() { return m_selectedRo; } + + void resetSelected() { + m_selectedRo = nullptr; + m_currentPoint = -1; + } + + PointComponent* getSavedPoint() { + if ( m_savedPoint >= 0 ) + return m_pointEntities[m_savedPoint]; + else + return nullptr; + } + + /** + * @brief smooth to keep a G1 continuity + * @param smooth state + */ + void setSmooth( bool smooth ); + + /** + * @brief symetry to keep a C1 continuity + * @param symetry state + */ + void setSymetry( bool symetry ); + + private: + inline float distanceSquared( const Ra::Core::Vector3f& pointA, + const Ra::Core::Vector3f& pointB ); + + unsigned int getPointIndex( unsigned int curveIdSize, + unsigned int currentPtIndex, + const Ra::Core::Vector3& firstCtrlPt, + const Ra::Core::Vector3& currentPt ); + + void updateCurve( unsigned int curveId, + unsigned int curveIdSize, + PointComponent* pointComponent, + const Ra::Core::Vector3& currentPt, + unsigned int currentPoint ); + + Ra::Core::Transform computePointTransform( PointComponent* pointCmp, + PointComponent* midPoint, + const Ra::Core::Vector3& worldPos ); + void subdivisionBezier( int vertexIndex, + unsigned int curveIndex, + const Ra::Core::Vector3Array& ctrlPts ); + void refreshPoint( unsigned int pointIndex, + const Ra::Core::Vector3& newPoint = Ra::Core::Vector3::Zero() ); + + void smoothify( int pointId ); + + void symmetrize( int pointId ); + + private: + Ra::Engine::Scene::EntityManager* m_entityMgr; + int m_currentPoint { -1 }; + std::shared_ptr m_selectedRo { nullptr }; + std::vector m_pointEntities; + std::vector m_curveEntities; + std::vector m_tangentPoints; + Ra::Engine::Rendering::RenderObjectManager* m_roMgr; + int m_savedPoint { -1 }; + + MyViewer* m_viewer { nullptr }; +}; diff --git a/examples/CurveEditor/CurveFactory.hpp b/examples/CurveEditor/CurveFactory.hpp new file mode 100644 index 00000000000..8f2ec3ccff7 --- /dev/null +++ b/examples/CurveEditor/CurveFactory.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "CurveComponent.hpp" +#include +#include + +class CurveFactory +{ + public: + static CurveComponent* + createCurveComponent( Ra::Engine::Scene::Entity* e, Ra::Core::Vector3Array ctrlPts, int id ) { + std::string name = "Curve_" + std::to_string( id ); + auto c = new CurveComponent( e, ctrlPts, name ); + c->initialize(); + return c; + } + static CurveComponent* createCurveComponent( Ra::Engine::Scene::Entity* e, + Ra::Core::Vector3Array ctrlPts, + const std::string& name ) { + auto c = new CurveComponent( e, ctrlPts, name ); + c->initialize(); + return c; + } + static Ra::Engine::Scene::Entity* createCurveEntity( Ra::Core::Vector3Array ctrlPts, + const std::string& name ) { + auto engine = Ra::Engine::RadiumEngine::getInstance(); + Ra::Engine::Scene::Entity* e = engine->getEntityManager()->createEntity( name ); + createCurveComponent( e, ctrlPts, name ); + return e; + } + + private: +}; diff --git a/examples/CurveEditor/Gui/MainWindow.cpp b/examples/CurveEditor/Gui/MainWindow.cpp new file mode 100644 index 00000000000..9af2417518d --- /dev/null +++ b/examples/CurveEditor/Gui/MainWindow.cpp @@ -0,0 +1,275 @@ +#include "MainWindow.hpp" +#include "BezierUtils/CubicBezierApproximation.hpp" +#include "PointFactory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Ra::Gui; +using namespace Ra::Engine; +using namespace Ra::Engine::Rendering; +using namespace Ra::Core::Utils; + +MainWindow::MainWindow( QWidget* parent ) : MainWindowInterface( parent ) { + if ( objectName().isEmpty() ) setObjectName( QString::fromUtf8( "RadiumSimpleWindow" ) ); + + // Initialize the minimum tools for a Radium-Guibased Application + m_viewer = new MyViewer(); + m_viewer->setObjectName( QStringLiteral( "m_viewer" ) ); + m_viewer->setBackgroundColor( Color::White() ); + m_engine = Ra::Engine::RadiumEngine::getInstance(); + + // Initialize the scene interactive representation + m_sceneModel = new Ra::Gui::ItemModel( Ra::Engine::RadiumEngine::getInstance(), this ); + m_selectionManager = new Ra::Gui::SelectionManager( m_sceneModel, this ); + + // initialize Gui for the application + auto viewerWidget = QWidget::createWindowContainer( m_viewer ); + viewerWidget->setAutoFillBackground( false ); + setCentralWidget( viewerWidget ); + setWindowTitle( QString( "Radium player" ) ); + setMinimumSize( 800, 600 ); + + // Dock widget + QDockWidget* dockWidget = new QDockWidget( "Dock", this ); + QWidget* myWidget = new QWidget(); + QVBoxLayout* layout = new QVBoxLayout(); + m_editCurveButton = new QPushButton( "Edit polyline" ); + m_button = new QPushButton( "smooth" ); + m_button->setCheckable( true ); + m_hidePolylineButton = new QPushButton( "Hide initial polyline" ); + m_hidePolylineButton->setCheckable( true ); + m_symetryButton = new QPushButton( "Symetry" ); + m_symetryButton->setCheckable( true ); + m_symetryButton->setEnabled( false ); + m_button->setEnabled( false ); + layout->addWidget( m_hidePolylineButton ); + layout->addWidget( m_editCurveButton ); + layout->addWidget( m_button ); + layout->addWidget( m_symetryButton ); + myWidget->setLayout( layout ); + dockWidget->setWidget( myWidget ); + dockWidget->setAllowedAreas( Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea ); + addDockWidget( Qt::LeftDockWidgetArea, dockWidget ); + m_dockWidget = dockWidget; + createConnections(); +} + +MainWindow::~MainWindow() = default; + +MyViewer* MainWindow::getViewer() { + return m_viewer; +} + +Ra::Gui::SelectionManager* MainWindow::getSelectionManager() { + return m_selectionManager; +} + +Ra::Gui::Timeline* MainWindow::getTimeline() { + return nullptr; +} + +void MainWindow::updateUi( Ra::Plugins::RadiumPluginInterface* plugin ) { + QString name; + if ( plugin->doAddWidget( name ) ) { m_dockWidget->setWidget( plugin->getWidget() ); } +} + +void MainWindow::onFrameComplete() {} + +void MainWindow::addRenderer( const std::string&, + std::shared_ptr e ) { + e->enableDebugDraw( false ); + m_viewer->addRenderer( e ); +} +void MainWindow::prepareDisplay() { + m_selectionManager->clear(); + if ( m_viewer->prepareDisplay() ) { emit frameUpdate(); } + m_viewer->getRenderer()->toggleDrawDebug(); +} + +void MainWindow::cleanup() { + m_viewer = nullptr; +} + +void MainWindow::createConnections() { + connect( m_viewer, &MyViewer::toggleBrushPicking, this, &MainWindow::toggleCirclePicking ); + connect( m_viewer, &MyViewer::onMouseMove, this, &MainWindow::handleMouseMoveEvent ); + connect( m_viewer, &MyViewer::onMouseRelease, this, &MainWindow::handleMouseReleaseEvent ); + connect( m_viewer, &MyViewer::onMousePress, this, &MainWindow::handleMousePressEvent ); + connect( + m_editCurveButton, &QPushButton::pressed, this, &MainWindow::onEditPolylineButtonPressed ); + connect( m_hidePolylineButton, + &QPushButton::clicked, + this, + &MainWindow::onHidePolylineButtonClicked ); + connect( m_button, &QPushButton::clicked, this, &MainWindow::onSmoothButtonClicked ); + connect( m_symetryButton, &QPushButton::clicked, this, &MainWindow::onSymetryButtonClicked ); + connect( + m_viewer, &MyViewer::onMouseDoubleClick, this, &MainWindow::handleMouseDoubleClickEvent ); +} + +void MainWindow::onSmoothButtonClicked() { + m_curveEditor->setSmooth( m_button->isChecked() ); + m_symetryButton->setEnabled( true ); +} + +void MainWindow::onSymetryButtonClicked() { + m_curveEditor->setSymetry( m_symetryButton->isChecked() ); +} + +void MainWindow::onHidePolylineButtonClicked() { + auto roMgr = Ra::Engine::RadiumEngine::getInstance()->getRenderObjectManager(); + auto ro = + roMgr->getRenderObject( m_initialPolyline->getComponents()[0]->getRenderObjects()[0] ); + if ( m_hidePolylineButton->isChecked() ) { ro->setVisible( false ); } + else + ro->setVisible( true ); + m_viewer->needUpdate(); +} + +void MainWindow::onEditPolylineButtonPressed() { + if ( m_polyline.empty() || m_edited ) return; + m_edited = true; + m_curveEditor = new CurveEditor( m_polyline, m_viewer ); +} + +void MainWindow::processSavedPoint() { + auto savedPoint = m_curveEditor->getSavedPoint(); + if ( savedPoint ) { + if ( savedPoint->m_state == PointComponent::DEFAULT ) { + m_button->setChecked( false ); + m_button->setEnabled( true ); + m_symetryButton->setEnabled( false ); + m_symetryButton->setChecked( false ); + } + else if ( savedPoint->m_state == PointComponent::SMOOTH ) { + m_button->setEnabled( true ); + m_button->setChecked( true ); + m_symetryButton->setEnabled( true ); + m_symetryButton->setChecked( false ); + } + else { + m_button->setEnabled( true ); + m_button->setChecked( true ); + m_symetryButton->setEnabled( true ); + m_symetryButton->setChecked( true ); + } + } + else { + m_button->setEnabled( false ); + m_button->setChecked( false ); + m_symetryButton->setEnabled( false ); + m_symetryButton->setChecked( false ); + } +} + +void MainWindow::handleMousePressEvent( QMouseEvent* event ) { + if ( m_edited ) { + if ( event->button() == Qt::RightButton ) m_clicked = true; + processSavedPoint(); + } +} + +void MainWindow::displayHelpDialog() { + m_viewer->displayHelpDialog(); +} + +void MainWindow::toggleCirclePicking( bool on ) { + m_isTracking = on; + centralWidget()->setMouseTracking( on ); +} + +Ra::Core::Vector3 MainWindow::getWorldPos( int x, int y ) { + const auto r = m_viewer->getCameraManipulator()->getCamera()->getRayFromScreen( { x, y } ); + std::vector t; + // {0,0,0} a point and {0,1,0} the normal + if ( Ra::Core::Geometry::RayCastPlane( r, { 0, 0, 0 }, { 0, 1, 0 }, t ) ) { + // vsPlane return at most one intersection t + return r.pointAt( t[0] ); + } + return Ra::Core::Vector3(); +} + +void MainWindow::handleMouseReleaseEvent( QMouseEvent* event ) { + m_clicked = false; + if ( m_curveEditor == nullptr ) return; + if ( m_curveEditor->getSelected() && event->button() == Qt::RightButton ) { + m_curveEditor->updateCurves( true ); + m_curveEditor->resetSelected(); + m_hovering = false; + } +} + +void MainWindow::handleMouseDoubleClickEvent( QMouseEvent* event ) { + if ( m_curveEditor == nullptr ) return; + auto worldPos = getWorldPos( event->x(), event->y() ); + + auto modifiers = event->modifiers(); + + m_viewer->makeCurrent(); + if ( modifiers == Qt::ShiftModifier ) { + m_curveEditor->addPointAtEnd( worldPos ); + m_viewer->doneCurrent(); + m_viewer->getRenderer()->buildAllRenderTechniques(); + m_viewer->needUpdate(); + return; + } + + m_curveEditor->addPointInCurve( worldPos, event->pos().x(), event->pos().y() ); + + m_viewer->doneCurrent(); + m_viewer->getRenderer()->buildAllRenderTechniques(); + m_viewer->needUpdate(); +} + +void MainWindow::handleMouseMoveEvent( QMouseEvent* event ) { + if ( m_curveEditor == nullptr ) return; + int mouseX = event->pos().x(); + int mouseY = event->pos().y(); + handleHover( mouseX, mouseY ); + handlePicking( mouseX, mouseY ); +} + +void MainWindow::handleHover( int mouseX, int mouseY ) { + m_viewer->makeCurrent(); + auto camera = m_viewer->getCameraManipulator()->getCamera(); + Ra::Engine::Rendering::Renderer::PickingQuery query { + Ra::Core::Vector2( mouseX, height() - mouseY ), + Ra::Engine::Rendering::Renderer::MANIPULATION, + Ra::Engine::Rendering::Renderer::RO }; + Ra::Engine::Data::ViewingParameters renderData { + camera->getViewMatrix(), camera->getProjMatrix(), 0 }; + + auto pres = m_viewer->getRenderer()->doPickingNow( query, renderData ); + m_viewer->doneCurrent(); + + if ( pres.getRoIdx() >= 0 && ( !m_hovering ) ) { + auto ro = m_engine->getRenderObjectManager()->getRenderObject( pres.getRoIdx() ); + m_hovering = m_curveEditor->processHover( ro ); + } + else if ( m_hovering && !( m_clicked ) ) { + m_curveEditor->processUnhovering(); + m_hovering = false; + } +} + +void MainWindow::handlePicking( int mouseX, int mouseY ) { + if ( m_curveEditor->getSelected() != nullptr && m_clicked ) { + auto worldPos = getWorldPos( mouseX, mouseY ); + m_curveEditor->processPicking( worldPos ); + } +} diff --git a/examples/CurveEditor/Gui/MainWindow.hpp b/examples/CurveEditor/Gui/MainWindow.hpp new file mode 100644 index 00000000000..480b1a1f80c --- /dev/null +++ b/examples/CurveEditor/Gui/MainWindow.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include +#include + +#include "CurveEditor.hpp" +#include "MyViewer.hpp" +#include "PointComponent.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/// This class manages most of the GUI of the application : +/// top menu, side toolbar and side dock. +class MainWindow : public Ra::Gui::MainWindowInterface +{ + Q_OBJECT + public: + /// Constructor and destructor. + explicit MainWindow( QWidget* parent = nullptr ); + virtual ~MainWindow() override; + MyViewer* getViewer() override; + Ra::Gui::SelectionManager* getSelectionManager() override; + Ra::Gui::Timeline* getTimeline() override; + void updateUi( Ra::Plugins::RadiumPluginInterface* plugin ) override; + void onFrameComplete() override; + void addRenderer( const std::string& name, + std::shared_ptr e ) override; + void toggleCirclePicking( bool on ); + Ra::Core::Vector3 getWorldPos( int x, int y ); + void handleHover( int mouseX, int mouseY ); + void handlePicking( int mouseX, int mouseY ); + void setPolyline( Ra::Core::VectorArray polyline ) { + m_polyline = polyline; + } + void setInitialPolyline( Ra::Engine::Scene::Entity* e ) { m_initialPolyline = e; } + + public slots: + void prepareDisplay() override; + void cleanup() override; + // Display help dialog about Viewer key-bindings + void displayHelpDialog() override; + void handleMouseMoveEvent( QMouseEvent* event ); + void handleMouseReleaseEvent( QMouseEvent* event ); + void handleMousePressEvent( QMouseEvent* event ); + void handleMouseDoubleClickEvent( QMouseEvent* event ); + void onEditPolylineButtonPressed(); + void onHidePolylineButtonClicked(); + void onSmoothButtonClicked(); + void onSymetryButtonClicked(); + signals: + void frameUpdate(); + + private: + void createConnections(); + + void processSavedPoint(); + + MyViewer* m_viewer; + Ra::Gui::SelectionManager* m_selectionManager; + Ra::Gui::ItemModel* m_sceneModel; + Ra::Engine::RadiumEngine* m_engine; + QDockWidget* m_dockWidget; + QPushButton* m_button; + QPushButton* m_editCurveButton; + QPushButton* m_hidePolylineButton; + QPushButton* m_symetryButton; + bool m_clicked = false; + bool m_isTracking { false }; + bool m_hovering { false }; + bool m_edited { false }; + Ra::Core::VectorArray m_polyline; + Ra::Engine::Scene::Entity* m_initialPolyline; + + CurveEditor* m_curveEditor { nullptr }; +}; diff --git a/examples/CurveEditor/Gui/MainWindow.ui b/examples/CurveEditor/Gui/MainWindow.ui new file mode 100644 index 00000000000..d28c894e56a --- /dev/null +++ b/examples/CurveEditor/Gui/MainWindow.ui @@ -0,0 +1,37 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 0 + 0 + 800 + 22 + + + + + About + + + + + + + + + diff --git a/examples/CurveEditor/Gui/MyViewer.cpp b/examples/CurveEditor/Gui/MyViewer.cpp new file mode 100644 index 00000000000..15ffd98244b --- /dev/null +++ b/examples/CurveEditor/Gui/MyViewer.cpp @@ -0,0 +1,9 @@ +#include "MyViewer.hpp" + +MyViewer::MyViewer() : Ra::Gui::Viewer() {} + +void MyViewer::mouseDoubleClickEvent( QMouseEvent* event ) { + emit onMouseDoubleClick( event ); +} + +MyViewer::~MyViewer() {} diff --git a/examples/CurveEditor/Gui/MyViewer.hpp b/examples/CurveEditor/Gui/MyViewer.hpp new file mode 100644 index 00000000000..1591e34f3ac --- /dev/null +++ b/examples/CurveEditor/Gui/MyViewer.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +class MyViewer : public Ra::Gui::Viewer +{ + Q_OBJECT; + + public: + MyViewer(); + virtual ~MyViewer(); + + signals: + void onMouseDoubleClick( QMouseEvent* event ); + + public slots: + void mouseDoubleClickEvent( QMouseEvent* event ) override; + + private: + /* data */ +}; diff --git a/examples/CurveEditor/PointComponent.cpp b/examples/CurveEditor/PointComponent.cpp new file mode 100644 index 00000000000..fc1f6361282 --- /dev/null +++ b/examples/CurveEditor/PointComponent.cpp @@ -0,0 +1,66 @@ +#include "PointComponent.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef IO_USE_ASSIMP +# include +#endif + +#include + +using namespace Ra; +using namespace Ra::Core; +using namespace Ra::Core::Utils; +using namespace Ra::Core::Geometry; +using namespace Ra::Engine; +using namespace Ra::Engine::Rendering; +using namespace Ra::Engine::Data; +using namespace Ra::Engine::Scene; + +/** + * This file contains a minimal radium/qt application which shows the geometrical primitives + * supported by Radium + */ + +PointComponent::PointComponent( Ra::Engine::Scene::Entity* entity, + Ra::Core::Vector3 point, + const std::vector& curveId, + const std::string& name, + Color color ) : + Ra::Engine::Scene::Component( name, entity ), + m_point( point ), + m_defaultPoint( point ), + m_curveId( curveId ), + m_color( color ) {} + +/// This function is called when the component is properly +/// setup, i.e. it has an entity. +void PointComponent::initialize() { + auto plainMaterial = make_shared( "Plain Material" ); + plainMaterial->m_color = m_color; + plainMaterial->m_perVertexColor = false; + + auto circle = RenderObject::createRenderObject( + "contourPt_circle", + this, + RenderObjectType::Geometry, + DrawPrimitives::Disk( m_point, { 0_ra, 1_ra, 0_ra }, 0.1, 64, Color::White() ), + {} ); + circle->setMaterial( plainMaterial ); + addRenderObject( circle ); +} diff --git a/examples/CurveEditor/PointComponent.hpp b/examples/CurveEditor/PointComponent.hpp new file mode 100644 index 00000000000..be7d2e87e60 --- /dev/null +++ b/examples/CurveEditor/PointComponent.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +class PointComponent : public Ra::Engine::Scene::Component +{ + + public: + PointComponent( Ra::Engine::Scene::Entity* entity, + Ra::Core::Vector3 point, + const std::vector& curveId, + const std::string& name, + Ra::Core::Utils::Color color ); + + /// This function is called when the component is properly + /// setup, i.e. it has an entity. + void initialize() override; + + enum State { DEFAULT = 0, SMOOTH, SYMETRIC }; + + State m_state { DEFAULT }; + Ra::Core::Vector3 m_point; + Ra::Core::Vector3 m_defaultPoint; + Ra::Core::Utils::Color m_color; + std::vector m_curveId; +}; diff --git a/examples/CurveEditor/PointFactory.hpp b/examples/CurveEditor/PointFactory.hpp new file mode 100644 index 00000000000..8ee67ec2db7 --- /dev/null +++ b/examples/CurveEditor/PointFactory.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "PointComponent.hpp" +#include +#include +#include + +class PointFactory +{ + public: + static PointComponent* + createPointComponent( Ra::Engine::Scene::Entity* e, + Ra::Core::Vector3 point, + const std::vector& curveId, + int id, + Ra::Core::Utils::Color color = Ra::Core::Utils::Color::Black() ) { + std::string name = "CtrlPt_" + std::to_string( id ); + auto c = new PointComponent( e, point, curveId, name, color ); + c->initialize(); + return c; + } + + static PointComponent* + createPointComponent( Ra::Engine::Scene::Entity* e, + Ra::Core::Vector3 point, + const std::vector& curveId, + const std::string& name, + Ra::Core::Utils::Color color = Ra::Core::Utils::Color::Black() ) { + auto c = new PointComponent( e, point, curveId, name, color ); + c->initialize(); + return c; + } + + static Ra::Engine::Scene::Entity* + createPointEntity( Ra::Core::Vector3 point, + const std::string& name, + const std::vector& curveId, + Ra::Core::Utils::Color color = Ra::Core::Utils::Color::Grey() ) { + auto engine = Ra::Engine::RadiumEngine::getInstance(); + Ra::Engine::Scene::Entity* e = engine->getEntityManager()->createEntity( name ); + createPointComponent( e, point, curveId, name, color ); + return e; + } + + private: +}; diff --git a/examples/CurveEditor/PolylineComponent.cpp b/examples/CurveEditor/PolylineComponent.cpp new file mode 100644 index 00000000000..91e100dfbf0 --- /dev/null +++ b/examples/CurveEditor/PolylineComponent.cpp @@ -0,0 +1,59 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef IO_USE_ASSIMP +# include +#endif + +#include + +using namespace Ra; +using namespace Ra::Core; +using namespace Ra::Core::Utils; +using namespace Ra::Core::Geometry; +using namespace Ra::Engine; +using namespace Ra::Engine::Rendering; +using namespace Ra::Engine::Data; +using namespace Ra::Engine::Scene; + +/** + * This file contains a minimal radium/qt application which shows the geometrical primitives + * supported by Radium + */ + +PolylineComponent::PolylineComponent( Ra::Engine::Scene::Entity* entity, + Vector3Array polylinePoints ) : + Ra::Engine::Scene::Component( "Polyline Component", entity ), m_polylinePts( polylinePoints ) {} + +/// This function is called when the component is properly +/// setup, i.e. it has an entity. +void PolylineComponent::initialize() { + auto plainMaterial = make_shared( "Plain Material" ); + plainMaterial->m_perVertexColor = true; + + // Render mesh + auto renderObject1 = RenderObject::createRenderObject( + "PolyMesh", + this, + RenderObjectType::Geometry, + DrawPrimitives::LineStrip( m_polylinePts, + Vector4Array { m_polylinePts.size(), { 0, 0, 0.7, 1 } } ), + Ra::Engine::Rendering::RenderTechnique {} ); + renderObject1->setMaterial( plainMaterial ); + addRenderObject( renderObject1 ); +} diff --git a/examples/CurveEditor/PolylineComponent.hpp b/examples/CurveEditor/PolylineComponent.hpp new file mode 100644 index 00000000000..8662864da69 --- /dev/null +++ b/examples/CurveEditor/PolylineComponent.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include + +class PolylineComponent : public Ra::Engine::Scene::Component +{ + + public: + PolylineComponent( Ra::Engine::Scene::Entity* entity, Ra::Core::Vector3Array polylinePoints ); + + /// This function is called when the component is properly + /// setup, i.e. it has an entity. + void initialize() override; + + Ra::Core::Vector3Array m_polylinePts; +}; diff --git a/examples/CurveEditor/PolylineFactory.hpp b/examples/CurveEditor/PolylineFactory.hpp new file mode 100644 index 00000000000..b63f872383f --- /dev/null +++ b/examples/CurveEditor/PolylineFactory.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "PolylineComponent.hpp" +#include +#include +#include +#include + +class PolylineFactory +{ + public: + static PolylineComponent* createPolylineComponent( Ra::Engine::Scene::Entity* e, + Ra::Core::Vector3Array polylinePts ) { + auto c = new PolylineComponent( e, polylinePts ); + c->initialize(); + return c; + } + static Ra::Engine::Scene::Entity* + createCurveEntity( Ra::Core::VectorArray polyline, + const std::string& name ) { + auto engine = Ra::Engine::RadiumEngine::getInstance(); + Ra::Engine::Scene::Entity* e = engine->getEntityManager()->createEntity( name ); + Ra::Core::Vector3Array polylinePts; + for ( auto& pt : polyline ) { + polylinePts.push_back( Ra::Core::Vector3( pt.x(), 0, pt.y() ) ); + } + createPolylineComponent( e, polylinePts ); + return e; + } + + private: +}; diff --git a/examples/CurveEditor/main.cpp b/examples/CurveEditor/main.cpp new file mode 100644 index 00000000000..11daa8b68d6 --- /dev/null +++ b/examples/CurveEditor/main.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +#include + +#include + +#include +#include +#include +#include + +#include + +#include "Gui/MainWindow.hpp" +#include "PolylineFactory.hpp" + +class MainWindowFactory : public Ra::Gui::BaseApplication::WindowFactory +{ + public: + using Ra::Gui::BaseApplication::WindowFactory::WindowFactory; + Ra::Gui::MainWindowInterface* createMainWindow() const { return new MainWindow(); } +}; + +int main( int argc, char* argv[] ) { + + // Create app and show viewer window + Ra::Gui::BaseApplication app( argc, argv ); + app.initialize( MainWindowFactory() ); + app.setContinuousUpdate( false ); + + // Create polyline from polyline points + Ra::Core::VectorArray polyline = { + { -7.07583, -7.77924 }, { -6.4575, -7.69091 }, { -5.35333, -7.33757 }, + { -3.27749, -6.54257 }, { -1.68749, -5.39423 }, { -0.671654, -4.20173 }, + { 0.123348, -3.18589 }, { 0.697517, -1.94923 }, { 1.44835, -0.270889 }, + { 1.84585, 1.14245 }, { 1.93419, 3.04162 }, { 1.84585, 4.36662 }, + { 1.36002, 5.82412 }, { 0.874184, 7.10496 }, { 0.167517, 8.2533 }, + { -0.759985, 9.3133 }, { -1.37832, 9.93163 }, { -1.68749, 10.0641 }, + { -2.21749, 10.1083 }, { -2.70333, 9.75497 }, { -3.23333, 9.00413 }, + { -3.49832, 8.16496 }, { -3.71916, 6.70746 }, { -3.71916, 5.38246 }, + { -3.54249, 3.96912 }, { -3.32166, 2.64412 }, { -2.74749, 0.612446 }, + { -1.73166, -1.81673 }, { -0.804153, -3.93673 }, { 0.0350161, -5.7034 }, + { 0.830018, -6.94007 }, { 1.36002, -7.47007 }, { 2.02252, -7.86757 }, + { 3.03835, -8.26508 }, { 3.70086, -8.39758 }, { 4.14253, -8.30924 }, + { 4.80503, -8.00007 } }; + auto window = static_cast( app.m_mainWindow.get() ); + window->setPolyline( polyline ); + + auto initialPolyline = PolylineFactory::createCurveEntity( polyline, "Initial polyline" ); + window->setInitialPolyline( initialPolyline ); + + app.m_mainWindow->prepareDisplay(); + + app.m_mainWindow->getViewer()->setCameraManipulator( new Ra::Gui::RotateAroundCameraManipulator( + *( app.m_mainWindow->getViewer()->getCameraManipulator() ), + app.m_mainWindow->getViewer() ) ); + + return app.exec(); +} diff --git a/src/Core/Geometry/Curve2D.cpp b/src/Core/Geometry/Curve2D.cpp new file mode 100644 index 00000000000..f6fe1c46c68 --- /dev/null +++ b/src/Core/Geometry/Curve2D.cpp @@ -0,0 +1,147 @@ +#include +#include + +namespace Ra { +namespace Core { +namespace Geometry { + +/*--------------------------------------------------*/ + +std::vector CubicBezier::bernsteinCoefsAt( float u, int deriv ) { + if ( deriv == 2 ) { return { 6 * ( 1 - u ), 6 * ( -2 + 3 * u ), 6 * ( 1 - 3 * u ), 6 * u }; } + else if ( deriv == 1 ) + return { -3 * powf( 1 - u, 2 ), + 3 * ( 1 - u ) * ( 1 - 3 * u ), + 3 * u * ( 2 - 3 * u ), + 3 * powf( u, 2 ) }; + else + return { powf( 1 - u, 3 ), + 3 * u * powf( 1 - u, 2 ), + 3 * powf( u, 2 ) * ( 1 - u ), + powf( u, 3 ) }; +} + +std::vector CubicBezier::getArcLengthParameterization( float resolution, + float epsilon ) const { + std::vector params; + float start = 0.0f; + float end = 1.0f; + float curParam = start; + float curDist = 0.0f; + + params.push_back( curParam ); + + Vector p0 = f( curParam ); + curParam += epsilon; + + while ( curParam <= end ) { + Vector p1 = f( curParam ); + curDist += sqrt( pow( p0.x() - p1.x(), 2 ) + pow( p0.y() - p1.y(), 2 ) ); + if ( curDist >= resolution ) { + params.push_back( curParam ); + curDist = 0.0f; + } + p0 = p1; + curParam += epsilon; + } + + // push last sample point to the end to ensure bounds [0, 1] + params[params.size() - 1] = end; + + return params; +} + +const VectorArray CubicBezier::getCtrlPoints() const { + VectorArray ctrlPts; + ctrlPts.reserve( 4 ); + ctrlPts.push_back( m_points[0] ); + ctrlPts.push_back( m_points[1] ); + ctrlPts.push_back( m_points[2] ); + ctrlPts.push_back( m_points[3] ); + return ctrlPts; +} + +/*--------------------------------------------------*/ + +std::pair PiecewiseCubicBezier::getLocalParameter( float u, int nbz ) { + int b { (int)( std::floor( u ) ) }; + float t { u - b }; + + if ( ( b == nbz ) && ( t == 0 ) ) { + b = nbz - 1; + t = 1; + } + return { b, t }; +} + +float PiecewiseCubicBezier::getGlobalParameter( float u, int nbz ) { + return u * nbz; +} + +VectorArray PiecewiseCubicBezier::getCtrlPoints() const { + VectorArray cp; + cp.reserve( 3 * getNbBezier() + 1 ); + for ( unsigned int i = 0; i < m_spline.size(); i++ ) { + cp.push_back( m_spline[i].getCtrlPoints()[0] ); + cp.push_back( m_spline[i].getCtrlPoints()[1] ); + cp.push_back( m_spline[i].getCtrlPoints()[2] ); + } + if ( !m_spline.empty() ) { cp.push_back( m_spline[m_spline.size() - 1].getCtrlPoints()[3] ); } + + return cp; +} + +void PiecewiseCubicBezier::setCtrlPoints( const VectorArray& cpoints ) { + int nbz { (int)( ( cpoints.size() - 1 ) / 3 ) }; + m_spline.clear(); + m_spline.reserve( nbz ); + for ( int b = 0; b < nbz; ++b ) { + m_spline.emplace_back( CubicBezier( + cpoints[3 * b], cpoints[3 * b + 1], cpoints[3 * b + 2], cpoints[3 * b + 3] ) ); + } +} + +std::vector PiecewiseCubicBezier::getUniformParameterization( int nbSamples ) const { + std::vector params; + params.resize( nbSamples ); + + float delta = { 1.0f / (float)( nbSamples - 1 ) }; + float acc = 0.0f; + int nbz = getNbBezier(); + + params[0] = getGlobalParameter( 0.0f, nbz ); + for ( int i = 1; i < nbSamples; ++i ) { + acc += delta; + params[i] = getGlobalParameter( acc, nbz ); + } + + params[params.size() - 1] = getGlobalParameter( 1.0f, nbz ); + + return params; +} + +std::vector PiecewiseCubicBezier::getArcLengthParameterization( float resolution, + float epsilon ) const { + + std::vector params; + int nbz = getNbBezier(); + + if ( nbz <= 0 ) return params; + + params = m_spline[0].getArcLengthParameterization( resolution, epsilon ); + + for ( int i = 1; i < nbz; ++i ) { + std::vector tmpParams = + m_spline[i].getArcLengthParameterization( resolution, epsilon ); + std::transform( tmpParams.begin(), + tmpParams.end(), + tmpParams.begin(), + [&]( auto const& elem ) { return elem + i; } ); + params.insert( params.end(), tmpParams.begin() + 1, tmpParams.end() ); + } + return params; +} + +} // namespace Geometry +} // namespace Core +} // namespace Ra diff --git a/src/Core/Geometry/Curve2D.hpp b/src/Core/Geometry/Curve2D.hpp index d043371419c..d0b9307dc00 100644 --- a/src/Core/Geometry/Curve2D.hpp +++ b/src/Core/Geometry/Curve2D.hpp @@ -67,6 +67,24 @@ class CubicBezier : public Curve2D inline Vector df( Scalar u ) const override; inline Vector fdf( Scalar t, Vector& grad ) const override; + /** + * @brief Computes the cubic Bernstein coefficients for parameter t + * @param t parameter of the coefficients + * @param deriv derivative order + * @return a vector of 4 scalar coefficients + */ + static std::vector bernsteinCoefsAt( float u, int deriv = 0 ); + + /** + * @brief get a list of curviline abscisses + * @param distance in cm accross the curve that separate two params value + * @param step sampling [0, 1] + * @return list of params [0, 1] + */ + std::vector getArcLengthParameterization( float resolution, float epsilon ) const; + + const VectorArray getCtrlPoints() const; + private: Vector m_points[4]; }; @@ -107,6 +125,96 @@ class SplineCurve : public Curve2D Core::VectorArray m_points; }; +class PiecewiseCubicBezier : public Curve2D +{ + public: + PiecewiseCubicBezier() {} + + /** + * @brief Spline of cubic Bézier segments. Construction guarantees C0 continuity. + * ie extremities of successive segments share the same coordinates + * @param vector of control points, should be 3*n+1 points where n is the number of segments + */ + PiecewiseCubicBezier( const Core::VectorArray& cpoints ) { setCtrlPoints( cpoints ); } + + PiecewiseCubicBezier( const PiecewiseCubicBezier& other ) { + setCtrlPoints( other.getCtrlPoints() ); + } + + int getNbBezier() const { return m_spline.size(); } + + const std::vector getSplines() const { return m_spline; } + + /** + * @brief Computes a sample point in the bezier spline + * @param u global parameter of the sample, should be in [0,nbz] + * integer part of u represents the id of the Bézier segment + * while decimal part of u represents the local Bézier parameter + * @param deriv derivative order of the sampling + * @return coordinates of the sample point + */ + inline Vector f( float u ) const override; + + /** + * @brief Computes a list of samples points in the bezier spline + * @param list of u global parameter of the sample, should be in [0,nbz] + * integer part of u represents the id of the Bézier segment + * while decimal part of u represents the local Bézier parameter + * @param deriv derivative order of the sampling + * @return coordinates of the sample point + */ + inline VectorArray f( std::vector params ) const; + + inline Vector df( float u ) const override; + + inline Vector fdf( Scalar t, Vector& grad ) const override; + + inline void addPoint( const Vector p ) override; + + VectorArray getCtrlPoints() const; + + void setCtrlPoints( const VectorArray& cpoints ); + + /** + * @brief Decomposes a spline global parameter into the local Bézier parameters (static) + * @param global parameter + * @param number of segments in the spline + * @return a pair (b,t) where b is the index of the bezier segment, and t the local parameter in + * the segment + */ + static std::pair getLocalParameter( float u, int nbz ); + + /** + * @brief Map a normalized parameter for the spline to a global parameter + * @param normalized parameter [0, 1] + * @param number of segments in the spline + * @return a global parameter t [0, nbz] + */ + static float getGlobalParameter( float u, int nbz ); + + /** + * @brief equivalent to linspace function + * @param number of param + * @return a list of parameters t [0, nbz] + */ + std::vector getUniformParameterization( int nbSamples ) const; + + /** + * @brief get a list of curviline abscisses + * @param distance in cm accross the curve that separate two params value + * @param step sampling [0, 1] + * @return list of params [0, nbz] + */ + std::vector getArcLengthParameterization( float resolution, float epsilon ) const; + + private: + std::vector m_spline; // Vector of Bézier segments in the spline + + std::pair getLocalParameter( float u ) const { + return getLocalParameter( u, getNbBezier() ); + } +}; + } // namespace Geometry } // namespace Core } // namespace Ra diff --git a/src/Core/Geometry/Curve2D.inl b/src/Core/Geometry/Curve2D.inl index 9ce71214639..dc5b856620b 100644 --- a/src/Core/Geometry/Curve2D.inl +++ b/src/Core/Geometry/Curve2D.inl @@ -113,6 +113,59 @@ Curve2D::Vector QuadraSpline::fdf( Scalar u, Vector& grad ) const { return spline.f( u ); } +/*--------------------------------------------------*/ + +Curve2D::Vector PiecewiseCubicBezier::f( float u ) const { + std::pair locpar { getLocalParameter( u ) }; + + if ( locpar.first < 0 || locpar.first > getNbBezier() - 1 ) { + Vector p; + p.fill( 0 ); + return p; + } + + return m_spline[locpar.first].f( locpar.second ); +} + +VectorArray PiecewiseCubicBezier::f( std::vector params ) const { + VectorArray controlPoints; + + for ( int i = 0; i < (int)params.size(); ++i ) { + controlPoints.push_back( f( params[i] ) ); + } + + return controlPoints; +} + +Curve2D::Vector PiecewiseCubicBezier::df( float u ) const { + std::pair locpar { getLocalParameter( u ) }; + + if ( locpar.first < 0 || locpar.first > getNbBezier() - 1 ) { + Vector p; + p.fill( 0 ); + return p; + } + + return m_spline[locpar.first].df( locpar.second ); +} + +Curve2D::Vector PiecewiseCubicBezier::fdf( Scalar t, Vector& grad ) const { + std::pair locpar { getLocalParameter( t ) }; + + if ( locpar.first < 0 || locpar.first > getNbBezier() - 1 ) { + Vector p; + p.fill( 0 ); + return p; + } + + return m_spline[locpar.first].fdf( locpar.second, grad ); +} + +void PiecewiseCubicBezier::addPoint( const Curve2D::Vector p ) { + if ( m_spline[m_spline.size() - 1].getCtrlPoints().size() < 4 ) + m_spline[m_spline.size() - 1].addPoint( p ); +} + } // namespace Geometry } // namespace Core } // namespace Ra diff --git a/src/Core/filelist.cmake b/src/Core/filelist.cmake index 60cff13fa1f..0171891e4b6 100644 --- a/src/Core/filelist.cmake +++ b/src/Core/filelist.cmake @@ -29,6 +29,7 @@ set(core_sources Geometry/Adjacency.cpp Geometry/Area.cpp Geometry/CatmullClarkSubdivider.cpp + Geometry/Curve2D.cpp Geometry/deprecated/TopologicalMesh.cpp Geometry/HeatDiffusion.cpp Geometry/IndexedGeometry.cpp diff --git a/src/Engine/Rendering/Renderer.hpp b/src/Engine/Rendering/Renderer.hpp index 5ffd1360701..ec6da2d6196 100644 --- a/src/Engine/Rendering/Renderer.hpp +++ b/src/Engine/Rendering/Renderer.hpp @@ -277,6 +277,8 @@ class RA_ENGINE_API Renderer inline void setBrushRadius( Scalar brushRadius ); + inline Scalar getBrushRadius(); + /// Tell if the renderer has an usable light. bool hasLight() const; diff --git a/src/Engine/Rendering/Renderer.inl b/src/Engine/Rendering/Renderer.inl index 1086f8c2e9a..81b373c8454 100644 --- a/src/Engine/Rendering/Renderer.inl +++ b/src/Engine/Rendering/Renderer.inl @@ -109,6 +109,10 @@ inline void Renderer::setBrushRadius( Scalar brushRadius ) { m_brushRadius = brushRadius; } +inline Scalar Renderer::getBrushRadius() { + return m_brushRadius; +} + inline void Renderer::setBackgroundColor( const Core::Utils::Color& color ) { m_backgroundColor = color; }