From 81b7b0c4571ad8c25aa5583d2cd576532d5f4399 Mon Sep 17 00:00:00 2001 From: David Carter Date: Mon, 31 Mar 2025 22:46:12 -0400 Subject: [PATCH 1/2] Material: Interface with an external module The ExternalManager class calls python functions in an external module to create, read, update, and delete material definitions. The API provided by the modules must conform to that defined in the MaterialManagerExternal.py file. All communications with the external module is routed through this class. --- src/Mod/Material/App/CMakeLists.txt | 26 + src/Mod/Material/App/ExternalManager.cpp | 788 ++++++++++++++++++ src/Mod/Material/App/ExternalManager.h | 108 +++ src/Mod/Material/App/Library.cpp | 10 + src/Mod/Material/App/Library.h | 15 +- .../MaterialAPI/MaterialManagerExternal.py | 246 ++++++ src/Mod/Material/App/MaterialAPI/__init__.py | 0 src/Mod/Material/App/MaterialManagerLocal.cpp | 12 +- 8 files changed, 1198 insertions(+), 7 deletions(-) create mode 100644 src/Mod/Material/App/ExternalManager.cpp create mode 100644 src/Mod/Material/App/ExternalManager.h create mode 100644 src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py create mode 100644 src/Mod/Material/App/MaterialAPI/__init__.py 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..249e049234 --- /dev/null +++ b/src/Mod/Material/App/ExternalManager.cpp @@ -0,0 +1,788 @@ +/*************************************************************************** + * 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) +{ + const ParameterGrp& rGrp = static_cast(rCaller); + 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 +// +//===== + +std::shared_ptr +ExternalManager::libraryFromTuple(const Py::Tuple& entry) +{ + auto pyName = entry.getItem(0); + QString libraryName; + if (!pyName.isNone()) { + libraryName = QString::fromStdString(pyName.as_string()); + } + auto pyIcon = entry.getItem(1); + QString icon; + if (!pyIcon.isNone()) { + icon = QString::fromStdString(pyIcon.as_string()); + } + auto pyReadOnly = entry.getItem(2); + bool readOnly = pyReadOnly.as_bool(); + auto pyTimestamp = entry.getItem(3); + QString timestamp; + if (!pyTimestamp.isNone()) { + timestamp = QString::fromStdString(pyTimestamp.as_string()); + } + + Base::Console().Log("Library name '%s', Icon '%s', readOnly %s, timestamp '%s'\n", + libraryName.toStdString().c_str(), + icon.toStdString().c_str(), + readOnly ? "true" : "false", + timestamp.toStdString().c_str()); + auto library = std::make_shared(libraryName, icon, readOnly, timestamp); + return library; +} + +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 = libraryFromTuple(Py::Tuple(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 = libraryFromTuple(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 = libraryFromTuple(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 = libraryFromTuple(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::Tuple(library); + + auto pyUUID = entry.getItem(0); + QString uuid; + if (!pyUUID.isNone()) { + uuid = QString::fromStdString(pyUUID.as_string()); + } + auto pyPath = entry.getItem(1); + QString path; + if (!pyPath.isNone()) { + path = QString::fromStdString(pyPath.as_string()); + } + auto pyName = entry.getItem(2); + QString name; + if (!pyName.isNone()) { + name = QString::fromStdString(pyName.as_string()); + } + + modelList->push_back(std::tuple(uuid, path, name)); + } + } + 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::Tuple(library); + + auto pyUUID = entry.getItem(0); + QString uuid; + if (!pyUUID.isNone()) { + uuid = QString::fromStdString(pyUUID.as_string()); + } + auto pyPath = entry.getItem(1); + QString path; + if (!pyPath.isNone()) { + path = QString::fromStdString(pyPath.as_string()); + } + auto pyName = entry.getItem(2); + QString name; + if (!pyName.isNone()) { + name = QString::fromStdString(pyName.as_string()); + } + + materialList->push_back(std::tuple(uuid, path, name)); + } + } + 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::Tuple(library); + + auto pyUUID = entry.getItem(0); + QString uuid; + if (!pyUUID.isNone()) { + uuid = QString::fromStdString(pyUUID.as_string()); + } + auto pyPath = entry.getItem(1); + QString path; + if (!pyPath.isNone()) { + path = QString::fromStdString(pyPath.as_string()); + } + auto pyName = entry.getItem(2); + QString name; + if (!pyName.isNone()) { + name = QString::fromStdString(pyName.as_string()); + } + + materialList->push_back(std::tuple(uuid, path, name)); + } + } + 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..56c4ae2a62 --- /dev/null +++ b/src/Mod/Material/App/ExternalManager.h @@ -0,0 +1,108 @@ +/*************************************************************************** + * 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; + +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(); + + static void initManager(); + void getConfiguration(); + void instantiate(); + void connect(); + std::shared_ptr libraryFromTuple(const Py::Tuple& 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..2f9db88722 --- /dev/null +++ b/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py @@ -0,0 +1,246 @@ +# *************************************************************************** +# * 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 typing import Tuple + +import Materials + +# This requires Python 3.12 or later. This isn't available on all platforms yet +# type MaterialName = str +# type MaterialIcon = str +# type MaterialReadOnly = bool +# type MaterialTimestamp = str +# type MaterialUUID = str +# type MaterialPath = str +# type MaterialLibraryType = Tuple[MaterialName, MaterialIcon, MaterialReadOnly, MaterialTimestamp] +# type MaterialLibraryObjectType = Tuple[MaterialUUID, MaterialPath, MaterialName] + +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: str, 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: str) -> 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(), From 5bbb07e24dbeece6f3962345644f8cbe6afd72e5 Mon Sep 17 00:00:00 2001 From: David Carter Date: Thu, 10 Apr 2025 06:13:38 -0400 Subject: [PATCH 2/2] Materials: Use data classes in interface specification --- src/Mod/Material/App/ExternalManager.cpp | 134 +++++++++--------- src/Mod/Material/App/ExternalManager.h | 8 +- .../MaterialAPI/MaterialManagerExternal.py | 37 ++--- 3 files changed, 92 insertions(+), 87 deletions(-) diff --git a/src/Mod/Material/App/ExternalManager.cpp b/src/Mod/Material/App/ExternalManager.cpp index 249e049234..22d3920e11 100644 --- a/src/Mod/Material/App/ExternalManager.cpp +++ b/src/Mod/Material/App/ExternalManager.cpp @@ -65,9 +65,8 @@ ExternalManager::~ExternalManager() _hGrp->Detach(this); } -void ExternalManager::OnChange(ParameterGrp::SubjectType& rCaller, ParameterGrp::MessageType Reason) +void ExternalManager::OnChange(ParameterGrp::SubjectType& /*rCaller*/, ParameterGrp::MessageType Reason) { - const ParameterGrp& rGrp = static_cast(rCaller); if (std::strncmp(Reason, "Current", 7) == 0) { if (_instantiated) { // The old manager object will be deleted when reconnecting @@ -167,36 +166,74 @@ ExternalManager* ExternalManager::getManager() // //===== -std::shared_ptr -ExternalManager::libraryFromTuple(const Py::Tuple& entry) +bool ExternalManager::checkMaterialLibraryType(const Py::Object& entry) { - auto pyName = entry.getItem(0); + 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()); } - auto pyIcon = entry.getItem(1); + QString icon; if (!pyIcon.isNone()) { icon = QString::fromStdString(pyIcon.as_string()); } - auto pyReadOnly = entry.getItem(2); + bool readOnly = pyReadOnly.as_bool(); - auto pyTimestamp = entry.getItem(3); + QString timestamp; if (!pyTimestamp.isNone()) { timestamp = QString::fromStdString(pyTimestamp.as_string()); } - Base::Console().Log("Library name '%s', Icon '%s', readOnly %s, timestamp '%s'\n", - libraryName.toStdString().c_str(), - icon.toStdString().c_str(), - readOnly ? "true" : "false", - timestamp.toStdString().c_str()); 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() { @@ -210,7 +247,7 @@ ExternalManager::libraries() Py::Callable libraries(_managerObject.getAttr("libraries")); Py::List list(libraries.apply()); for (auto lib : list) { - auto library = libraryFromTuple(Py::Tuple(lib)); + auto library = libraryFromObject(Py::Object(lib)); libList->push_back(library); } } @@ -239,7 +276,7 @@ std::shared_ptr>> ExternalManager::modelLib Py::Callable libraries(_managerObject.getAttr("modelLibraries")); Py::List list(libraries.apply()); for (auto lib : list) { - auto library = libraryFromTuple(Py::Tuple(lib)); + auto library = libraryFromObject(Py::Tuple(lib)); libList->push_back(library); } } @@ -268,7 +305,7 @@ std::shared_ptr>> ExternalManager::material Py::Callable libraries(_managerObject.getAttr("materialLibraries")); Py::List list(libraries.apply()); for (auto lib : list) { - auto library = libraryFromTuple(Py::Tuple(lib)); + auto library = libraryFromObject(Py::Tuple(lib)); libList->push_back(library); } } @@ -299,7 +336,7 @@ std::shared_ptr ExternalManager::getLibrary(const QString& name) Py::Tuple result(libraries.apply(args)); Py::Object libObject = result.getItem(0); - auto lib = libraryFromTuple(Py::Tuple(libObject)); + auto lib = libraryFromObject(Py::Tuple(libObject)); return std::make_shared(*lib); } else { @@ -424,25 +461,12 @@ ExternalManager::libraryModels(const QString& libraryName) args.setItem(0, Py::String(libraryName.toStdString())); Py::List list(libraries.apply(args)); for (auto library : list) { - auto entry = Py::Tuple(library); - - auto pyUUID = entry.getItem(0); - QString uuid; - if (!pyUUID.isNone()) { - uuid = QString::fromStdString(pyUUID.as_string()); - } - auto pyPath = entry.getItem(1); - QString path; - if (!pyPath.isNone()) { - path = QString::fromStdString(pyPath.as_string()); - } - auto pyName = entry.getItem(2); - QString name; - if (!pyName.isNone()) { - name = QString::fromStdString(pyName.as_string()); + auto entry = Py::Object(library); + if (!checkMaterialObjectType(entry)) { + throw InvalidModel(); } - modelList->push_back(std::tuple(uuid, path, name)); + modelList->push_back(materialObjectTypeFromObject(entry)); } } else { @@ -473,25 +497,12 @@ ExternalManager::libraryMaterials(const QString& libraryName) args.setItem(0, Py::String(libraryName.toStdString())); Py::List list(libraries.apply(args)); for (auto library : list) { - auto entry = Py::Tuple(library); - - auto pyUUID = entry.getItem(0); - QString uuid; - if (!pyUUID.isNone()) { - uuid = QString::fromStdString(pyUUID.as_string()); - } - auto pyPath = entry.getItem(1); - QString path; - if (!pyPath.isNone()) { - path = QString::fromStdString(pyPath.as_string()); - } - auto pyName = entry.getItem(2); - QString name; - if (!pyName.isNone()) { - name = QString::fromStdString(pyName.as_string()); + auto entry = Py::Object(library); + if (!checkMaterialObjectType(entry)) { + throw InvalidMaterial(); } - materialList->push_back(std::tuple(uuid, path, name)); + materialList->push_back(materialObjectTypeFromObject(entry)); } } else { @@ -534,25 +545,12 @@ ExternalManager::libraryMaterials(const QString& libraryName, Py::Object(new MaterialFilterOptionsPy(new MaterialFilterOptions(options)), true)); Py::List list(libraries.apply(args)); for (auto library : list) { - auto entry = Py::Tuple(library); - - auto pyUUID = entry.getItem(0); - QString uuid; - if (!pyUUID.isNone()) { - uuid = QString::fromStdString(pyUUID.as_string()); - } - auto pyPath = entry.getItem(1); - QString path; - if (!pyPath.isNone()) { - path = QString::fromStdString(pyPath.as_string()); - } - auto pyName = entry.getItem(2); - QString name; - if (!pyName.isNone()) { - name = QString::fromStdString(pyName.as_string()); + auto entry = Py::Object(library); + if (!checkMaterialObjectType(entry)) { + throw InvalidMaterial(); } - materialList->push_back(std::tuple(uuid, path, name)); + materialList->push_back(materialObjectTypeFromObject(entry)); } } else { diff --git a/src/Mod/Material/App/ExternalManager.h b/src/Mod/Material/App/ExternalManager.h index 56c4ae2a62..91f92b49ca 100644 --- a/src/Mod/Material/App/ExternalManager.h +++ b/src/Mod/Material/App/ExternalManager.h @@ -28,6 +28,7 @@ #include class QMutex; +class QString; namespace Materials { @@ -83,13 +84,16 @@ public: private: ExternalManager(); - ~ExternalManager(); + ~ExternalManager() override; static void initManager(); void getConfiguration(); void instantiate(); void connect(); - std::shared_ptr libraryFromTuple(const Py::Tuple& entry); + 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; diff --git a/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py b/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py index 2f9db88722..152e4b3525 100644 --- a/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py +++ b/src/Mod/Material/App/MaterialAPI/MaterialManagerExternal.py @@ -23,19 +23,22 @@ __author__ = "David Carter" __url__ = "https://www.davesrocketshop.com" from abc import ABC, abstractmethod -# from typing import Tuple +from dataclasses import dataclass import Materials -# This requires Python 3.12 or later. This isn't available on all platforms yet -# type MaterialName = str -# type MaterialIcon = str -# type MaterialReadOnly = bool -# type MaterialTimestamp = str -# type MaterialUUID = str -# type MaterialPath = str -# type MaterialLibraryType = Tuple[MaterialName, MaterialIcon, MaterialReadOnly, MaterialTimestamp] -# type MaterialLibraryObjectType = Tuple[MaterialUUID, MaterialPath, MaterialName] +@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 @@ -55,7 +58,7 @@ class MaterialManagerExternal(ABC): # @abstractmethod - def libraries(self) -> list: #[MaterialLibraryType]: + 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 @@ -65,7 +68,7 @@ class MaterialManagerExternal(ABC): pass @abstractmethod - def modelLibraries(self) -> list: #[MaterialLibraryType]: + 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 @@ -78,7 +81,7 @@ class MaterialManagerExternal(ABC): pass @abstractmethod - def materialLibraries(self) -> list: #[MaterialLibraryType]: + 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 @@ -98,7 +101,7 @@ class MaterialManagerExternal(ABC): pass @abstractmethod - def createLibrary(self, name: str, icon: str, readOnly: bool) -> None: + def createLibrary(self, name: str, icon: bytes, readOnly: bool) -> None: """Create a new library Create a new library with the given name""" @@ -112,7 +115,7 @@ class MaterialManagerExternal(ABC): pass @abstractmethod - def changeIcon(self, name: str, icon: str) -> None: + def changeIcon(self, name: str, icon: bytes) -> None: """Change the library icon Change the library icon""" @@ -126,7 +129,7 @@ class MaterialManagerExternal(ABC): pass @abstractmethod - def libraryModels(self, library: str) -> list: #[MaterialLibraryObjectType]: + 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""" @@ -135,7 +138,7 @@ class MaterialManagerExternal(ABC): @abstractmethod def libraryMaterials(self, library: str, filter: Materials.MaterialFilter = None, - options: Materials.MaterialFilterOptions = None) -> list: #[MaterialLibraryObjectType]: + 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"""