diff --git a/src/Mod/Material/App/CMakeLists.txt b/src/Mod/Material/App/CMakeLists.txt index 825242fce7..bc976d7293 100644 --- a/src/Mod/Material/App/CMakeLists.txt +++ b/src/Mod/Material/App/CMakeLists.txt @@ -6,6 +6,10 @@ endif(MSVC) add_definitions(-DYAML_CPP_STATIC_DEFINE) +if(BUILD_MATERIAL_EXTERNAL) + add_definitions(-DBUILD_MATERIAL_EXTERNAL) +endif(BUILD_MATERIAL_EXTERNAL) + include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/src @@ -55,6 +59,11 @@ generate_from_py(MaterialProperty) generate_from_py(Model) generate_from_py(UUIDs) +SET(MaterialsAPI_Files + MaterialAPI/__init__.py + MaterialAPI/MaterialManagerExternal.py +) + SET(Python_SRCS Exceptions.h Array2D.pyi @@ -126,6 +135,12 @@ SET(Materials_SRCS PyVariants.h trim.h ) +if(BUILD_MATERIAL_EXTERNAL) + list(APPEND Materials_SRCS + ExternalManager.cpp + ExternalManager.h + ) +endif(BUILD_MATERIAL_EXTERNAL) if(FREECAD_USE_PCH) add_definitions(-D_PreComp_) @@ -143,3 +158,14 @@ SET_BIN_DIR(Materials Materials /Mod/Material) SET_PYTHON_PREFIX_SUFFIX(Materials) INSTALL(TARGETS Materials DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +ADD_CUSTOM_TARGET(MaterialsAPILib ALL + SOURCES ${MaterialsAPI_Files} ${Material_QRC_SRCS} +) + +fc_target_copy_resource(MaterialsAPILib + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_BINARY_DIR}/Mod/Material + ${MaterialsAPI_Files}) + +INSTALL(FILES ${MaterialsAPI_Files} DESTINATION Mod/Material/MaterialAPI) diff --git a/src/Mod/Material/App/ExternalManager.cpp b/src/Mod/Material/App/ExternalManager.cpp new file mode 100644 index 0000000000..22d3920e11 --- /dev/null +++ b/src/Mod/Material/App/ExternalManager.cpp @@ -0,0 +1,786 @@ +/*************************************************************************** + * Copyright (c) 2024 David Carter * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include "Exceptions.h" +#include "ExternalManager.h" +#include "MaterialLibrary.h" +#include "MaterialLibraryPy.h" +#include "MaterialPy.h" +#include "ModelLibrary.h" +#include "ModelPy.h" +#include "MaterialFilterPy.h" +#include "MaterialFilterOptionsPy.h" + + +using namespace Materials; + +/* TRANSLATOR Material::Materials */ + +ExternalManager* ExternalManager::_manager = nullptr; +QMutex ExternalManager::_mutex; + +ExternalManager::ExternalManager() + : _instantiated(false) +{ + _hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Material/ExternalInterface"); + _hGrp->Attach(this); + + getConfiguration(); +} + +ExternalManager::~ExternalManager() +{ + _hGrp->Detach(this); +} + +void ExternalManager::OnChange(ParameterGrp::SubjectType& /*rCaller*/, ParameterGrp::MessageType Reason) +{ + if (std::strncmp(Reason, "Current", 7) == 0) { + if (_instantiated) { + // The old manager object will be deleted when reconnecting + _instantiated = false; + } + getConfiguration(); + } +} + +void ExternalManager::getConfiguration() +{ + // _hGrp = App::GetApplication().GetParameterGroupByPath( + // "User parameter:BaseApp/Preferences/Mod/Material/ExternalInterface"); + auto current = _hGrp->GetASCII("Current", "None"); + if (current == "None") { + _moduleName = ""; + _className = ""; + } + else { + auto groupName = + "User parameter:BaseApp/Preferences/Mod/Material/ExternalInterface/Interfaces/" + + current; + auto hGrp = App::GetApplication().GetParameterGroupByPath(groupName.c_str()); + _moduleName = hGrp->GetASCII("Module", ""); + _className = hGrp->GetASCII("Class", ""); + } +} + +void ExternalManager::instantiate() +{ + _instantiated = false; + Base::Console().Log("Loading external manager...\n"); + + if (_moduleName.empty() || _className.empty()) { + Base::Console().Log("External module not defined\n"); + return; + } + + try { + Base::PyGILStateLocker lock; + Py::Module mod(PyImport_ImportModule(_moduleName.c_str()), true); + + if (mod.isNull()) { + Base::Console().Log(" failed\n"); + return; + } + + Py::Callable managerClass(mod.getAttr(_className)); + _managerObject = managerClass.apply(); + if (_managerObject.hasAttr("APIVersion")) { + _instantiated = true; + } + + if (_instantiated) { + Base::Console().Log("done\n"); + } + else { + Base::Console().Log("failed\n"); + } + } + catch (Py::Exception& e) { + Base::Console().Log("failed\n"); + e.clear(); + } +} + +void ExternalManager::connect() +{ + if (!_instantiated) { + instantiate(); + + if (!_instantiated) { + throw ConnectionError(); + } + } +} + +void ExternalManager::initManager() +{ + QMutexLocker locker(&_mutex); + + if (!_manager) { + _manager = new ExternalManager(); + } +} + +ExternalManager* ExternalManager::getManager() +{ + initManager(); + + return _manager; +} + +//===== +// +// Library management +// +//===== + +bool ExternalManager::checkMaterialLibraryType(const Py::Object& entry) +{ + return entry.hasAttr("name") && entry.hasAttr("icon") && entry.hasAttr("readOnly") + && entry.hasAttr("timestamp"); +} + +std::shared_ptr +ExternalManager::libraryFromObject(const Py::Object& entry) +{ + if (!checkMaterialLibraryType(entry)) { + throw InvalidLibrary(); + } + + Py::String pyName(entry.getAttr("name")); + Py::Bytes pyIcon(entry.getAttr("icon")); + Py::Boolean pyReadOnly(entry.getAttr("readOnly")); + Py::String pyTimestamp(entry.getAttr("timestamp")); + + QString libraryName; + if (!pyName.isNone()) { + libraryName = QString::fromStdString(pyName.as_string()); + } + + QString icon; + if (!pyIcon.isNone()) { + icon = QString::fromStdString(pyIcon.as_string()); + } + + bool readOnly = pyReadOnly.as_bool(); + + QString timestamp; + if (!pyTimestamp.isNone()) { + timestamp = QString::fromStdString(pyTimestamp.as_string()); + } + + auto library = std::make_shared(libraryName, icon, readOnly, timestamp); + return library; +} + +bool ExternalManager::checkMaterialObjectType(const Py::Object& entry) +{ + return entry.hasAttr("UUID") && entry.hasAttr("path") && entry.hasAttr("name"); +} + +std::tuple +ExternalManager::materialObjectTypeFromObject(const Py::Object& entry) +{ + QString uuid; + auto pyUUID = entry.getAttr("UUID"); + if (!pyUUID.isNone()) { + uuid = QString::fromStdString(pyUUID.as_string()); + } + + QString path; + auto pyPath = entry.getAttr("path"); + if (!pyPath.isNone()) { + path = QString::fromStdString(pyPath.as_string()); + } + + QString name; + auto pyName = entry.getAttr("name"); + if (!pyName.isNone()) { + name = QString::fromStdString(pyName.as_string()); + } + + return std::tuple(uuid, path, name); +} + +std::shared_ptr>> +ExternalManager::libraries() +{ + auto libList = std::make_shared>>(); + + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("libraries")) { + Py::Callable libraries(_managerObject.getAttr("libraries")); + Py::List list(libraries.apply()); + for (auto lib : list) { + auto library = libraryFromObject(Py::Object(lib)); + libList->push_back(library); + } + } + else { + Base::Console().Log("\tlibraries() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw LibraryNotFound(e1.what()); + } + + return libList; +} + +std::shared_ptr>> ExternalManager::modelLibraries() +{ + auto libList = std::make_shared>>(); + + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("modelLibraries")) { + Py::Callable libraries(_managerObject.getAttr("modelLibraries")); + Py::List list(libraries.apply()); + for (auto lib : list) { + auto library = libraryFromObject(Py::Tuple(lib)); + libList->push_back(library); + } + } + else { + Base::Console().Log("\tmodelLibraries() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw LibraryNotFound(e1.what()); + } + + return libList; +} + +std::shared_ptr>> ExternalManager::materialLibraries() +{ + auto libList = std::make_shared>>(); + + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("materialLibraries")) { + Py::Callable libraries(_managerObject.getAttr("materialLibraries")); + Py::List list(libraries.apply()); + for (auto lib : list) { + auto library = libraryFromObject(Py::Tuple(lib)); + libList->push_back(library); + } + } + else { + Base::Console().Log("\tmaterialLibraries() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw LibraryNotFound(e1.what()); + } + + return libList; +} + +std::shared_ptr ExternalManager::getLibrary(const QString& name) +{ + // throw LibraryNotFound("Not yet implemented"); + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("getLibrary")) { + Py::Callable libraries(_managerObject.getAttr("getLibrary")); + Py::Tuple args(1); + args.setItem(0, Py::String(name.toStdString())); + Py::Tuple result(libraries.apply(args)); + + Py::Object libObject = result.getItem(0); + auto lib = libraryFromObject(Py::Tuple(libObject)); + return std::make_shared(*lib); + } + else { + Base::Console().Log("\tgetLibrary() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw CreationError(e1.what()); + } +} + +void ExternalManager::createLibrary(const QString& libraryName, const QString& icon, bool readOnly) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("createLibrary")) { + Py::Callable libraries(_managerObject.getAttr("createLibrary")); + Py::Tuple args(3); + args.setItem(0, Py::String(libraryName.toStdString())); + args.setItem(1, Py::String(icon.toStdString())); + args.setItem(2, Py::Boolean(readOnly)); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\tcreateLibrary() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw CreationError(e1.what()); + } +} + +void ExternalManager::renameLibrary(const QString& libraryName, const QString& newName) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("renameLibrary")) { + Py::Callable libraries(_managerObject.getAttr("renameLibrary")); + Py::Tuple args(2); + args.setItem(0, Py::String(libraryName.toStdString())); + args.setItem(1, Py::String(newName.toStdString())); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\trenameLibrary() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw RenameError(e1.what()); + } +} + +void ExternalManager::changeIcon(const QString& libraryName, const QString& icon) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("changeIcon")) { + Py::Callable libraries(_managerObject.getAttr("changeIcon")); + Py::Tuple args(2); + args.setItem(0, Py::String(libraryName.toStdString())); + args.setItem(1, Py::String(icon.toStdString())); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\tchangeIcon() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw ReplacementError(e1.what()); + } +} + +void ExternalManager::removeLibrary(const QString& libraryName) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("removeLibrary")) { + Py::Callable libraries(_managerObject.getAttr("removeLibrary")); + Py::Tuple args(1); + args.setItem(0, Py::String(libraryName.toStdString())); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\tremoveLibrary() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw DeleteError(e1.what()); + } +} + +std::shared_ptr>> +ExternalManager::libraryModels(const QString& libraryName) +{ + auto modelList = std::make_shared>>(); + + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("libraryModels")) { + Py::Callable libraries(_managerObject.getAttr("libraryModels")); + Py::Tuple args(1); + args.setItem(0, Py::String(libraryName.toStdString())); + Py::List list(libraries.apply(args)); + for (auto library : list) { + auto entry = Py::Object(library); + if (!checkMaterialObjectType(entry)) { + throw InvalidModel(); + } + + modelList->push_back(materialObjectTypeFromObject(entry)); + } + } + else { + Base::Console().Log("\tlibraryModels() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw LibraryNotFound(e1.what()); + } + + return modelList; +} + +std::shared_ptr>> +ExternalManager::libraryMaterials(const QString& libraryName) +{ + auto materialList = std::make_shared>>(); + + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("libraryMaterials")) { + Py::Callable libraries(_managerObject.getAttr("libraryMaterials")); + Py::Tuple args(1); + args.setItem(0, Py::String(libraryName.toStdString())); + Py::List list(libraries.apply(args)); + for (auto library : list) { + auto entry = Py::Object(library); + if (!checkMaterialObjectType(entry)) { + throw InvalidMaterial(); + } + + materialList->push_back(materialObjectTypeFromObject(entry)); + } + } + else { + Base::Console().Log("\tlibraryMaterials() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw LibraryNotFound(e1.what()); + } + + return materialList; +} + +std::shared_ptr>> +ExternalManager::libraryMaterials(const QString& libraryName, + const std::shared_ptr& filter, + const MaterialFilterOptions& options) +{ + auto materialList = std::make_shared>>(); + + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("libraryMaterials")) { + Py::Callable libraries(_managerObject.getAttr("libraryMaterials")); + Py::Tuple args(3); + args.setItem(0, Py::String(libraryName.toStdString())); + if (filter) { + args.setItem(1, + Py::Object(new MaterialFilterPy(new MaterialFilter(*filter)), true)); + } + else { + args.setItem(1, Py::None()); + } + args.setItem( + 2, + Py::Object(new MaterialFilterOptionsPy(new MaterialFilterOptions(options)), true)); + Py::List list(libraries.apply(args)); + for (auto library : list) { + auto entry = Py::Object(library); + if (!checkMaterialObjectType(entry)) { + throw InvalidMaterial(); + } + + materialList->push_back(materialObjectTypeFromObject(entry)); + } + } + else { + Base::Console().Log("\tlibraryMaterials() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw LibraryNotFound(e1.what()); + } + + return materialList; +} + +//===== +// +// Model management +// +//===== + +std::shared_ptr ExternalManager::getModel(const QString& uuid) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("getModel")) { + Py::Callable libraries(_managerObject.getAttr("getModel")); + Py::Tuple args(1); + args.setItem(0, Py::String(uuid.toStdString())); + Py::Tuple result(libraries.apply(args)); // ignore return for now + + Py::Object uuidObject = result.getItem(0); + Py::Tuple libraryObject(result.getItem(1)); + Py::Object modelObject = result.getItem(2); + + Py::Object pyName = libraryObject.getItem(0); + Py::Object pyIcon = libraryObject.getItem(1); + Py::Object readOnly = libraryObject.getItem(2); + + QString name; + if (!pyName.isNone()) { + name = QString::fromStdString(pyName.as_string()); + } + QString icon; + if (!pyIcon.isNone()) { + icon = QString::fromStdString(pyIcon.as_string()); + } + auto library = + std::make_shared(name, QString(), icon, readOnly.as_bool()); + + Model* model = static_cast(*modelObject)->getModelPtr(); + model->setUUID(uuid); + model->setLibrary(library); + auto shared = std::make_shared(*model); + + return shared; + } + else { + Base::Console().Log("\tgetModel() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw ModelNotFound(e1.what()); + } +} + +void ExternalManager::addModel(const QString& libraryName, + const QString& path, + const std::shared_ptr& model) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("addModel")) { + Py::Callable libraries(_managerObject.getAttr("addModel")); + Py::Tuple args(3); + args.setItem(0, Py::String(libraryName.toStdString())); + args.setItem(1, Py::String(path.toStdString())); + args.setItem(2, Py::Object(new ModelPy(new Model(*model)), true)); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\taddModel() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw CreationError(e1.what()); + } +} + +void ExternalManager::migrateModel(const QString& libraryName, + const QString& path, + const std::shared_ptr& model) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("migrateModel")) { + Py::Callable libraries(_managerObject.getAttr("migrateModel")); + Py::Tuple args(3); + args.setItem(0, Py::String(libraryName.toStdString())); + args.setItem(1, Py::String(path.toStdString())); + args.setItem(2, Py::Object(new ModelPy(new Model(*model)), true)); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\tmigrateModel() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw CreationError(e1.what()); + } +} + +//===== +// +// Material management +// +//===== + +std::shared_ptr ExternalManager::getMaterial(const QString& uuid) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("getMaterial")) { + Py::Callable libraries(_managerObject.getAttr("getMaterial")); + Py::Tuple args(1); + args.setItem(0, Py::String(uuid.toStdString())); + Py::Tuple result(libraries.apply(args)); + + Py::Object uuidObject = result.getItem(0); + Py::Tuple libraryObject(result.getItem(1)); + Py::Object materialObject = result.getItem(2); + + Py::Object pyName = libraryObject.getItem(0); + Py::Object pyIcon = libraryObject.getItem(1); + Py::Object readOnly = libraryObject.getItem(2); + + QString name; + if (!pyName.isNone()) { + name = QString::fromStdString(pyName.as_string()); + } + QString icon; + if (!pyIcon.isNone()) { + icon = QString::fromStdString(pyIcon.as_string()); + } + auto library = + std::make_shared(name, QString(), icon, readOnly.as_bool()); + + Material* material = static_cast(*materialObject)->getMaterialPtr(); + material->setUUID(uuid); + material->setLibrary(library); + auto shared = std::make_shared(*material); + + return shared; + } + else { + Base::Console().Log("\tgetMaterial() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw MaterialNotFound(e1.what()); + } +} + +void ExternalManager::addMaterial(const QString& libraryName, + const QString& path, + const std::shared_ptr& material) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("addMaterial")) { + Py::Callable libraries(_managerObject.getAttr("addMaterial")); + Py::Tuple args(3); + args.setItem(0, Py::String(libraryName.toStdString())); + args.setItem(1, Py::String(path.toStdString())); + args.setItem(2, Py::Object(new MaterialPy(new Material(*material)), true)); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\taddMaterial() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw CreationError(e1.what()); + } +} + +void ExternalManager::migrateMaterial(const QString& libraryName, + const QString& path, + const std::shared_ptr& material) +{ + connect(); + + Base::PyGILStateLocker lock; + try { + if (_managerObject.hasAttr("migrateMaterial")) { + Py::Callable libraries(_managerObject.getAttr("migrateMaterial")); + Py::Tuple args(3); + args.setItem(0, Py::String(libraryName.toStdString())); + args.setItem(1, Py::String(path.toStdString())); + auto mat = new Material(*material); + args.setItem(2, Py::Object(new MaterialPy(mat), true)); + libraries.apply(args); // No return expected + } + else { + Base::Console().Log("\tmigrateMaterial() not found\n"); + throw ConnectionError(); + } + } + catch (Py::Exception& e) { + Base::PyException e1; // extract the Python error text + throw CreationError(e1.what()); + } +} diff --git a/src/Mod/Material/App/ExternalManager.h b/src/Mod/Material/App/ExternalManager.h new file mode 100644 index 0000000000..91f92b49ca --- /dev/null +++ b/src/Mod/Material/App/ExternalManager.h @@ -0,0 +1,112 @@ +/*************************************************************************** + * Copyright (c) 2024 David Carter * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#ifndef MATERIAL_EXTERNALMANAGER_H +#define MATERIAL_EXTERNALMANAGER_H + +#include +#include + +#include + +class QMutex; +class QString; + +namespace Materials +{ + +class Library; +class Material; +class Model; +class MaterialFilter; +class MaterialFilterOptions; + +class MaterialsExport ExternalManager: public ParameterGrp::ObserverType +{ +public: + + static ExternalManager* getManager(); + + /// Observer message from the ParameterGrp + void OnChange(ParameterGrp::SubjectType& rCaller, ParameterGrp::MessageType Reason) override; + + // Library management + std::shared_ptr>> libraries(); + std::shared_ptr>> modelLibraries(); + std::shared_ptr>> materialLibraries(); + std::shared_ptr getLibrary(const QString& name); + void createLibrary(const QString& libraryName, const QString& icon, bool readOnly = true); + void renameLibrary(const QString& libraryName, const QString& newName); + void changeIcon(const QString& libraryName, const QString& icon); + void removeLibrary(const QString& libraryName); + std::shared_ptr>> + libraryModels(const QString& libraryName); + std::shared_ptr>> + libraryMaterials(const QString& libraryName); + std::shared_ptr>> + libraryMaterials(const QString& libraryName, + const std::shared_ptr& filter, + const MaterialFilterOptions& options); + + // Model management + std::shared_ptr getModel(const QString& uuid); + void + addModel(const QString& libraryName, const QString& path, const std::shared_ptr& model); + void + migrateModel(const QString& libraryName, const QString& path, const std::shared_ptr& model); + + // Material management + std::shared_ptr getMaterial(const QString& uuid); + void addMaterial(const QString& libraryName, + const QString& path, + const std::shared_ptr& material); + void migrateMaterial(const QString& libraryName, + const QString& path, + const std::shared_ptr& material); + +private: + ExternalManager(); + ~ExternalManager() override; + + static void initManager(); + void getConfiguration(); + void instantiate(); + void connect(); + bool checkMaterialLibraryType(const Py::Object& entry); + std::shared_ptr libraryFromObject(const Py::Object& entry); + bool checkMaterialObjectType(const Py::Object& entry); + std::tuple materialObjectTypeFromObject(const Py::Object& entry); + + static ExternalManager* _manager; + static QMutex _mutex; + + // COnfiguration + ParameterGrp::handle _hGrp; + std::string _moduleName; + std::string _className; + bool _instantiated; + + Py::Object _managerObject; +}; + +} // namespace Materials + +#endif // MATERIAL_EXTERNALMANAGER_H \ No newline at end of file diff --git a/src/Mod/Material/App/Library.cpp b/src/Mod/Material/App/Library.cpp index 57bbd50480..7e87310375 100644 --- a/src/Mod/Material/App/Library.cpp +++ b/src/Mod/Material/App/Library.cpp @@ -40,6 +40,16 @@ Library::Library(const QString& libraryName, const QString& icon, bool readOnly) , _readOnly(readOnly) {} +Library::Library(const QString& libraryName, + const QString& icon, + bool readOnly, + const QString& timestamp) + : _name(libraryName) + , _iconPath(icon) + , _readOnly(readOnly) + , _timestamp(timestamp) +{} + Library::Library(const QString& libraryName, const QString& dir, const QString& icon, bool readOnly) : _name(libraryName) , _directory(QDir::cleanPath(dir)) diff --git a/src/Mod/Material/App/Library.h b/src/Mod/Material/App/Library.h index b2249a91a8..5d773218d5 100644 --- a/src/Mod/Material/App/Library.h +++ b/src/Mod/Material/App/Library.h @@ -39,6 +39,10 @@ class MaterialsExport Library: public Base::BaseClass public: Library() = default; Library(const QString& libraryName, const QString& icon, bool readOnly = true); + Library(const QString& libraryName, + const QString& icon, + bool readOnly, + const QString& timestamp); Library(const QString& libraryName, const QString& dir, const QString& icon, @@ -53,7 +57,7 @@ public: { _name = newName; } - bool sameName(const QString& name) + bool isName(const QString& name) { return (_name == name); } @@ -87,6 +91,14 @@ public: { return QDir(_directory).absolutePath(); } + QString getTimestamp() const + { + return _timestamp; + } + void setTimestamp(const QString& timestamp) + { + _timestamp = timestamp; + } bool operator==(const Library& library) const; bool operator!=(const Library& library) const @@ -107,6 +119,7 @@ private: QString _directory; QString _iconPath; bool _readOnly; + QString _timestamp; }; } // namespace Materials diff --git a/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py b/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py new file mode 100644 index 0000000000..152e4b3525 --- /dev/null +++ b/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py @@ -0,0 +1,249 @@ +# *************************************************************************** +# * Copyright (c) 2024 David Carter * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * 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 Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__author__ = "David Carter" +__url__ = "https://www.davesrocketshop.com" + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +import Materials + +@dataclass +class MaterialLibraryType: + name: str + icon: bytes + readOnly: bool + timestamp: str + +@dataclass +class MaterialLibraryObjectType: + UUID: str + path: str + name: str + +class MaterialManagerExternal(ABC): + """Abstract base class for all external material managers + + Any external interface should be derivedfrom this base class.""" + + @classmethod + def APIVersion(cls) -> tuple: + """Returns a tuple of 3 integers describing the API version + + The version returned should be the latest supported version. This method + allows the interface to use older modules.""" + return (1, 0, 0) + + # + # Library methods + # + + @abstractmethod + def libraries(self) -> list[MaterialLibraryType]: + """Returns a list of libraries managed by this interface + + The list contains a series of tuples describing all libraries managed by + this module. Each tuple containes the library name, icon, a boolean to indicate + if it is a read only library, and a timestamp that indicates when it was last + modified.""" + pass + + @abstractmethod + def modelLibraries(self) -> list[MaterialLibraryType]: + """Returns a list of libraries managed by this interface + + The list contains a series of tuples describing all libraries managed by + this module. Each tuple containes the library name, icon, and a boolean to indicate + if it is a read only library, and a timestamp that indicates when it was last + modified. + + This differs from the libraries() function in that it only returns libraries + containing model objects.""" + pass + + @abstractmethod + def materialLibraries(self) -> list[MaterialLibraryType]: + """Returns a list of libraries managed by this interface + + The list contains a series of tuples describing all libraries managed by + this module. Each tuple containes the library name, icon, and a boolean to indicate + if it is a read only library, and a timestamp that indicates when it was last + modified. + + This differs from the libraries() function in that it only returns libraries + containing material objects.""" + pass + + @abstractmethod + def getLibrary(self, name: str) -> tuple: + """Get the library + + Retrieve the library with the given name""" + pass + + @abstractmethod + def createLibrary(self, name: str, icon: bytes, readOnly: bool) -> None: + """Create a new library + + Create a new library with the given name""" + pass + + @abstractmethod + def renameLibrary(self, oldName: str, newName: str) -> None: + """Rename an existing library + + Change the name of an existing library""" + pass + + @abstractmethod + def changeIcon(self, name: str, icon: bytes) -> None: + """Change the library icon + + Change the library icon""" + pass + + @abstractmethod + def removeLibrary(self, library: str) -> None: + """Delete a library and its contents + + Deletes the library and any models or materials it contains""" + pass + + @abstractmethod + def libraryModels(self, library: str) -> list[MaterialLibraryObjectType]: + """Returns a list of models managed by this library + + Each list entry is a tuple containing the UUID, path, and name of the model""" + pass + + @abstractmethod + def libraryMaterials(self, library: str, + filter: Materials.MaterialFilter = None, + options: Materials.MaterialFilterOptions = None) -> list[MaterialLibraryObjectType]: + """Returns a list of materials managed by this library + + Each list entry is a tuple containing the UUID, path, and name of the material""" + pass + + # + # Model methods + # + + @abstractmethod + def getModel(self, uuid: str) -> Materials.Model: + """Retrieve a model given its UUID""" + pass + + @abstractmethod + def addModel(self, library: str, path: str, model: Materials.Model) -> None: + """Add a model to a library in the given folder. + + This will throw a DatabaseModelExistsError exception if the model already exists.""" + pass + + @abstractmethod + def migrateModel(self, library: str, path: str, model: Materials.Model) -> None: + """Add the model to the library. + + If the model already exists, then no action is performed.""" + pass + + @abstractmethod + def updateModel(self, library: str, path: str, model: Materials.Model) -> None: + """Update the given model""" + pass + + @abstractmethod + def setModelPath(self, library: str, path: str, model: Materials.Model) -> None: + """Change the model path within the library""" + pass + + @abstractmethod + def renameModel(self, library: str, name: str, model: Materials.Model) -> None: + """Change the model name""" + pass + + @abstractmethod + def moveModel(self, library: str, path: str, model: Materials.Model) -> None: + """Move a model across libraries + + Move the model to the desired path in a different library. This should also + remove the model from the old library if that library is managed by this + interface""" + pass + + @abstractmethod + def removeModel(self, model: Materials.Model) -> None: + """Remove the model from the library""" + pass + + # + # Material methods + # + + @abstractmethod + def getMaterial(self, uuid: str) -> Materials.Material: + """ Retrieve a material given its UUID """ + pass + + @abstractmethod + def addMaterial(self, library: str, path: str, material: Materials.Material) -> None: + """Add a material to a library in the given folder. + + This will throw a DatabaseMaterialExistsError exception if the model already exists.""" + pass + + @abstractmethod + def migrateMaterial(self, library: str, path: str, material: Materials.Material) -> None: + """Add the material to the library in the given folder. + + If the material already exists, then no action is performed.""" + pass + + @abstractmethod + def updateMaterial(self, library: str, path: str, material: Materials.Material) -> None: + """Update the given material""" + pass + + @abstractmethod + def setMaterialPath(self, library: str, path: str, material: Materials.Material) -> None: + """Change the material path within the library""" + pass + + @abstractmethod + def renameMaterial(self, library: str, name: str, material: Materials.Material) -> None: + """Change the material name""" + pass + + @abstractmethod + def moveMaterial(self, library: str, path: str, material: Materials.Material) -> None: + """Move a material across libraries + + Move the material to the desired path in a different library. This should also + remove the material from the old library if that library is managed by this + interface""" + pass + + @abstractmethod + def removeMaterial(self, material: Materials.Material) -> None: + """Remove the material from the library""" + pass diff --git a/src/Mod/Material/App/MaterialAPI/__init__.py b/src/Mod/Material/App/MaterialAPI/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/Material/App/MaterialManagerLocal.cpp b/src/Mod/Material/App/MaterialManagerLocal.cpp index 42b3daabe1..7db46b2bad 100644 --- a/src/Mod/Material/App/MaterialManagerLocal.cpp +++ b/src/Mod/Material/App/MaterialManagerLocal.cpp @@ -130,7 +130,7 @@ MaterialManagerLocal::getMaterialLibraries() std::shared_ptr MaterialManagerLocal::getLibrary(const QString& name) const { for (auto& library : *_libraryList) { - if (library->isLocal() && library->sameName(name)) { + if (library->isLocal() && library->isName(name)) { return library; } } @@ -160,7 +160,7 @@ void MaterialManagerLocal::createLibrary(const QString& libraryName, void MaterialManagerLocal::renameLibrary(const QString& libraryName, const QString& newName) { for (auto& library : *_libraryList) { - if (library->isLocal() && library->sameName(libraryName)) { + if (library->isLocal() && library->isName(libraryName)) { auto materialLibrary = reinterpret_cast&>(library); materialLibrary->setName(newName); @@ -174,7 +174,7 @@ void MaterialManagerLocal::renameLibrary(const QString& libraryName, const QStri void MaterialManagerLocal::changeIcon(const QString& libraryName, const QString& icon) { for (auto& library : *_libraryList) { - if (library->isLocal() && library->sameName(libraryName)) { + if (library->isLocal() && library->isName(libraryName)) { auto materialLibrary = reinterpret_cast&>(library); materialLibrary->setIconPath(icon); @@ -188,7 +188,7 @@ void MaterialManagerLocal::changeIcon(const QString& libraryName, const QString& void MaterialManagerLocal::removeLibrary(const QString& libraryName) { for (auto& library : *_libraryList) { - if (library->isLocal() && library->sameName(libraryName)) { + if (library->isLocal() && library->isName(libraryName)) { _libraryList->remove(library); // At this point we should rebuild the material map @@ -207,7 +207,7 @@ MaterialManagerLocal::libraryMaterials(const QString& libraryName) for (auto& it : *_materialMap) { // This is needed to resolve cyclic dependencies auto library = it.second->getLibrary(); - if (library->sameName(libraryName)) { + if (library->isName(libraryName)) { materials->push_back(std::tuple(it.first, it.second->getDirectory(), it.second->getName())); @@ -245,7 +245,7 @@ MaterialManagerLocal::libraryMaterials(const QString& libraryName, for (auto& it : *_materialMap) { // This is needed to resolve cyclic dependencies auto library = it.second->getLibrary(); - if (library->sameName(libraryName)) { + if (library->isName(libraryName)) { if (passFilter(it.second, filter, options)) { materials->push_back(std::tuple(it.first, it.second->getDirectory(),