commit 40e6016e6ce62d7c882a23d60a367c9561db2916 Author: theAdib Date: Sun Oct 25 10:39:21 2020 +0100 add files to plugin diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d1bf134 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,23 @@ +# CloudCompare Json RPC plugin based on example for standard plugins + +# Add an option to CMake to control whether we build this plugin or not +option( PLUGIN_JSONRPC "Install Json RPC plugin" ON ) + +if ( PLUGIN_JSONRPC ) + project( JsonRPCPlugin ) + + AddPlugin( NAME ${PROJECT_NAME} ) + + find_package(Qt5 COMPONENTS Network WebSockets REQUIRED) + target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::WebSockets) + + add_subdirectory( include ) + add_subdirectory( src ) + + target_include_directories( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + # set dependencies to necessary libraries + # target_link_libraries( ${PROJECT_NAME} LIB1 ) +endif() diff --git a/JsonRPCPlugin.qrc b/JsonRPCPlugin.qrc new file mode 100644 index 0000000..8c2773d --- /dev/null +++ b/JsonRPCPlugin.qrc @@ -0,0 +1,6 @@ + + + images/icon.png + info.json + + diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000..2c923cc Binary files /dev/null and b/images/icon.png differ diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt new file mode 100644 index 0000000..58bfd17 --- /dev/null +++ b/include/CMakeLists.txt @@ -0,0 +1,11 @@ + +target_sources( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/JsonRPCPlugin.h + ${CMAKE_CURRENT_LIST_DIR}/jsonrpcserver.h +) + +target_include_directories( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/include/JsonRPCPlugin.h b/include/JsonRPCPlugin.h new file mode 100644 index 0000000..bf9d532 --- /dev/null +++ b/include/JsonRPCPlugin.h @@ -0,0 +1,75 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: ExamplePlugin # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: XXX # +//# # +//########################################################################## + +#pragma once + +#include "ccStdPluginInterface.h" +//#include +#include "jsonrpcserver.h" + +//! Example qCC plugin +/** Replace 'ExamplePlugin' by your own plugin class name throughout and then + check 'ExamplePlugin.cpp' for more directions. + + Each plugin requires an info.json file to provide information about itself - + the name, authors, maintainers, icon, etc.. + + The one method you are required to implement is 'getActions'. This should + return all actions (QAction objects) for the plugin. CloudCompare will + automatically add these with their icons in the plugin toolbar and to the + plugin menu. If your plugin returns several actions, CC will create a + dedicated toolbar and a sub-menu for your plugin. You are responsible for + connecting these actions to methods in your plugin. + + Use the ccStdPluginInterface::m_app variable for access to most of the CC + components (database, 3D views, console, etc.) - see the ccMainAppInterface + class in ccMainAppInterface.h. +**/ +class JsonRPCPlugin : public QObject, public ccStdPluginInterface +{ + Q_OBJECT + Q_INTERFACES( ccPluginInterface ccStdPluginInterface ) + + // Replace "Example" by your plugin name (IID should be unique - let's hope your plugin name is unique ;) + // The info.json file provides information about the plugin to the loading system and + // it is displayed in the plugin information dialog. + Q_PLUGIN_METADATA( IID "cccorp.cloudcompare.plugin.JsonRPC" FILE "../info.json" ) + +public: + explicit JsonRPCPlugin( QObject *parent = nullptr ); + ~JsonRPCPlugin() override = default; + + // Inherited from ccStdPluginInterface + void onNewSelection( const ccHObject::Container &selectedEntities ) override; + QList getActions() override; +public slots: + void triggered(bool checked); + JsonRPCResult execute(QString method, QMap params); + +private: + //! Default action + /** You can add as many actions as you want in a plugin. + Each action will correspond to an icon in the dedicated + toolbar and an entry in the plugin menu. + **/ + QAction* m_action{nullptr}; +protected: + //jcon::JsonRpcTcpServer rpc_server; + // QTcpServer rpc_server; + // QWebSocketServer wsrpc_server(QStringLiteral("CloudCompare"), QWebSocketServer::NonSecureMode); + JsonRPCServer rpc_server; +}; diff --git a/include/jsonrpcserver.h b/include/jsonrpcserver.h new file mode 100644 index 0000000..5839d6c --- /dev/null +++ b/include/jsonrpcserver.h @@ -0,0 +1,52 @@ +#ifndef JSONRPCSERVER_H +#define JSONRPCSERVER_H + +#include +#include +#include +#include +#include +#include "ccStdPluginInterface.h" + +class JsonRPCResult { +public: + static JsonRPCResult error(int code, QString message) { + JsonRPCResult result = { .isError = true, .error_code = code, .error_message = message }; + return result; + } + static JsonRPCResult success(QVariant value) { + JsonRPCResult result = { .isError = false, .result = value }; + return result; + } + bool isError{true}; + int error_code{-32601}; + QString error_message = "Method not found"; + QVariant result; + +}; + +class JsonRPCServer : public QObject +{ + Q_OBJECT +public: + explicit JsonRPCServer(QObject *parent = nullptr); + ~JsonRPCServer(); + + void listen(unsigned int port); + void close(); + +signals: + JsonRPCResult execute(QString method, QMap params); +private slots: + void onNewConnection(); + void onClosed(); + void processTextMessage(QString message); + void processBinaryMessage(QByteArray message); + void socketDisconnected(); + +private: + QWebSocketServer *ws_server{nullptr}; + QList connections; +}; + +#endif // JSONRPCSERVER_H diff --git a/info.json b/info.json new file mode 100644 index 0000000..44cf5ac --- /dev/null +++ b/info.json @@ -0,0 +1,24 @@ +{ + "type" : "Standard", + "name" : "Json RPC (Standard Plugin)", + "icon" : ":/CC/plugin/JsonRPCPlugin/images/icon.png", + "description": "This is a Json RPC interface for CloudCompare.", + "authors" : [ + { + "name" : "theAdib", + "email" : "theadib@gmail.com" + } + ], + "maintainers" : [ + { + "name" : "theAdib", + "email" : "theadib@gmail.com" + } + ], + "references" : [ + { + "text" : "Sword swallowing and its side effects", + "url" : "http://www.bmj.com/content/333/7582/1285" + } + ] +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..6f5fb33 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,6 @@ + +target_sources( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/JsonRPCPlugin.cpp + ${CMAKE_CURRENT_LIST_DIR}/jsonrpcserver.cpp +) diff --git a/src/JsonRPCPlugin.cpp b/src/JsonRPCPlugin.cpp new file mode 100644 index 0000000..4e3c723 --- /dev/null +++ b/src/JsonRPCPlugin.cpp @@ -0,0 +1,184 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: JsonRPCPlugin # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: XXX # +//# # +//########################################################################## + +// First: +// Replace all occurrences of 'ExamplePlugin' by your own plugin class name in this file. +// This includes the resource path to info.json in the constructor. + +// Second: +// Open ExamplePlugin.qrc, change the "prefix" and the icon filename for your plugin. +// Change the name of the file to .qrc + +// Third: +// Open the info.json file and fill in the information about the plugin. +// "type" should be one of: "Standard", "GL", or "I/O" (required) +// "name" is the name of the plugin (required) +// "icon" is the Qt resource path to the plugin's icon (from the .qrc file) +// "description" is used as a tootip if the plugin has actions and is displayed in the plugin dialog +// "authors", "maintainers", and "references" show up in the plugin dialog as well + +#include + +#include "JsonRPCPlugin.h" +#include "ccGLWindow.h" +#include "ccMainAppInterface.h" +#include "FileIOFilter.h" +#include "ccGenericPointCloud.h" + + +// Default constructor: +// - pass the Qt resource path to the info.json file (from .qrc file) +// - constructor should mainly be used to initialize actions and other members +JsonRPCPlugin::JsonRPCPlugin( QObject *parent ) + : QObject( parent ) + , ccStdPluginInterface( ":/CC/plugin/JsonRPCPlugin/info.json" ) +{ + qDebug() << "JsonRPCPlugin::JsonRPCPlugin"; + + connect(&rpc_server, &JsonRPCServer::execute, this, &JsonRPCPlugin::execute); +} + + +// This method should enable or disable your plugin actions +// depending on the currently selected entities ('selectedEntities'). +void JsonRPCPlugin::onNewSelection( const ccHObject::Container &selectedEntities ) +{ + qDebug() << "JsonRPCPlugin::onNewSelection"; + if ( m_action == nullptr ) + { + return; + } + + // If you need to check for a specific type of object, you can use the methods + // in ccHObjectCaster.h or loop and check the objects' classIDs like this: + // + // for ( ccHObject *object : selectedEntities ) + // { + // if ( object->getClassID() == CC_TYPES::VIEWPORT_2D_OBJECT ) + // { + // // ... do something with the viewports + // } + // } + + // For example - only enable our action if something is selected. + m_action->setEnabled(true); +} + +// This method returns all the 'actions' your plugin can perform. +// getActions() will be called only once, when plugin is loaded. +QList JsonRPCPlugin::getActions() +{ + qDebug() << "JsonRPCPlugin::getActions"; + + // default action (if it has not been already created, this is the moment to do it) + if ( !m_action ) + { + // Here we use the default plugin name, description, and icon, + // but each action should have its own. + m_action = new QAction( getName(), this ); + m_action->setToolTip( getDescription() ); + m_action->setIcon( getIcon() ); + m_action->setCheckable(true); + m_action->setChecked(false); + + // Connect appropriate signal + connect( m_action, &QAction::triggered, this, &JsonRPCPlugin::triggered); + } + + return { m_action }; +} + +void JsonRPCPlugin::triggered(bool checked) +{ + qDebug() << "JsonRPCPlugin::triggered " << checked; + + if(checked) { + rpc_server.listen(6001); + } else { + rpc_server.close(); + } +} + +JsonRPCResult JsonRPCPlugin::execute(QString method, QMap params) +{ + qDebug() << method << params; + if(m_app == nullptr) { + return JsonRPCResult(); + } + + JsonRPCResult result; + bool need_redraw = false; + if(method == "open") { + QString filename = params["filename"].toString(); + // code copied from MainWindow::addToDB() + //to use the same 'global shift' for multiple files + CCVector3d loadCoordinatesShift(0,0,0); + bool loadCoordinatesTransEnabled = false; + + FileIOFilter::LoadParameters parameters; + { + parameters.alwaysDisplayLoadDialog = true; + parameters.shiftHandlingMode = ccGlobalShiftManager::DIALOG_IF_NECESSARY; + parameters.coordinatesShift = &loadCoordinatesShift; + parameters.coordinatesShiftEnabled = &loadCoordinatesTransEnabled; + parameters.parentWidget = m_app->getActiveGLWindow(); + } + + if(params.contains("silent")) { + parameters.alwaysDisplayLoadDialog = false; + } + CC_FILE_ERROR res = CC_FERR_NO_ERROR; + ccHObject* newGroup = FileIOFilter::LoadFromFile(filename, parameters, res, params["filter"].toString()); + + if(newGroup) { + //disable the normals on all loaded clouds! + ccHObject::Container clouds; + newGroup->filterChildren(clouds, true, CC_TYPES::POINT_CLOUD); + for (ccHObject* cloud : clouds) + { + if (cloud) + { + static_cast(cloud)->showNormals(false); + } + } + + m_app->addToDB(newGroup); + need_redraw = true; + result = JsonRPCResult::success(0); + } else { + result = JsonRPCResult::error(1, "cancelled by user"); + } + } else if(method == "clear") { + + // remove everything below root + auto root = m_app->dbRootObject(); + ccHObject* child; + while((child = root->getChild(0)) != nullptr) { + m_app->removeFromDB(child, true); + } + need_redraw = true; + result = JsonRPCResult::success(0); + } + // redraw + if(need_redraw) { + ccGLWindow* win = m_app->getActiveGLWindow(); + if (win) + win->redraw(); + } + + return result; +} diff --git a/src/jsonrpcserver.cpp b/src/jsonrpcserver.cpp new file mode 100644 index 0000000..bff222c --- /dev/null +++ b/src/jsonrpcserver.cpp @@ -0,0 +1,128 @@ +#include "jsonrpcserver.h" +#include +#include + +JsonRPCServer::JsonRPCServer(QObject *parent) : QObject(parent), + ws_server(new QWebSocketServer(QStringLiteral("CloudCompare"), QWebSocketServer::NonSecureMode)) +{ + if(ws_server) { + connect(ws_server, &QWebSocketServer::newConnection, this, &JsonRPCServer::onNewConnection); + connect(ws_server, &QWebSocketServer::closed, this, &JsonRPCServer::onClosed); + } +} + +JsonRPCServer::~JsonRPCServer() +{ + for(QWebSocket *conn: connections) { + conn->close(); + delete conn; + } +} + +void JsonRPCServer::listen(unsigned int port) +{ + qDebug() << "JsonRPCServer::listen"; + if(ws_server == nullptr) { + return; + } + if(ws_server->isListening()) { + ws_server->close(); + } + ws_server->listen(QHostAddress::Any, port); +} + +void JsonRPCServer::close() +{ + qDebug() << "JsonRPCServer::close"; + if(ws_server == nullptr) { + return; + } + ws_server->close(); + + for(QWebSocket *conn: connections) { + conn->close(); + conn->deleteLater(); + } + connections.clear(); +} + +void JsonRPCServer::onNewConnection() +{ + qDebug() << "JsonRPCServer::onNewConnection"; + QWebSocket *pSocket = ws_server->nextPendingConnection(); + if(pSocket == nullptr) { + return; + } + connect(pSocket, &QWebSocket::textMessageReceived, this, &JsonRPCServer::processTextMessage); + connect(pSocket, &QWebSocket::binaryMessageReceived, this, &JsonRPCServer::processBinaryMessage); + connect(pSocket, &QWebSocket::disconnected, this, &JsonRPCServer::socketDisconnected); + + connections.append(pSocket); +} + +void JsonRPCServer::onClosed() +{ + qDebug() << "JsonRPCServer::onClosed"; +} + +void JsonRPCServer::processTextMessage(QString message) +{ + QWebSocket *pClient = qobject_cast(sender()); + qDebug() << "Message received:" << message; + if (pClient == nullptr) { + return; + } + QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); + auto method = doc["method"].toString(); + auto params = doc["params"].toVariant().toMap(); + + qDebug() << "method: " << method << ", params: " << params; + // check invalid JSON RPC + JsonRPCResult result; + if(!method.isEmpty() && !doc["jsonrpc"].toString().isEmpty()) { + // perform the RPC + result = emit execute(method, params); + } else { + result = JsonRPCResult::error(-32600, "Invalid Request"); + } + // now build JSON response + if(!doc.object().contains("id")) { + // abort here, on notification there is NO RESPONSE + return; + } + QJsonObject response; + response["jsonrpc"] = "2.0"; + response["id"] = doc["id"]; + if(result.isError) { + QJsonObject error; + error["code"] = result.error_code; + if(!result.error_message.isEmpty()) { + error["message"] = result.error_message; + } + response["error"] = error; + } else { + response["result"] = QJsonValue::fromVariant(result.result); + } + + pClient->sendTextMessage(QJsonDocument(response).toJson()); +} + +void JsonRPCServer::processBinaryMessage(QByteArray message) +{ + QWebSocket *pClient = qobject_cast(sender()); + qDebug() << "Binary Message received:" << message; + if (pClient) { + pClient->sendBinaryMessage(message); + } +} + +void JsonRPCServer::socketDisconnected() +{ + QWebSocket *pClient = qobject_cast(sender()); + qDebug() << "socketDisconnected:" << pClient; + if (pClient) { + connections.removeAll(pClient); + pClient->deleteLater(); + } +} + diff --git a/test/client_clear.py b/test/client_clear.py new file mode 100755 index 0000000..b6f2fdc --- /dev/null +++ b/test/client_clear.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import asyncio +import websockets + +async def hello(): + uri = "ws://localhost:6001" + async with websockets.connect(uri) as websocket: + await websocket.send('{"jsonrpc": "2.0", "method": "clear", "id": 5}') + result = await websocket.recv() + print("result: ", result) + +asyncio.get_event_loop().run_until_complete(hello()) + + + diff --git a/test/client_open.py b/test/client_open.py new file mode 100755 index 0000000..50ea237 --- /dev/null +++ b/test/client_open.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import asyncio +import websockets + +async def hello(): + uri = "ws://localhost:6001" + async with websockets.connect(uri) as websocket: + await websocket.send('{"jsonrpc": "2.0", "method": "open2", "params": {"filename": "/home/adib/Dokumente/teapt.ply"}, "id": 4}') + result = await websocket.recv() + print("result: ", result) + +asyncio.get_event_loop().run_until_complete(hello()) + + + diff --git a/test/client_open2.py b/test/client_open2.py new file mode 100755 index 0000000..212dbf3 --- /dev/null +++ b/test/client_open2.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import asyncio +import websockets + +async def hello(): + uri = "ws://localhost:6001" + async with websockets.connect(uri) as websocket: + await websocket.send('{"jsonrpc": "2.0", "method": "open", "params": {"filename": "/home/adib/Dokumente/teapot.ply", "filter":"PLY mesh (*.ply)", "silent":true}, "id": 4}') + result = await websocket.recv() + print("result: ", result) + +asyncio.get_event_loop().run_until_complete(hello()) + + +