Sync - load curves from JSON file
This commit is contained in:
parent
699e409e64
commit
ffe0fd8337
8 changed files with 297 additions and 69 deletions
23
curves.json
Normal file
23
curves.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"dof:sharp-distance" : [
|
||||
{
|
||||
"type" : "smooth" ,
|
||||
"values" : [ 35 , 100 , 35 ] ,
|
||||
"durations" : [ 300 , 300 ]
|
||||
}
|
||||
] ,
|
||||
"dof:sharp-range" : [
|
||||
{
|
||||
"type" : "smooth" ,
|
||||
"values" : [ 20 , 2 ] ,
|
||||
"durations" : [ 900 ]
|
||||
}
|
||||
] ,
|
||||
"dof:falloff" : [
|
||||
{
|
||||
"type" : "linear" ,
|
||||
"values" : [ 2 , 2 ] ,
|
||||
"durations" : [ 1 ]
|
||||
}
|
||||
]
|
||||
}
|
30
demo.cc
30
demo.cc
|
@ -24,40 +24,12 @@ bool T_Demo::initialise( )
|
|||
combine->output( ) );
|
||||
|
||||
Globals::Sync( ).clearInputs( );
|
||||
// XXX should come from program
|
||||
Globals::Sync( ).addInput( "dof:sharp-distance" , 15 );
|
||||
Globals::Sync( ).addInput( "dof:sharp-range" , 5 );
|
||||
Globals::Sync( ).addInput( "dof:falloff" , 10 );
|
||||
Globals::Sync( ).addInput( "dof:max-blur" , 16 );
|
||||
Globals::Sync( ).addInput( "dof:samples" , 16 );
|
||||
|
||||
// XXX test curve
|
||||
{
|
||||
T_SyncCurve curve;
|
||||
curve.name = "dof:sharp-distance";
|
||||
{
|
||||
T_SyncSegment seg;
|
||||
seg.nPoints = 4;
|
||||
seg.type = T_SyncSegment::SMOOTH;
|
||||
seg.durations.push_back( 300 );
|
||||
seg.durations.push_back( 300 );
|
||||
seg.durations.push_back( 300 );
|
||||
seg.values.push_back( 15 );
|
||||
seg.values.push_back( 30 );
|
||||
seg.values.push_back( 15 );
|
||||
seg.values.push_back( 15 );
|
||||
curve.segments.emplace_back( std::move( seg ) );
|
||||
}
|
||||
{
|
||||
T_SyncSegment seg;
|
||||
seg.nPoints = 2;
|
||||
seg.type = T_SyncSegment::LINEAR;
|
||||
seg.durations.push_back( 600 );
|
||||
seg.values.push_back( 100 );
|
||||
seg.values.push_back( 20 );
|
||||
curve.segments.emplace_back( std::move( seg ) );
|
||||
}
|
||||
Globals::Sync( ).setCurve( std::move( curve ) );
|
||||
}
|
||||
Globals::Sync( ).updateCurveCaches( );
|
||||
|
||||
return true;
|
||||
|
|
|
@ -52,4 +52,4 @@ struct T_WatchedFiles
|
|||
bool triggered;
|
||||
std::vector< int > identifiers;
|
||||
};
|
||||
|
||||
using P_WatchedFiles = std::unique_ptr< T_WatchedFiles >;
|
||||
|
|
1
main.cc
1
main.cc
|
@ -71,6 +71,7 @@ void T_Main::mainLoop( )
|
|||
|
||||
Globals::Watcher( ).check( );
|
||||
Globals::Shaders( ).update( );
|
||||
Globals::Sync( ).checkCurveFile( );
|
||||
|
||||
glFinish( );
|
||||
p.startFrame( );
|
||||
|
|
198
sync.cc
198
sync.cc
|
@ -1,5 +1,18 @@
|
|||
#include "externals.hh"
|
||||
#include "sync.hh"
|
||||
#include "globals.hh"
|
||||
|
||||
namespace {
|
||||
|
||||
const std::map< std::string , T_SyncSegment::E_SegmentType > SegmentTypes_( ([] {
|
||||
std::map< std::string , T_SyncSegment::E_SegmentType > t;
|
||||
t.emplace( "linear" , T_SyncSegment::LINEAR );
|
||||
t.emplace( "ramp" , T_SyncSegment::RAMP );
|
||||
t.emplace( "smooth" , T_SyncSegment::SMOOTH );
|
||||
return t;
|
||||
})());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*= T_SyncTime ===============================================================*/
|
||||
|
@ -228,6 +241,23 @@ void T_SyncManager::setTime(
|
|||
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
void T_SyncManager::checkCurveFile( )
|
||||
{
|
||||
if ( watcher_ ) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf( "CURVE INIT\n" );
|
||||
bool missing;
|
||||
if ( loadCurves_( missing ) || !missing ) {
|
||||
watcher_ = std::make_unique< T_WatchedFiles >(
|
||||
Globals::Watcher( ) ,
|
||||
[this] { curvesChanged_( ); } );
|
||||
watcher_->watch( "curves.json" );
|
||||
}
|
||||
printf( "INIT MISSING IS %c\n" , missing ? 'Y' : 'N' );
|
||||
}
|
||||
|
||||
void T_SyncManager::clearCurves( )
|
||||
{
|
||||
curves_.clear( );
|
||||
|
@ -241,6 +271,174 @@ void T_SyncManager::setCurve(
|
|||
updateCurveCaches( );
|
||||
}
|
||||
|
||||
void T_SyncManager::curvesChanged_( )
|
||||
{
|
||||
bool missing;
|
||||
if ( !loadCurves_( missing ) && missing ) {
|
||||
watcher_.reset( );
|
||||
} else {
|
||||
// XXX temp bullshit to work around the deletion problem
|
||||
watcher_->clear( );
|
||||
watcher_->watch( "curves.json" );
|
||||
}
|
||||
}
|
||||
|
||||
bool T_SyncManager::loadCurves_(
|
||||
__wr__ bool& missing )
|
||||
{
|
||||
using T_STI_ = std::istream_iterator< char >;
|
||||
std::ifstream file( "curves.json" );
|
||||
picojson::value root;
|
||||
try {
|
||||
missing = !file.is_open( );
|
||||
if ( missing ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string errors;
|
||||
picojson::parse( root , T_STI_( file ) , T_STI_( ) , &errors );
|
||||
if ( !errors.empty( ) ) {
|
||||
printf( "Failed to parse 'curves.json':\n%s\n" , errors.c_str( ) );
|
||||
return false;
|
||||
}
|
||||
} catch ( std::ios_base::failure const& e ) {
|
||||
printf( "I/O error while reading 'curves.json'\n%s\n" , e.what( ) );
|
||||
missing = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
T_SyncCurves nCurves;
|
||||
if ( !loadCurvesData_( nCurves , root ) ) {
|
||||
return false;
|
||||
}
|
||||
curves_ = std::move( nCurves );
|
||||
updateCurveCaches( );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool T_SyncManager::loadCurvesData_(
|
||||
__wr__ T_SyncCurves& curves ,
|
||||
__rd__ picojson::value const& root )
|
||||
{
|
||||
if ( !root.is< T_JSONObject >( ) ) {
|
||||
printf( "Curves data: root is not a JSON object\n" );
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const& r( root.get< T_JSONObject >( ) );
|
||||
bool ok( true );
|
||||
for ( auto const& item : r ) {
|
||||
if ( curves.indexOf( item.first ) != -1 ) {
|
||||
printf( "Curves data: duplicate curve '%s'\n" ,
|
||||
item.first.c_str( ) );
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
if ( !item.second.is< T_JSONArray >( ) ) {
|
||||
printf( "Curves data: entry for curve '%s' is not an array\n" ,
|
||||
item.first.c_str( ) );
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
T_SyncCurve nsc;
|
||||
nsc.name = item.first;
|
||||
|
||||
bool segsOk( true );
|
||||
for ( auto const& v : item.second.get< T_JSONArray >( ) ) {
|
||||
if ( !v.is< T_JSONObject >( ) ) {
|
||||
printf( "Curves data: curve '%s': invalid segment\n" ,
|
||||
item.first.c_str( ) );
|
||||
segsOk = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
T_SyncSegment segment;
|
||||
try {
|
||||
if ( !loadSegmentData_( segment , nsc.name ,
|
||||
v.get< T_JSONObject >( ) ) ) {
|
||||
segsOk = false;
|
||||
continue;
|
||||
}
|
||||
} catch ( X_JsonGetFailed const& ) {
|
||||
printf( "Curves data: curve '%s': could not parse segment data\n" ,
|
||||
item.first.c_str( ) );
|
||||
segsOk = false;
|
||||
continue;
|
||||
}
|
||||
nsc.segments.emplace_back( std::move( segment ) );
|
||||
}
|
||||
|
||||
ok = ok && segsOk;
|
||||
if ( nsc.segments.empty( ) && segsOk ) {
|
||||
printf( "Curves data: curve '%s': no segments\n" ,
|
||||
item.first.c_str( ) );
|
||||
ok = false;
|
||||
}
|
||||
curves.setCurve( std::move( nsc ) );
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool T_SyncManager::loadSegmentData_(
|
||||
__wr__ T_SyncSegment& segment ,
|
||||
__rd__ std::string const& curve ,
|
||||
__rd__ T_JSONObject const& sd )
|
||||
{
|
||||
auto const& sType( jsonGet< std::string >( sd , "type" ) );
|
||||
|
||||
const auto p( SegmentTypes_.find( sType ) );
|
||||
if ( p == SegmentTypes_.end( ) ) {
|
||||
printf( "Curves data: curve '%s': invalid segment type '%s'\n" ,
|
||||
curve.c_str( ) , sType.c_str( ) );
|
||||
return false;
|
||||
}
|
||||
segment.type = p->second;
|
||||
|
||||
auto const& vList( jsonGet< T_JSONArray >( sd , "values" ) );
|
||||
auto const& dList( jsonGet< T_JSONArray >( sd , "durations" ) );
|
||||
|
||||
if ( vList.size( ) < 2 ) {
|
||||
printf( "Curves data: curve '%s': segment doesn't have enough values\n" ,
|
||||
curve.c_str( ) );
|
||||
return false;
|
||||
}
|
||||
if ( vList.size( ) != dList.size( ) + 1 ) {
|
||||
printf( "Curves data: curve '%s': segment values / durations count mismatch\n" ,
|
||||
curve.c_str( ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
for ( auto const& vv : vList ) {
|
||||
if ( !vv.is< double >( ) ) {
|
||||
printf( "Curves data: curve '%s': non-numeric entry in segment values\n" ,
|
||||
curve.c_str( ) );
|
||||
return false;
|
||||
}
|
||||
segment.values.push_back( float( vv.get< double >( ) ) );
|
||||
}
|
||||
|
||||
for ( auto const& dv : dList ) {
|
||||
if ( !dv.is< double >( ) ) {
|
||||
printf( "Curves data: curve '%s': non-numeric entry in segment durations\n" ,
|
||||
curve.c_str( ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
const double dvn( dv.get< double >( ) );
|
||||
if ( fmod( dvn , 1.0 ) != 0.0 || dvn <= 0 ) {
|
||||
printf( "Curves data: curve '%s': invalid segment duration %f\n" ,
|
||||
curve.c_str( ) , dvn );
|
||||
return false;
|
||||
}
|
||||
segment.durations.push_back( uint32_t( dvn ) );
|
||||
}
|
||||
|
||||
segment.nPoints = segment.values.size( );
|
||||
return true;
|
||||
}
|
||||
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
void T_SyncManager::updateCurveCaches( )
|
||||
|
|
23
sync.hh
23
sync.hh
|
@ -1,8 +1,6 @@
|
|||
#pragma once
|
||||
#ifndef REAL_BUILD
|
||||
# include "externals.hh"
|
||||
#endif
|
||||
|
||||
#include "filewatcher.hh"
|
||||
#include "utilities.hh"
|
||||
|
||||
// Duration and current playing time
|
||||
struct T_SyncTime
|
||||
|
@ -174,15 +172,30 @@ struct T_SyncManager
|
|||
// ---------------------------------------------------------------------
|
||||
// Curves
|
||||
|
||||
void checkCurveFile( );
|
||||
void clearCurves( );
|
||||
void setCurve( __rd__ T_SyncCurve curve );
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
private:
|
||||
void curvesChanged_( );
|
||||
bool loadCurves_( __wr__ bool& missing );
|
||||
bool loadCurvesData_(
|
||||
__wr__ T_SyncCurves& curves ,
|
||||
__rd__ picojson::value const& root );
|
||||
bool loadSegmentData_(
|
||||
__wr__ T_SyncSegment& segment ,
|
||||
__rd__ std::string const& curve ,
|
||||
__rd__ T_JSONObject const& sd );
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Update
|
||||
|
||||
public:
|
||||
void updateCurveCaches( );
|
||||
void updateValues( );
|
||||
|
||||
private:
|
||||
P_WatchedFiles watcher_;
|
||||
T_SyncTime time_;
|
||||
T_SyncValues values_;
|
||||
T_SyncCurves curves_;
|
||||
|
|
24
utilities.cc
24
utilities.cc
|
@ -44,25 +44,25 @@ void anglesToMatrix(
|
|||
/*= JSON =====================================================================*/
|
||||
|
||||
template void jsonAdd< double >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ double const& v );
|
||||
template void jsonAdd< picojson::value::object >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
template void jsonAdd< T_JSONObject >(
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ picojson::value::object const& v );
|
||||
template void jsonAdd< picojson::value::array >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rd__ T_JSONObject const& v );
|
||||
template void jsonAdd< T_JSONArray >(
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ picojson::value::array const& v );
|
||||
__rd__ T_JSONArray const& v );
|
||||
template void jsonAdd< std::string >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ std::string const& v );
|
||||
|
||||
template< >
|
||||
void jsonAdd< picojson::value >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ picojson::value const& v )
|
||||
{
|
||||
|
@ -72,7 +72,7 @@ void jsonAdd< picojson::value >(
|
|||
|
||||
template< >
|
||||
void jsonAdd< int >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ int const& v )
|
||||
{
|
||||
|
@ -82,7 +82,7 @@ void jsonAdd< int >(
|
|||
|
||||
template< >
|
||||
void jsonAdd< unsigned >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ unsigned const& v )
|
||||
{
|
||||
|
@ -107,7 +107,7 @@ picojson::value jsonVector( float const* vector , const int nComponents )
|
|||
void jsonGetVectorOpt(
|
||||
__wr__ float* vector ,
|
||||
__rd__ const size_t size ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* key )
|
||||
{
|
||||
using namespace picojson;
|
||||
|
|
65
utilities.hh
65
utilities.hh
|
@ -62,6 +62,9 @@ inline auto find(
|
|||
|
||||
/*= JSON utilities ===========================================================*/
|
||||
|
||||
using T_JSONObject = picojson::value::object;
|
||||
using T_JSONArray = picojson::value::array;
|
||||
|
||||
// Convert a vector to a JSON array
|
||||
picojson::value jsonVector(
|
||||
__rd__ float const* vector ,
|
||||
|
@ -77,7 +80,7 @@ inline picojson::value jsonVector(
|
|||
// Add an entry to a JSON object
|
||||
template< typename T >
|
||||
inline void jsonAdd(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ T const& v )
|
||||
{
|
||||
|
@ -86,35 +89,35 @@ inline void jsonAdd(
|
|||
}
|
||||
|
||||
extern template void jsonAdd< double >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ double const& v );
|
||||
extern template void jsonAdd< picojson::value::object >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
extern template void jsonAdd< T_JSONObject >(
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ picojson::value::object const& v );
|
||||
extern template void jsonAdd< picojson::value::array >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rd__ T_JSONObject const& v );
|
||||
extern template void jsonAdd< T_JSONArray >(
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ picojson::value::array const& v );
|
||||
__rd__ T_JSONArray const& v );
|
||||
extern template void jsonAdd< std::string >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ std::string const& v );
|
||||
|
||||
template< >
|
||||
void jsonAdd< picojson::value >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ picojson::value const& v );
|
||||
template< >
|
||||
void jsonAdd< int >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ int const& v );
|
||||
template< >
|
||||
void jsonAdd< unsigned >(
|
||||
__rw__ picojson::value::object& object ,
|
||||
__rw__ T_JSONObject& object ,
|
||||
__rd__ std::string const& key ,
|
||||
__rd__ unsigned const& v );
|
||||
|
||||
|
@ -123,11 +126,30 @@ void jsonAdd< unsigned >(
|
|||
// JSON read type error
|
||||
class X_JsonGetFailed : public std::exception { };
|
||||
|
||||
// Get a JSON object field; throws X_JsonGetFailed if the type is incorrect or
|
||||
// if the field is missing.
|
||||
template< typename T >
|
||||
inline T const& jsonGet(
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* const key )
|
||||
{
|
||||
const std::string k( key );
|
||||
if ( object.find( k ) == object.end( ) ) {
|
||||
throw X_JsonGetFailed( );
|
||||
}
|
||||
|
||||
picojson::value const& v( object.at( k ) );
|
||||
if ( !v.is< T >( ) ) {
|
||||
throw X_JsonGetFailed( );
|
||||
}
|
||||
return v.get< T >( );
|
||||
}
|
||||
|
||||
// Get a pointer to a JSON object field, or nullptr if the field doesn't exist.
|
||||
// Throws X_JsonGetFailed if the type is incorrect.
|
||||
template< typename T >
|
||||
inline T const* jsonGetOpt(
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* const key )
|
||||
{
|
||||
const std::string k( key );
|
||||
|
@ -147,7 +169,7 @@ inline T const* jsonGetOpt(
|
|||
template< typename T >
|
||||
inline bool jsonSetFromOpt(
|
||||
__wr__ T& out ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* const key )
|
||||
{
|
||||
T const* ptr( jsonGetOpt< T >( object , key ) );
|
||||
|
@ -161,7 +183,7 @@ inline bool jsonSetFromOpt(
|
|||
template< >
|
||||
inline bool jsonSetFromOpt< int >(
|
||||
__wr__ int& out ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* const key )
|
||||
{
|
||||
double const* ptr( jsonGetOpt< double >( object , key ) );
|
||||
|
@ -175,7 +197,7 @@ inline bool jsonSetFromOpt< int >(
|
|||
template< >
|
||||
inline bool jsonSetFromOpt< unsigned >(
|
||||
__wr__ unsigned& out ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* const key )
|
||||
{
|
||||
double const* ptr( jsonGetOpt< double >( object , key ) );
|
||||
|
@ -189,7 +211,7 @@ inline bool jsonSetFromOpt< unsigned >(
|
|||
template< >
|
||||
inline bool jsonSetFromOpt< float >(
|
||||
__wr__ float& out ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* const key )
|
||||
{
|
||||
double const* ptr( jsonGetOpt< double >( object , key ) );
|
||||
|
@ -203,13 +225,13 @@ inline bool jsonSetFromOpt< float >(
|
|||
void jsonGetVectorOpt(
|
||||
__wr__ float* vector ,
|
||||
__rd__ const size_t size ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* key );
|
||||
|
||||
// Read a GLM vec3 from a JSON array of numbers
|
||||
inline void jsonGetVectorOpt(
|
||||
__wr__ glm::vec3& vector ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* key )
|
||||
{
|
||||
jsonGetVectorOpt( &vector.x , 3 , object , key );
|
||||
|
@ -219,12 +241,11 @@ inline void jsonGetVectorOpt(
|
|||
template< typename T >
|
||||
inline bool jsonLoadOpt(
|
||||
__rw__ T& out ,
|
||||
__rd__ picojson::value::object const& object ,
|
||||
__rd__ T_JSONObject const& object ,
|
||||
__rd__ char const* key )
|
||||
{
|
||||
using T_Obj = picojson::value::object;
|
||||
try {
|
||||
auto const* sub( jsonGetOpt< T_Obj >( object , key ) );
|
||||
auto const* sub( jsonGetOpt< T_JSONObject >( object , key ) );
|
||||
if ( sub ) {
|
||||
return out.load( *sub );
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue