diff --git a/lgtm.yml b/lgtm.yml index 3ba78f4bb2..6481c55a72 100644 --- a/lgtm.yml +++ b/lgtm.yml @@ -5,8 +5,70 @@ path_classifiers: - "src/Mod/Import/App/config_control_design.py" - "src/Mod/Import/App/ifc2x3.py" - "src/Mod/Import/App/ifc4.py" + library: + - "src/zipios++/" + - "src/3rdParty/" + - "src/Mod/Import/App/SCL" + template: + - "src/Tools/examplePy2wiki.py" + unmaintained: + - "src/Mod/Robot/" + - "src/Mod/Ship/" + legacy: + - "src/Mod/Assembly/" + - "src/Mod/Drawing/" + extraction: javascript: index: filters: exclude: "**/translations/*.ts" + cpp: + prepare: + packages: + - "cmake" + - "cmake-gui" + - "libboost-date-time-dev" + - "libboost-dev" + - "libboost-filesystem-dev" + - "libboost-graph-dev" + - "libboost-iostreams-dev" + - "libboost-program-options-dev" + - "libboost-python-dev" + - "libboost-regex-dev" + - "libboost-serialization-dev" + - "libboost-thread-dev" + - "libcoin-dev" + - "libeigen3-dev" + - "libgts-bin" + - "libgts-dev" + - "libkdtree++-dev" + - "libmedc-dev" + - "libocct-data-exchange-dev" + - "libocct-ocaf-dev" + - "libocct-visualization-dev" + - "libopencv-dev" + - "libproj-dev" + - "libpyside2-dev" + - "libshiboken2-dev" + - "libspnav-dev" + - "libvtk7-dev" + - "libx11-dev" + - "libxerces-c-dev" + - "libzipios++-dev" + - "occt-draw" + - "pyside2-tools" + - "python3-dev" + - "python3-matplotlib" + - "python3-pivy" + - "python3-ply" + - "python3-pyside2.qtcore" + - "python3-pyside2uic" + - "qtbase5-dev" + - "qttools5-dev" + - "swig" + configure: + command: "cmake ./ -DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_GUI=OFF -DBUILD_ARCH=OFF\ + \ -DBUILD_DRAWING=OFF -DBUILD_IMAGE=OFF -DBUILD_INSPECTION=OFF -DBUILD_OPENSCAD=OFF\ + \ -DBUILD_RAYTRACING=OFF -DBUILD_REVERSEENGINEERING=OFF -DBUILD_SURFACE=OFF -DBUILD_START=OFF\ + \ -DBUILD_ROBOT=OFF -DBUILD_PATH=OFF -DBUILD_FEM=OFF" \ No newline at end of file diff --git a/src/App/Extension.h b/src/App/Extension.h index cf60e0094b..4f2da76a06 100644 --- a/src/App/Extension.h +++ b/src/App/Extension.h @@ -336,13 +336,9 @@ public: ExtensionPythonT() { ExtensionT::m_isPythonExtension = true; ExtensionT::initExtensionType(ExtensionPythonT::getExtensionClassTypeId()); - - EXTENSION_ADD_PROPERTY(ExtensionProxy,(Py::Object())); } virtual ~ExtensionPythonT() { } - - PropertyPythonObject ExtensionProxy; }; typedef ExtensionPythonT ExtensionPython; @@ -352,7 +348,7 @@ typedef ExtensionPythonT ExtensionPython; Base::PyGILStateLocker lock;\ Py::Object result;\ try {\ - Property* proxy = this->extensionGetPropertyByName("ExtensionProxy");\ + Property* proxy = this->getExtendedContainer()->getPropertyByName("Proxy");\ if (proxy && proxy->getTypeId() == PropertyPythonObject::getClassTypeId()) {\ Py::Object feature = static_cast(proxy)->getValue();\ if (feature.hasAttr(std::string("function"))) {\ diff --git a/src/App/ExtensionContainerPy.xml b/src/App/ExtensionContainerPy.xml index 79a9e105d8..2497c3dda1 100644 --- a/src/App/ExtensionContainerPy.xml +++ b/src/App/ExtensionContainerPy.xml @@ -17,8 +17,7 @@ - Adds an extension to the object. Requires the string identifier as well as the python object - used to check for overridden functions (most likely self) + Adds an extension to the object. Requires the string identifier for the python extension as argument diff --git a/src/App/ExtensionContainerPyImp.cpp b/src/App/ExtensionContainerPyImp.cpp index 75a6f4d148..a7f6705887 100644 --- a/src/App/ExtensionContainerPyImp.cpp +++ b/src/App/ExtensionContainerPyImp.cpp @@ -198,10 +198,16 @@ PyObject* ExtensionContainerPy::hasExtension(PyObject *args) { PyObject* ExtensionContainerPy::addExtension(PyObject *args) { char *typeId; - PyObject* proxy; - if (!PyArg_ParseTuple(args, "sO", &typeId, &proxy)) + PyObject* proxy = nullptr; + if (!PyArg_ParseTuple(args, "s|O", &typeId, &proxy)) return NULL; + if (proxy) { + PyErr_SetString(PyExc_DeprecationWarning, "Second argument is deprecated. It is ignored and will be removed in future versions. " + "The default Python feature proxy is used for extension method overrides."); + PyErr_Print(); + } + //get the extension type asked for Base::Type extension = Base::Type::fromName(typeId); if (extension.isBad() || !extension.isDerivedFrom(App::Extension::getExtensionClassTypeId())) { @@ -223,16 +229,7 @@ PyObject* ExtensionContainerPy::addExtension(PyObject *args) { GetApplication().signalBeforeAddingDynamicExtension(*getExtensionContainerPtr(), typeId); ext->initExtension(getExtensionContainerPtr()); - //set the proxy to allow python overrides - App::Property* pp = ext->extensionGetPropertyByName("ExtensionProxy"); - if (!pp) { - std::stringstream str; - str << "Accessing the proxy property failed!" << std::ends; - throw Py::Exception(Base::BaseExceptionFreeCADError,str.str()); - } - static_cast(pp)->setPyObject(proxy); - - // The PyTypeObject is shared by all instances of this type and therefore + // The PyTypeObject is shared by all instances of this type and therefore // we have to add new methods only once. PyObject* obj = ext->getExtensionPyObject(); PyMethodDef* meth = reinterpret_cast(obj->ob_type->tp_methods); diff --git a/src/Base/core-base.dox b/src/Base/core-base.dox index 05a78de886..eb38a35ceb 100644 --- a/src/Base/core-base.dox +++ b/src/Base/core-base.dox @@ -23,5 +23,5 @@ /** \defgroup GeomPrimers Geometry primers * \ingroup BASE - \brief Basic structures used by geomoetric objects + \brief Basic structures used by geometric objects */ diff --git a/src/Gui/3Dconnexion/GuiNativeEventWin32.cpp b/src/Gui/3Dconnexion/GuiNativeEventWin32.cpp index ec519955fb..24726546be 100644 --- a/src/Gui/3Dconnexion/GuiNativeEventWin32.cpp +++ b/src/Gui/3Dconnexion/GuiNativeEventWin32.cpp @@ -71,8 +71,8 @@ Gui::GuiNativeEvent* Gui::GuiNativeEvent::gMouseInput = 0; #define _TRACE_RI_TYPE 0 #define _TRACE_RIDI_DEVICENAME 0 #define _TRACE_RIDI_DEVICEINFO 0 -#define _TRACE_RI_RAWDATA 1 -#define _TRACE_3DINPUT_PERIOD 1 +#define _TRACE_RI_RAWDATA 0 +#define _TRACE_3DINPUT_PERIOD 0 #ifdef _WIN64 typedef unsigned __int64 QWORD; diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp index f759c4cd04..b167bf0a6e 100644 --- a/src/Gui/Action.cpp +++ b/src/Gui/Action.cpp @@ -694,11 +694,42 @@ void WorkbenchGroup::slotRemoveWorkbench(const char* name) // -------------------------------------------------------------------- +class RecentFilesAction::Private: public ParameterGrp::ObserverType +{ +public: + Private(RecentFilesAction *master, const char *path):master(master) + { + handle = App::GetApplication().GetParameterGroupByPath(path); + handle->Attach(this); + } + + virtual ~Private() + { + handle->Detach(this); + } + + void OnChange(Base::Subject &, const char *reason) + { + if (!updating && reason && strcmp(reason, "RecentFiles")==0) { + Base::StateLocker guard(updating); + master->restore(); + } + } + +public: + RecentFilesAction *master; + ParameterGrp::handle handle; + bool updating = false; +}; + +// -------------------------------------------------------------------- + /* TRANSLATOR Gui::RecentFilesAction */ RecentFilesAction::RecentFilesAction ( Command* pcCmd, QObject * parent ) : ActionGroup( pcCmd, parent ), visibleItems(4), maximumItems(20) { + _pimpl.reset(new Private(this, "User parameter:BaseApp/Preferences/RecentFiles")); restore(); } @@ -808,13 +839,10 @@ void RecentFilesAction::resizeList(int size) /** Loads all recent files from the preferences. */ void RecentFilesAction::restore() { - ParameterGrp::handle hGrp = App::GetApplication().GetUserParameter().GetGroup("BaseApp")->GetGroup("Preferences"); - if (hGrp->HasGroup("RecentFiles")) { - hGrp = hGrp->GetGroup("RecentFiles"); - // we want at least 20 items but we do only show the number of files - // that is defined in user parameters - this->visibleItems = hGrp->GetInt("RecentFiles", this->visibleItems); - } + ParameterGrp::handle hGrp = _pimpl->handle; + // we want at least 20 items but we do only show the number of files + // that is defined in user parameters + this->visibleItems = hGrp->GetInt("RecentFiles", this->visibleItems); int count = std::max(this->maximumItems, this->visibleItems); for (int i=0; iGetGroup("Preferences")->GetGroup("RecentFiles"); + ParameterGrp::handle hGrp = _pimpl->handle; int count = hGrp->GetInt("RecentFiles", this->visibleItems); // save number of files hGrp->Clear(); @@ -845,6 +872,7 @@ void RecentFilesAction::save() hGrp->SetASCII(key.toLatin1(), value.toUtf8()); } + Base::StateLocker guard(_pimpl->updating); hGrp->SetInt("RecentFiles", count); // restore } @@ -1004,7 +1032,7 @@ void RecentMacrosAction::restore() } int count = std::max(this->maximumItems, this->visibleItems); - for (int i=0; iactions().size(); iaddAction(QLatin1String(""))->setVisible(false); std::vector MRU = hGrp->GetASCIIs("MRU"); QStringList files; diff --git a/src/Gui/Action.h b/src/Gui/Action.h index cd7d0ad3a8..5aa6f2c313 100644 --- a/src/Gui/Action.h +++ b/src/Gui/Action.h @@ -24,6 +24,7 @@ #ifndef GUI_ACTION_H #define GUI_ACTION_H +#include #include #include #include @@ -210,6 +211,10 @@ private: private: int visibleItems; /**< Number of visible items */ int maximumItems; /**< Number of maximum items */ + + class Private; + friend class Private; + std::unique_ptr _pimpl; }; // -------------------------------------------------------------------- diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index e301134f9a..6fd86ecd84 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -717,7 +717,12 @@ void Application::importFrom(const char* FileName, const char* DocName, const ch // the original file name is required QString filename = QString::fromUtf8(File.filePath().c_str()); - getMainWindow()->appendRecentFile(filename); + auto parameterGroup = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General"); + bool addToRecent = parameterGroup->GetBool("RecentIncludesImported", true); + parameterGroup->SetBool("RecentIncludesImported", addToRecent); // Make sure it gets added to the parameter list + if (addToRecent) { + getMainWindow()->appendRecentFile(filename); + } FileDialog::setWorkingDirectory(filename); } catch (const Base::PyException& e){ @@ -768,12 +773,17 @@ void Application::exportTo(const char* FileName, const char* DocName, const char std::string code = str.str(); // the original file name is required Gui::Command::runCommand(Gui::Command::App, code.c_str()); - // search for a module that is able to open the exported file because otherwise - // it doesn't need to be added to the recent files list (#0002047) - std::map importMap = App::GetApplication().getImportFilters(te.c_str()); - if (!importMap.empty()) - getMainWindow()->appendRecentFile(QString::fromUtf8(File.filePath().c_str())); + auto parameterGroup = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General"); + bool addToRecent = parameterGroup->GetBool("RecentIncludesExported", false); + parameterGroup->SetBool("RecentIncludesExported", addToRecent); // Make sure it gets added to the parameter list + if (addToRecent) { + // search for a module that is able to open the exported file because otherwise + // it doesn't need to be added to the recent files list (#0002047) + std::map importMap = App::GetApplication().getImportFilters(te.c_str()); + if (!importMap.empty()) + getMainWindow()->appendRecentFile(QString::fromUtf8(File.filePath().c_str())); + } // allow exporters to pass _objs__ to submodules before deleting it Gui::Command::runCommand(Gui::Command::App, "del __objs__"); } diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 344a249882..3b6bb31bc8 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -317,10 +317,12 @@ set(Gui_MOC_HDRS DlgReportViewImp.h DlgSettings3DViewImp.h DlgSettingsNavigation.h + DlgSettingsSelection.h DlgSettingsViewColor.h DlgSettingsColorGradientImp.h DlgSettingsDocumentImp.h DlgSettingsImageImp.h + DlgSettingsLazyLoadedImp.h DlgSettingsMacroImp.h DlgSettingsUnitsImp.h DlgCheckableMessageBox.h @@ -443,11 +445,13 @@ SET(Gui_UIC_SRCS DlgReportView.ui DlgSettings3DView.ui DlgSettingsNavigation.ui + DlgSettingsSelection.ui DlgSettingsUnits.ui DlgSettingsViewColor.ui DlgSettingsColorGradient.ui DlgSettingsDocument.ui DlgSettingsImage.ui + DlgSettingsLazyLoaded.ui DlgSettingsMacro.ui DlgCheckableMessageBox.ui DlgToolbars.ui @@ -673,11 +677,13 @@ SET(Dialog_Settings_CPP_SRCS DlgReportViewImp.cpp DlgSettings3DViewImp.cpp DlgSettingsNavigation.cpp + DlgSettingsSelection.cpp DlgSettingsUnitsImp.cpp DlgSettingsViewColor.cpp DlgSettingsColorGradientImp.cpp DlgSettingsDocumentImp.cpp DlgSettingsImageImp.cpp + DlgSettingsLazyLoadedImp.cpp DlgSettingsMacroImp.cpp ) SET(Dialog_Settings_HPP_SRCS @@ -688,11 +694,13 @@ SET(Dialog_Settings_HPP_SRCS DlgReportViewImp.h DlgSettings3DViewImp.h DlgSettingsNavigation.h + DlgSettingsSelection.h DlgSettingsUnitsImp.h DlgSettingsViewColor.h DlgSettingsColorGradientImp.h DlgSettingsDocumentImp.h DlgSettingsImageImp.h + DlgSettingsLazyLoadedImp.h DlgSettingsMacroImp.h ) SET(Dialog_Settings_SRCS @@ -705,11 +713,13 @@ SET(Dialog_Settings_SRCS DlgReportView.ui DlgSettings3DView.ui DlgSettingsNavigation.ui + DlgSettingsSelection.ui DlgSettingsUnits.ui DlgSettingsViewColor.ui DlgSettingsColorGradient.ui DlgSettingsDocument.ui DlgSettingsImage.ui + DlgSettingsLazyLoaded.ui DlgSettingsMacro.ui ) SOURCE_GROUP("Dialog\\Settings" FILES ${Dialog_Settings_SRCS}) diff --git a/src/Gui/CommandDoc.cpp b/src/Gui/CommandDoc.cpp index ae83d95d7d..67a3cc8397 100644 --- a/src/Gui/CommandDoc.cpp +++ b/src/Gui/CommandDoc.cpp @@ -25,6 +25,7 @@ #ifndef _PreComp_ # include # include +# include # include # include # include @@ -279,37 +280,205 @@ StdCmdExport::StdCmdExport() eType = 0; } +/** +Create a default filename from a user-specified format string + +Format options are: +%F - the basename of the .FCStd file (or the label, if it is not saved yet) +%Lx - the label of the selected object(s), separated by character 'x' +%Px - the label of the selected object(s) and their first parent, separated by character 'x' +%U - the date and time, in UTC, ISO 8601 +%D - the date and time, in local timezone, ISO 8601 +Any other characters are treated literally, though if the filename is illegal +it will be changed on saving. + +The format string is stored in two user preferences (not currently exposed in the GUI): +* BaseApp/Preferences/General/ExportDefaultFilenameSingle +* BaseApp/Preferences/General/ExportDefaultFilenameMultiple +*/ +QString createDefaultExportBasename() +{ + QString defaultFilename; + + auto selection = Gui::Selection().getObjectsOfType(App::DocumentObject::getClassTypeId()); + QString exportFormatString; + if (selection.size() == 1) { + exportFormatString = QString::fromStdString (App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General")-> + GetASCII("ExportDefaultFilenameSingle", "%F-%P-")); + } + else { + exportFormatString = QString::fromStdString (App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General")-> + GetASCII("ExportDefaultFilenameMultiple", "%F")); + } + + // For code simplicity, pull all values we might need + + // %F - the basename of the.FCStd file(or the label, if it is not saved yet) + QString docFilename = QString::fromUtf8(App::GetApplication().getActiveDocument()->getFileName()); + QFileInfo fi(docFilename); + QString fcstdBasename = fi.completeBaseName(); + if (fcstdBasename.isEmpty()) + fcstdBasename = QString::fromStdString(App::GetApplication().getActiveDocument()->Label.getStrValue()); + + // %L - the label of the selected object(s) + QStringList objectLabels; + for (const auto& object : selection) + objectLabels.push_back(QString::fromStdString(object->Label.getStrValue())); + + // %P - the label of the selected objects and their first parent + QStringList parentLabels; + for (const auto& object : selection) { + auto parents = object->getParents(); + QString firstParent; + if (!parents.empty()) + firstParent = QString::fromStdString(parents.front().first->Label.getStrValue()); + parentLabels.append(firstParent + QString::fromStdString(object->Label.getStrValue())); + } + + // %U - the date and time, in UTC, ISO 8601 + QDateTime utc = QDateTime(QDateTime::currentDateTimeUtc()); + QString utcISO8601 = utc.toString(Qt::ISODate); + + // %D - the date and time, in local timezone, ISO 8601 + QDateTime local = utc.toLocalTime(); + QString localISO8601 = local.toString(Qt::ISODate); + + // Parse the format string one character at a time: + for (int i = 0; i < exportFormatString.size(); ++i) { + auto c = exportFormatString.at(i); + if (c != QLatin1Char('%')) { + // Anything that's not a format start character is just a literal + defaultFilename.append(c); + } + else { + // The format start character now requires us to look at at least the next single + // character (if there isn't another character, the % just gets eaten) + if (i < exportFormatString.size() - 1) { + ++i; + auto formatChar = exportFormatString.at(i); + QChar separatorChar = QLatin1Char('-'); + // If this format type requires an additional char, read that now (or default to + // '-' if the format string ends) + if (formatChar == QLatin1Char('L') || + formatChar == QLatin1Char('P')) { + if (i < exportFormatString.size() - 1) { + ++i; + separatorChar = exportFormatString.at(i); + } + } + + // Handle our format characters: + if (formatChar == QLatin1Char('F')) { + defaultFilename.append(fcstdBasename); + } + else if (formatChar == QLatin1Char('L')) { + defaultFilename.append(objectLabels.join(separatorChar)); + } + else if (formatChar == QLatin1Char('P')) { + defaultFilename.append(parentLabels.join(separatorChar)); + } + else if (formatChar == QLatin1Char('U')) { + defaultFilename.append(utcISO8601); + } + else if (formatChar == QLatin1Char('D')) { + defaultFilename.append(localISO8601); + } + else { + FC_WARN("When parsing default export filename format string, %" + << QString(formatChar).toStdString() + << " is not a known format string."); + } + } + } + } + + // Finally, clean the string so it's valid for all operating systems: + QString invalidCharacters = QLatin1String("/\\?%*:|\"<>"); + for (const auto &c : invalidCharacters) + defaultFilename.replace(c,QLatin1String("_")); + + return defaultFilename; +} + void StdCmdExport::activated(int iMsg) { Q_UNUSED(iMsg); - if (Gui::Selection().countObjectsOfType(App::DocumentObject::getClassTypeId()) == 0) { + static QString lastExportFullPath = QString(); + static bool lastExportUsedGeneratedFilename = true; + static QString lastExportFilterUsed = QString(); + + auto selection = Gui::Selection().getObjectsOfType(App::DocumentObject::getClassTypeId()); + if (selection.size() == 0) { QMessageBox::warning(Gui::getMainWindow(), QString::fromUtf8(QT_TR_NOOP("No selection")), - QString::fromUtf8(QT_TR_NOOP("Please select first the objects you want to export."))); + QString::fromUtf8(QT_TR_NOOP("Select the objects to export before choosing Export."))); return; } - // fill the list of registered endings + // fill the list of registered suffixes QStringList filterList; - std::map FilterList = App::GetApplication().getExportFilters(); - std::map::const_iterator jt; - for (jt=FilterList.begin();jt != FilterList.end();++jt) { + std::map filterMap = App::GetApplication().getExportFilters(); + for (const auto &filter : filterMap) { // ignore the project file format - if (jt->first.find("(*.FCStd)") == std::string::npos) { - filterList << QString::fromLatin1(jt->first.c_str()); + if (filter.first.find("(*.FCStd)") == std::string::npos) + filterList << QString::fromStdString(filter.first); + } + QString formatList = filterList.join(QLatin1String(";;")); + Base::Reference hPath = + App::GetApplication().GetUserParameter().GetGroup("BaseApp")->GetGroup("Preferences")->GetGroup("General"); + QString selectedFilter = QString::fromStdString(hPath->GetASCII("FileExportFilter")); + if (!lastExportFilterUsed.isEmpty()) + selectedFilter = lastExportFilterUsed; + + // Create a default filename for the export + // * If this is the first export this session default, generate a new default. + // * If this is a repeated export during the same session: + // * If the user accepted the default filename last time, regenerate a new + // default, potentially updating the object label. + // * If not, default to their previously-set export filename. + QString defaultFilename = lastExportFullPath; + + bool filenameWasGenerated = false; + // We want to generate a new default name in two cases: + if (defaultFilename.isEmpty() || lastExportUsedGeneratedFilename) { + // First, get the name and path of the current .FCStd file, if there is one: + QString docFilename = QString::fromUtf8( + App::GetApplication().getActiveDocument()->getFileName()); + + // Find the default location for our exported file. Three possibilities: + QString defaultExportPath; + if (!lastExportFullPath.isEmpty()) { + QFileInfo fi(lastExportFullPath); + defaultExportPath = fi.path(); + } + else if (!docFilename.isEmpty()) { + QFileInfo fi(docFilename); + defaultExportPath = fi.path(); + } + else { + defaultExportPath = Gui::FileDialog::getWorkingDirectory(); + } + + if (lastExportUsedGeneratedFilename /*<- static, true on first call*/ ) { + defaultFilename = defaultExportPath + QLatin1Char('/') + createDefaultExportBasename(); + + // Append the last extension used, if there is one. + if (!lastExportFullPath.isEmpty()) { + QFileInfo lastExportFile(lastExportFullPath); + if (!lastExportFile.suffix().isEmpty()) + defaultFilename += QLatin1String(".") + lastExportFile.suffix(); + } + filenameWasGenerated = true; } } - QString formatList = filterList.join(QLatin1String(";;")); - Base::Reference hPath = App::GetApplication().GetUserParameter().GetGroup("BaseApp") - ->GetGroup("Preferences")->GetGroup("General"); - QString selectedFilter = QString::fromStdString(hPath->GetASCII("FileExportFilter")); - + // Launch the file selection modal dialog QString fileName = FileDialog::getSaveFileName(getMainWindow(), - QObject::tr("Export file"), QString(), formatList, &selectedFilter); + QObject::tr("Export file"), defaultFilename, formatList, &selectedFilter); if (!fileName.isEmpty()) { hPath->SetASCII("FileExportFilter", selectedFilter.toLatin1().constData()); + lastExportFilterUsed = selectedFilter; // So we can select the same one next time SelectModule::Dict dict = SelectModule::exportHandler(fileName, selectedFilter); // export the files with the associated modules for (SelectModule::Dict::iterator it = dict.begin(); it != dict.end(); ++it) { @@ -317,6 +486,18 @@ void StdCmdExport::activated(int iMsg) getActiveGuiDocument()->getDocument()->getName(), it.value().toLatin1()); } + + // Keep a record of if the user used our suggested generated filename. If they + // did, next time we can recreate it, which will update the object label if + // there is one. + QFileInfo defaultExportFI(defaultFilename); + QFileInfo thisExportFI(fileName); + if (filenameWasGenerated && + thisExportFI.completeBaseName() == defaultExportFI.completeBaseName()) + lastExportUsedGeneratedFilename = true; + else + lastExportUsedGeneratedFilename = false; + lastExportFullPath = fileName; } } diff --git a/src/Gui/DlgPreferencesImp.cpp b/src/Gui/DlgPreferencesImp.cpp index 6ac6bd973f..e70981521a 100644 --- a/src/Gui/DlgPreferencesImp.cpp +++ b/src/Gui/DlgPreferencesImp.cpp @@ -48,16 +48,18 @@ #include "BitmapFactory.h" #include "MainWindow.h" - using namespace Gui::Dialog; +const int DlgPreferencesImp::GroupNameRole = Qt::UserRole; + /* TRANSLATOR Gui::Dialog::DlgPreferencesImp */ std::list DlgPreferencesImp::_pages; +DlgPreferencesImp* DlgPreferencesImp::_activeDialog = nullptr; /** - * Constructs a DlgPreferencesImp which is a child of 'parent', with the - * name 'name' and widget flags set to 'f' + * Constructs a DlgPreferencesImp which is a child of 'parent', with + * widget flags set to 'fl' * * The dialog will by default be modeless, unless you set 'modal' to * true to construct a modal dialog. @@ -67,8 +69,8 @@ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) invalidParameter(false), canEmbedScrollArea(true) { ui->setupUi(this); - ui->listBox->setFixedWidth(130); - ui->listBox->setGridSize(QSize(108, 120)); + ui->listBox->setFixedWidth(108); + ui->listBox->setGridSize(QSize(108, 75)); connect(ui->buttonBox, SIGNAL (helpRequested()), getMainWindow(), SLOT (whatsThis())); @@ -76,6 +78,11 @@ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) this, SLOT(changeGroup(QListWidgetItem *, QListWidgetItem*))); setupPages(); + + // Maintain a static pointer to the current active dialog (if there is one) so that + // if the static page manipulation functions are called while the dialog is showing + // it can update its content. + DlgPreferencesImp::_activeDialog = this; } /** @@ -83,50 +90,19 @@ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) */ DlgPreferencesImp::~DlgPreferencesImp() { - // no need to delete child widgets, Qt does it all for us - delete ui; + if (DlgPreferencesImp::_activeDialog == this) { + DlgPreferencesImp::_activeDialog = nullptr; + } } void DlgPreferencesImp::setupPages() { // make sure that pages are ready to create GetWidgetFactorySupplier(); - for (std::list::iterator it = _pages.begin(); it != _pages.end(); ++it) { - QTabWidget* tabWidget = new QTabWidget; - ui->tabWidgetStack->addWidget(tabWidget); - - QByteArray group = it->first.c_str(); - QListWidgetItem *item = new QListWidgetItem(ui->listBox); - item->setData(Qt::UserRole, QVariant(group)); - item->setText(QObject::tr(group.constData())); - std::string fileName = it->first; - for (std::string::iterator ch = fileName.begin(); ch != fileName.end(); ++ch) { - if (*ch == ' ') *ch = '_'; - else *ch = tolower(*ch); - } - fileName = std::string("preferences-") + fileName; - QPixmap icon = Gui::BitmapFactory().pixmapFromSvg(fileName.c_str(), QSize(96,96)); - if (icon.isNull()) { - icon = Gui::BitmapFactory().pixmap(fileName.c_str()); - if (icon.isNull()) { - qWarning() << "No group icon found for " << fileName.c_str(); - } - else if (icon.size() != QSize(96,96)) { - qWarning() << "Group icon for " << fileName.c_str() << " is not of size 96x96"; - } - } - item->setIcon(icon); - item->setTextAlignment(Qt::AlignHCenter); - item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - for (std::list::iterator jt = it->second.begin(); jt != it->second.end(); ++jt) { - PreferencePage* page = WidgetFactory().createPreferencePage(jt->c_str()); - if (page) { - tabWidget->addTab(page, page->windowTitle()); - page->loadSettings(); - } - else { - Base::Console().Warning("%s is not a preference page\n", jt->c_str()); - } + for (const auto &group : _pages) { + QTabWidget* groupTab = createTabForGroup(group.first); + for (const auto &page : group.second) { + createPageInGroup(groupTab, page); } } @@ -134,6 +110,64 @@ void DlgPreferencesImp::setupPages() ui->listBox->setCurrentRow(0); } +/** + * Create the necessary widgets for a new group named \a groupName. Returns a + * pointer to the group's QTabWidget: that widget's lifetime is managed by the + * tabWidgetStack, do not manually deallocate. + */ +QTabWidget* DlgPreferencesImp::createTabForGroup(const std::string &groupName) +{ + QString groupNameQString = QString::fromStdString(groupName); + + QTabWidget* tabWidget = new QTabWidget; + ui->tabWidgetStack->addWidget(tabWidget); + tabWidget->setProperty("GroupName", QVariant(groupNameQString)); + + QListWidgetItem* item = new QListWidgetItem(ui->listBox); + item->setData(GroupNameRole, QVariant(groupNameQString)); + item->setText(QObject::tr(groupNameQString.toLatin1())); + item->setToolTip(QObject::tr(groupNameQString.toLatin1())); + std::string fileName = groupName; + for (auto & ch : fileName) { + if (ch == ' ') ch = '_'; + else ch = tolower(ch); + } + fileName = std::string("preferences-") + fileName; + QPixmap icon = Gui::BitmapFactory().pixmapFromSvg(fileName.c_str(), QSize(48, 48)); + if (icon.isNull()) { + icon = Gui::BitmapFactory().pixmap(fileName.c_str()); + if (icon.isNull()) { + qWarning() << "No group icon found for " << fileName.c_str(); + } + else if (icon.size() != QSize(48, 48)) { + icon = icon.scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation); + qWarning() << "Group icon for " << fileName.c_str() << " is not of size 48x48, so it was scaled"; + } + } + item->setIcon(icon); + item->setTextAlignment(Qt::AlignHCenter); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + + return tabWidget; +} + +/** + * Create a new preference page called \a pageName on the group tab \a tabWidget. + */ +void DlgPreferencesImp::createPageInGroup(QTabWidget *tabWidget, const std::string &pageName) +{ + PreferencePage* page = WidgetFactory().createPreferencePage(pageName.c_str()); + if (page) { + tabWidget->addTab(page, page->windowTitle()); + page->loadSettings(); + page->setProperty("GroupName", tabWidget->property("GroupName")); + page->setProperty("PageName", QVariant(QString::fromStdString(pageName))); + } + else { + Base::Console().Warning("%s is not a preference page\n", pageName.c_str()); + } +} + void DlgPreferencesImp::changeGroup(QListWidgetItem *current, QListWidgetItem *previous) { if (!current) @@ -150,16 +184,29 @@ void DlgPreferencesImp::changeGroup(QListWidgetItem *current, QListWidgetItem *p */ void DlgPreferencesImp::addPage(const std::string& className, const std::string& group) { + std::list::iterator groupToAddTo = _pages.end(); for (std::list::iterator it = _pages.begin(); it != _pages.end(); ++it) { if (it->first == group) { - it->second.push_back(className); - return; + groupToAddTo = it; + break; } } - std::list pages; - pages.push_back(className); - _pages.push_back(std::make_pair(group, pages)); + if (groupToAddTo != _pages.end()) { + // The group exists: add this page to the end of the list + groupToAddTo->second.push_back(className); + } + else { + // This is a new group: create it, with its one page + std::list pages; + pages.push_back(className); + _pages.push_back(std::make_pair(group, pages)); + } + + if (DlgPreferencesImp::_activeDialog != nullptr) { + // If the dialog is currently showing, tell it to insert the new page + _activeDialog->reloadPages(); + } } void DlgPreferencesImp::removePage(const std::string& className, const std::string& group) @@ -193,7 +240,7 @@ void DlgPreferencesImp::activateGroupPage(const QString& group, int index) int ct = ui->listBox->count(); for (int i=0; ilistBox->item(i); - if (item->data(Qt::UserRole).toString() == group) { + if (item->data(GroupNameRole).toString() == group) { ui->listBox->setCurrentItem(item); QTabWidget* tabWidget = (QTabWidget*)ui->tabWidgetStack->widget(i); tabWidget->setCurrentIndex(index); @@ -250,6 +297,57 @@ void DlgPreferencesImp::restoreDefaults() } } +/** + * If the dialog is currently showing and the static variable _pages changed, this function + * will rescan that list of pages and add any that are new to the current dialog. It will not + * remove any pages that are no longer in the list, and will not change the user's current + * active page. + */ +void DlgPreferencesImp::reloadPages() +{ + // Make sure that pages are ready to create + GetWidgetFactorySupplier(); + + for (const auto &group : _pages) { + QString groupName = QString::fromStdString(group.first); + + // First, does this group already exist? + QTabWidget* tabWidget = nullptr; + for (int tabNumber = 0; tabNumber < ui->tabWidgetStack->count(); ++tabNumber) { + auto thisTabWidget = qobject_cast(ui->tabWidgetStack->widget(tabNumber)); + if (thisTabWidget->property("GroupName").toString() == groupName) { + tabWidget = thisTabWidget; + break; + } + } + + // This is a new tab that wasn't there when we started this instance of the dialog: + if (!tabWidget) { + tabWidget = createTabForGroup(group.first); + } + + // Move on to the pages in the group to see if we need to add any + for (const auto& page : group.second) { + + // Does this page already exist? + QString pageName = QString::fromStdString(page); + bool pageExists = false; + for (int pageNumber = 0; pageNumber < tabWidget->count(); ++pageNumber) { + PreferencePage* prefPage = qobject_cast(tabWidget->widget(pageNumber)); + if (prefPage && prefPage->property("PageName").toString() == pageName) { + pageExists = true; + break; + } + } + + // This is a new page that wasn't there when we started this instance of the dialog: + if (!pageExists) { + createPageInGroup(tabWidget, page); + } + } + } +} + void DlgPreferencesImp::applyChanges() { // Checks if any of the classes that represent several pages of settings @@ -366,7 +464,7 @@ void DlgPreferencesImp::changeEvent(QEvent *e) // update the items' text for (int i=0; ilistBox->count(); i++) { QListWidgetItem *item = ui->listBox->item(i); - QByteArray group = item->data(Qt::UserRole).toByteArray(); + QByteArray group = item->data(GroupNameRole).toByteArray(); item->setText(QObject::tr(group.constData())); } } else { diff --git a/src/Gui/DlgPreferencesImp.h b/src/Gui/DlgPreferencesImp.h index 6b6ff17ee4..172baba36d 100644 --- a/src/Gui/DlgPreferencesImp.h +++ b/src/Gui/DlgPreferencesImp.h @@ -25,9 +25,11 @@ #define GUI_DIALOG_DLGPREFERENCESIMP_H #include +#include class QAbstractButton; class QListWidgetItem; +class QTabWidget; namespace Gui { namespace Dialog { @@ -123,6 +125,7 @@ protected: void showEvent(QShowEvent*); void resizeEvent(QResizeEvent*); + protected Q_SLOTS: void changeGroup(QListWidgetItem *current, QListWidgetItem *previous); void on_buttonBox_clicked(QAbstractButton*); @@ -132,16 +135,23 @@ private: /** @name for internal use only */ //@{ void setupPages(); + QTabWidget* createTabForGroup(const std::string& groupName); + void createPageInGroup(QTabWidget* tabWidget, const std::string& pageName); void applyChanges(); void restoreDefaults(); + void reloadPages(); //@} private: - typedef std::pair > TGroupPages; + typedef std::pair> TGroupPages; static std::list _pages; /**< Name of all registered preference pages */ - Ui_DlgPreferences* ui; + std::unique_ptr ui; bool invalidParameter; bool canEmbedScrollArea; + + static const int GroupNameRole; /**< A name for our Qt::UserRole, used when storing user data in a list item */ + + static DlgPreferencesImp* _activeDialog; /**< Defaults to the nullptr, points to the current instance if there is one */ }; } // namespace Dialog diff --git a/src/Gui/DlgPropertyLink.cpp b/src/Gui/DlgPropertyLink.cpp index 6fb8f2c1f0..ff99b918ed 100644 --- a/src/Gui/DlgPropertyLink.cpp +++ b/src/Gui/DlgPropertyLink.cpp @@ -897,7 +897,7 @@ QTreeWidgetItem *DlgPropertyLink::createItem( if(allowSubObject) { item->setChildIndicatorPolicy(obj->getLinkedObject(true)->getOutList().size()? QTreeWidgetItem::ShowIndicator:QTreeWidgetItem::DontShowIndicator); - item->setFlags(item->flags() | Qt::ItemIsEditable); + item->setFlags(item->flags() | Qt::ItemIsEditable | Qt::ItemIsUserCheckable); } const char *typeName = obj->getTypeId().getName(); diff --git a/src/Gui/DlgSettingsLazyLoaded.ui b/src/Gui/DlgSettingsLazyLoaded.ui new file mode 100644 index 0000000000..3bfd0e9490 --- /dev/null +++ b/src/Gui/DlgSettingsLazyLoaded.ui @@ -0,0 +1,106 @@ + + + Gui::Dialog::DlgSettingsLazyLoaded + + + + 0 + 0 + 607 + 859 + + + + Unloaded Workbenches + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <html><head/><body><p>Load the selected workbenches, adding their preference windows to the preferences dialog.</p></body></html> + + + Load Selected + + + + + + + + + + 0 + 0 + + + + + 0 + 150 + + + + <html><head/><body><p>Available unloaded workbenches</p></body></html> + + + QAbstractItemView::ExtendedSelection + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + <html><head/><body><p>To preserve resources, FreeCAD does not load workbenches until they are used. Loading them may provide access to additional preferences related to their functionality.</p><p>The following workbenches are available in your installation, but are not yet loaded:</p></body></html> + + + true + + + + + + + Qt::Vertical + + + + 429 + 37 + + + + + + + + + + diff --git a/src/Gui/DlgSettingsLazyLoadedImp.cpp b/src/Gui/DlgSettingsLazyLoadedImp.cpp new file mode 100644 index 0000000000..660c10c79b --- /dev/null +++ b/src/Gui/DlgSettingsLazyLoadedImp.cpp @@ -0,0 +1,119 @@ +/*************************************************************************** + * Copyright (c) 2020 Chris Hennes (chennes@pioneerlibrarysystem.org) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "DlgSettingsLazyLoadedImp.h" +#include "ui_DlgSettingsLazyLoaded.h" +#include "PrefWidgets.h" +#include "AutoSaver.h" + +#include "Application.h" +#include "WorkbenchManager.h" +#include "Workbench.h" + +using namespace Gui::Dialog; + +const uint DlgSettingsLazyLoadedImp::WorkbenchNameRole = Qt::UserRole; + +/* TRANSLATOR Gui::Dialog::DlgSettingsLazyLoadedImp */ + +/** + * Constructs a DlgSettingsLazyLoadedImp + */ +DlgSettingsLazyLoadedImp::DlgSettingsLazyLoadedImp( QWidget* parent ) + : PreferencePage( parent ) + , ui(new Ui_DlgSettingsLazyLoaded) +{ + ui->setupUi(this); + buildUnloadedWorkbenchList(); + connect(ui->loadButton, SIGNAL(clicked()), this, SLOT(onLoadClicked())); +} + +/** + * Destroys the object and frees any allocated resources + */ +DlgSettingsLazyLoadedImp::~DlgSettingsLazyLoadedImp() +{ +} + + +void DlgSettingsLazyLoadedImp::saveSettings() +{ + +} + +void DlgSettingsLazyLoadedImp::loadSettings() +{ + +} + +void DlgSettingsLazyLoadedImp::onLoadClicked() +{ + Workbench* originalActiveWB = WorkbenchManager::instance()->active(); + auto selection = ui->workbenchList->selectedItems(); + for (const auto& item : selection) { + auto name = item->data(WorkbenchNameRole).toString().toStdString(); + Application::Instance->activateWorkbench(name.c_str()); + } + Application::Instance->activateWorkbench(originalActiveWB->name().c_str()); + buildUnloadedWorkbenchList(); +} + + +/** +Build the list of unloaded workbenches. +*/ +void DlgSettingsLazyLoadedImp::buildUnloadedWorkbenchList() +{ + ui->workbenchList->clear(); + QStringList workbenches = Application::Instance->workbenches(); + for (const auto& wbName : workbenches) { + const auto& wb = WorkbenchManager::instance()->getWorkbench(wbName.toStdString()); + if (!wb) { + auto wbIcon = Application::Instance->workbenchIcon(wbName); + auto wbDisplayName = Application::Instance->workbenchMenuText(wbName); + auto wbTooltip = Application::Instance->workbenchToolTip(wbName); + QListWidgetItem *wbRow = new QListWidgetItem(wbIcon, wbDisplayName); + wbRow->setData(WorkbenchNameRole, QVariant(wbName)); // Store the actual internal name for easier loading + wbRow->setToolTip(wbTooltip); + ui->workbenchList->addItem(wbRow); // Transfers ownership to the QListWidget + } + } + ui->workbenchList->sortItems(); +} + +/** + * Sets the strings of the subwidgets using the current language. + */ +void DlgSettingsLazyLoadedImp::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + ui->retranslateUi(this); + } + else { + QWidget::changeEvent(e); + } +} + +#include "moc_DlgSettingsLazyLoadedImp.cpp" \ No newline at end of file diff --git a/src/Gui/DlgSettingsLazyLoadedImp.h b/src/Gui/DlgSettingsLazyLoadedImp.h new file mode 100644 index 0000000000..71c9ae07d0 --- /dev/null +++ b/src/Gui/DlgSettingsLazyLoadedImp.h @@ -0,0 +1,66 @@ +/*************************************************************************** + * Copyright (c) 2020 Chris Hennes (chennes@pioneerlibrarysystem.org) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef GUI_DIALOG_DLGSETTINGSLAZYLOADED_IMP_H +#define GUI_DIALOG_DLGSETTINGSLAZYLOADED_IMP_H + +#include "PropertyPage.h" +#include + +namespace Gui { +namespace Dialog { +class Ui_DlgSettingsLazyLoaded; + + +/** + * The DlgSettingsLazyLoadedImp class implements a pseudo-preference page explain why + * the remaining preference pages aren't loaded yet, and to help the user do so on demand. + * \author Jürgen Riegel + */ +class DlgSettingsLazyLoadedImp : public PreferencePage +{ + Q_OBJECT + +public: + DlgSettingsLazyLoadedImp( QWidget* parent = 0 ); + ~DlgSettingsLazyLoadedImp(); + + void saveSettings(); + void loadSettings(); + +protected Q_SLOTS: + void onLoadClicked(); + +protected: + void buildUnloadedWorkbenchList(); + void changeEvent(QEvent *e); + +private: + std::unique_ptr ui; + static const uint WorkbenchNameRole; +}; + +} // namespace Dialog +} // namespace Gui + +#endif // GUI_DIALOG_DLGSETTINGSLAZYLOADED_IMP_H diff --git a/src/Gui/DlgSettingsSelection.cpp b/src/Gui/DlgSettingsSelection.cpp new file mode 100644 index 0000000000..aff8d341d2 --- /dev/null +++ b/src/Gui/DlgSettingsSelection.cpp @@ -0,0 +1,79 @@ +/*************************************************************************** + * Copyright (c) 2021 Werner Mayer * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#ifndef _PreComp_ +#endif + +#include "DlgSettingsSelection.h" +#include "ui_DlgSettingsSelection.h" +#include + +using namespace Gui::Dialog; + +/* TRANSLATOR Gui::Dialog::DlgSettingsSelection */ + +DlgSettingsSelection::DlgSettingsSelection(QWidget* parent) + : PreferencePage(parent) + , ui(new Ui_DlgSettingsSelection) +{ + ui->setupUi(this); +} + +DlgSettingsSelection::~DlgSettingsSelection() +{ +} + +void DlgSettingsSelection::saveSettings() +{ + auto handle = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/TreeView"); + handle->SetBool("SyncView", ui->checkBoxAutoSwitch->isChecked()); + handle->SetBool("SyncSelection", ui->checkBoxAutoExpand->isChecked()); + handle->SetBool("PreSelection", ui->checkBoxPreselect->isChecked()); + handle->SetBool("RecordSelection", ui->checkBoxRecord->isChecked()); + handle->SetBool("CheckBoxesSelection", ui->checkBoxSelectionCheckBoxes->isChecked()); +} + +void DlgSettingsSelection::loadSettings() +{ + auto handle = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/TreeView"); + ui->checkBoxAutoSwitch->setChecked(handle->GetBool("SyncView")); + ui->checkBoxAutoExpand->setChecked(handle->GetBool("SyncSelection")); + ui->checkBoxPreselect->setChecked(handle->GetBool("PreSelection")); + ui->checkBoxRecord->setChecked(handle->GetBool("RecordSelection")); + ui->checkBoxSelectionCheckBoxes->setChecked(handle->GetBool("CheckBoxesSelection")); +} + +void DlgSettingsSelection::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + ui->retranslateUi(this); + } + else { + QWidget::changeEvent(e); + } +} + +#include "moc_DlgSettingsSelection.cpp" + diff --git a/src/Gui/DlgSettingsSelection.h b/src/Gui/DlgSettingsSelection.h new file mode 100644 index 0000000000..b58846a6a5 --- /dev/null +++ b/src/Gui/DlgSettingsSelection.h @@ -0,0 +1,62 @@ +/*************************************************************************** + * Copyright (c) 2021 Werner Mayer * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef GUI_DIALOG_DLGSETTINGSSELECTION_H +#define GUI_DIALOG_DLGSETTINGSSELECTION_H + +#include "PropertyPage.h" +#include + +class QDoubleSpinBox; + +namespace Gui { +namespace Dialog { +class Ui_DlgSettingsSelection; + +/** + * The Ui_DlgSettingsSelection class implements a preference page to change settings + * for the selection. + * \author Werner Mayer + */ +class DlgSettingsSelection : public PreferencePage +{ + Q_OBJECT + +public: + DlgSettingsSelection(QWidget* parent = nullptr); + ~DlgSettingsSelection(); + + void saveSettings(); + void loadSettings(); + +protected: + void changeEvent(QEvent *e); + +private: + std::unique_ptr ui; +}; + +} // namespace Dialog +} // namespace Gui + +#endif // GUI_DIALOG_DLGSETTINGSSELECTION_H diff --git a/src/Gui/DlgSettingsSelection.ui b/src/Gui/DlgSettingsSelection.ui new file mode 100644 index 0000000000..12ac3da475 --- /dev/null +++ b/src/Gui/DlgSettingsSelection.ui @@ -0,0 +1,69 @@ + + + Gui::Dialog::DlgSettingsSelection + + + + 0 + 0 + 670 + 411 + + + + Selection + + + + + + Auto switch to the 3D view containing the selected item + + + + + + + Auto expand tree item when the corresponding object is selected in 3D view + + + + + + + Preselect the object in 3D view when mouse over the tree item + + + + + + + Record selection in tree view in order to go back/forward using navigation button + + + + + + + Add checkboxes for selection in document tree + + + + + + + Qt::Vertical + + + + 20 + 245 + + + + + + + + + diff --git a/src/Gui/FileDialog.cpp b/src/Gui/FileDialog.cpp index 139b1bdf78..f2ddf091e3 100644 --- a/src/Gui/FileDialog.cpp +++ b/src/Gui/FileDialog.cpp @@ -135,6 +135,7 @@ QString FileDialog::getSaveFileName (QWidget * parent, const QString & caption, const QString & filter, QString * selectedFilter, Options options) { QString dirName = dir; + bool hasFilename = false; if (dirName.isEmpty()) { dirName = getWorkingDirectory(); } else { @@ -144,14 +145,25 @@ QString FileDialog::getSaveFileName (QWidget * parent, const QString & caption, dirName += QLatin1String("/"); dirName += fi.fileName(); } + if (!fi.fileName().isEmpty()) { + hasFilename = true; + } - // get the suffix for the filter + // get the suffix for the filter: use the selected filter if there is one, + // otherwise find the first valid suffix in the complete list of filters + const QString *filterToSearch; + if (selectedFilter != nullptr) { + filterToSearch = selectedFilter; + } + else { + filterToSearch = &filter; + } QRegExp rx; rx.setPattern(QLatin1String("\\s(\\(\\*\\.\\w{1,})\\W")); - int index = rx.indexIn(filter); + int index = rx.indexIn(*filterToSearch); if (index != -1) { // get the suffix with the leading dot - QString suffix = filter.mid(index+3, rx.matchedLength()-4); + QString suffix = filterToSearch->mid(index+3, rx.matchedLength()-4); if (fi.suffix().isEmpty()) dirName += suffix; } @@ -199,6 +211,8 @@ QString FileDialog::getSaveFileName (QWidget * parent, const QString & caption, dlg.setFileMode(QFileDialog::AnyFile); dlg.setAcceptMode(QFileDialog::AcceptSave); dlg.setDirectory(dirName); + if (hasFilename) + dlg.selectFile(dirName); dlg.setOptions(options); dlg.setNameFilters(filter.split(QLatin1String(";;"))); if (selectedFilter && !selectedFilter->isEmpty()) diff --git a/src/Gui/Icons/preferences-workbenches.svg b/src/Gui/Icons/preferences-workbenches.svg new file mode 100644 index 0000000000..370abc9f77 --- /dev/null +++ b/src/Gui/Icons/preferences-workbenches.svg @@ -0,0 +1,831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + 2005-10-15 + + + Andreas Nilsson + + + + + edit + copy + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/Icons/resource.qrc b/src/Gui/Icons/resource.qrc index b626752ac7..28d9c0b170 100644 --- a/src/Gui/Icons/resource.qrc +++ b/src/Gui/Icons/resource.qrc @@ -26,6 +26,7 @@ preferences-display.svg preferences-general.svg preferences-import-export.svg + preferences-workbenches.svg utilities-terminal.svg ClassBrowser/const_member.png ClassBrowser/member.png diff --git a/src/Gui/Inventor/MarkerBitmaps.cpp b/src/Gui/Inventor/MarkerBitmaps.cpp index 732cbe5f36..90ed4a6af2 100644 --- a/src/Gui/Inventor/MarkerBitmaps.cpp +++ b/src/Gui/Inventor/MarkerBitmaps.cpp @@ -345,12 +345,12 @@ const char circleLine11_marker[CIRCLE_LINE11_WIDTH * CIRCLE_LINE11_HEIGHT + 1] = " " " xxxxxx " " xxxxxxxx " -" xx xx " +" xxx xxx" " xx xx" " xx xx" " xx xx" " xx xx" -" xx xx " +" xxx xxx" " xxxxxxxx " " xxxxxx "}; @@ -381,7 +381,7 @@ const char circleLine15_marker[CIRCLE_LINE15_WIDTH * CIRCLE_LINE15_HEIGHT + 1] = " " " xxxxxx " " xxxxxxxxxx " -" xx xx " +" xxx xxx " " xx xx " " xx xx" " xx xx" @@ -390,7 +390,7 @@ const char circleLine15_marker[CIRCLE_LINE15_WIDTH * CIRCLE_LINE15_HEIGHT + 1] = " xx xx" " xx xx" " xx xx " -" xx xxxx" +" xxx xxx " " xxxxxxxxxx " " xxxxxx "}; @@ -401,12 +401,12 @@ const char circleFilled11_marker[CIRCLE_FILLED11_WIDTH * CIRCLE_FILLED11_HEIGHT " " " xxxxxx " " xxxxxxxx " -" xxxxxxxx " " xxxxxxxxxx" " xxxxxxxxxx" " xxxxxxxxxx" " xxxxxxxxxx" -" xxxxxxxx " +" xxxxxxxxxx" +" xxxxxxxxxx" " xxxxxxxx " " xxxxxx "}; @@ -446,7 +446,7 @@ const char circleFilled15_marker[CIRCLE_FILLED15_WIDTH * CIRCLE_FILLED15_HEIGHT " xxxxxxxxxxxxxx" " xxxxxxxxxxxxxx" " xxxxxxxxxxxx " -" xxxxxxxxxxxx" +" xxxxxxxxxxxx " " xxxxxxxxxx " " xxxxxx "}; diff --git a/src/Gui/TaskView/TaskDialog.cpp b/src/Gui/TaskView/TaskDialog.cpp index 75643b1837..e36279c843 100644 --- a/src/Gui/TaskView/TaskDialog.cpp +++ b/src/Gui/TaskView/TaskDialog.cpp @@ -79,6 +79,11 @@ void TaskDialog::open() } +void TaskDialog::closed() +{ + +} + void TaskDialog::clicked(int) { diff --git a/src/Gui/TaskView/TaskDialog.h b/src/Gui/TaskView/TaskDialog.h index e26bbc8fb8..51c44ba359 100644 --- a/src/Gui/TaskView/TaskDialog.h +++ b/src/Gui/TaskView/TaskDialog.h @@ -103,6 +103,8 @@ public: public: /// is called by the framework when the dialog is opened virtual void open(); + /// is called by the framework when the dialog is closed + virtual void closed(); /// is called by the framework if a button is clicked which has no accept or reject role virtual void clicked(int); /// is called by the framework if the dialog is accepted (Ok) diff --git a/src/Gui/TaskView/TaskView.cpp b/src/Gui/TaskView/TaskView.cpp index 12fa730d60..456c411905 100644 --- a/src/Gui/TaskView/TaskView.cpp +++ b/src/Gui/TaskView/TaskView.cpp @@ -651,6 +651,7 @@ void TaskView::removeDialog(void) addTaskWatcher(); if (remove) { + remove->closed(); remove->emitDestructionSignal(); delete remove; } @@ -775,6 +776,11 @@ void TaskView::removeTaskWatcher(void) void TaskView::accept() { + if (!ActiveDialog) { // Protect against segfaults due to out-of-order deletions + Base::Console().Warning("ActiveDialog was null in call to TaskView::accept()\n"); + return; + } + // Make sure that if 'accept' calls 'closeDialog' the deletion is postponed until // the dialog leaves the 'accept' method ActiveDialog->setProperty("taskview_accept_or_reject", true); @@ -786,6 +792,11 @@ void TaskView::accept() void TaskView::reject() { + if (!ActiveDialog) { // Protect against segfaults due to out-of-order deletions + Base::Console().Warning("ActiveDialog was null in call to TaskView::reject()\n"); + return; + } + // Make sure that if 'reject' calls 'closeDialog' the deletion is postponed until // the dialog leaves the 'reject' method ActiveDialog->setProperty("taskview_accept_or_reject", true); diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index 222625d279..d2557d3dc1 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -139,6 +139,11 @@ void TreeParams::onSyncSelectionChanged() { TreeWidget::scrollItemToTop(); } +void TreeParams::onCheckBoxesSelectionChanged() +{ + TreeWidget::instance()->synchronizeSelectionCheckBoxes(); +} + void TreeParams::onDocumentModeChanged() { App::GetApplication().setActiveDocument(App::GetApplication().getActiveDocument()); } @@ -549,6 +554,8 @@ TreeWidget::TreeWidget(const char *name, QWidget* parent) this, SLOT(onItemExpanded(QTreeWidgetItem*))); connect(this, SIGNAL(itemSelectionChanged()), this, SLOT(onItemSelectionChanged())); + connect(this, SIGNAL(itemChanged(QTreeWidgetItem*, int)), + this, SLOT(onItemChanged(QTreeWidgetItem*, int))); connect(this->preselectTimer, SIGNAL(timeout()), this, SLOT(onPreSelectTimer())); connect(this->selectTimer, SIGNAL(timeout()), @@ -2800,6 +2807,33 @@ void TreeWidget::onItemSelectionChanged () this->blockConnection(lock); } +static bool isSelectionCheckBoxesEnabled() { + return TreeParams::Instance()->CheckBoxesSelection(); +} + +void TreeWidget::synchronizeSelectionCheckBoxes() { + const bool useCheckBoxes = isSelectionCheckBoxesEnabled(); + for (QTreeWidgetItemIterator it(this); *it; ++it) { + if (const auto item = dynamic_cast(*it)) { + if (useCheckBoxes) + item->QTreeWidgetItem::setCheckState(0, item->isSelected() ? Qt::Checked : Qt::Unchecked); + else + item->setData(0, Qt::CheckStateRole, QVariant()); + } + } + resizeColumnToContents(0); +} + +void TreeWidget::onItemChanged(QTreeWidgetItem *item, int column) { + if (column == 0 && isSelectionCheckBoxesEnabled()) { + bool selected = item->isSelected(); + bool checked = item->checkState(0) == Qt::Checked; + if (checked != selected) { + item->setSelected(checked); + } + } +} + void TreeWidget::onSelectTimer() { _updateStatus(false); @@ -3923,6 +3957,7 @@ void DocumentItem::clearSelection(DocumentObjectItem *exclude) item->selected = 0; item->mySubs.clear(); item->setSelected(false); + item->setCheckState(false); } END_FOREACH_ITEM; treeWidget()->blockSignals(ok); @@ -3933,8 +3968,10 @@ void DocumentItem::updateSelection(QTreeWidgetItem *ti, bool unselect) { auto child = ti->child(i); if(child && child->type()==TreeWidget::ObjectType) { auto childItem = static_cast(child); - if(unselect) + if (unselect) { childItem->setSelected(false); + childItem->setCheckState(false); + } updateItemSelection(childItem); if(unselect && childItem->isGroup()) { // If the child item being force unselected by its group parent @@ -3952,8 +3989,17 @@ void DocumentItem::updateSelection(QTreeWidgetItem *ti, bool unselect) { void DocumentItem::updateItemSelection(DocumentObjectItem *item) { bool selected = item->isSelected(); - if((selected && item->selected>0) || (!selected && !item->selected)) + bool checked = item->checkState(0) == Qt::Checked; + + if(selected && !checked) + item->setCheckState(true); + + if(!selected && checked) + item->setCheckState(false); + + if((selected && item->selected>0) || (!selected && !item->selected)) { return; + } if(item->selected != -1) item->mySubs.clear(); item->selected = selected; @@ -4026,6 +4072,7 @@ void DocumentItem::updateItemSelection(DocumentObjectItem *item) { if(!Gui::Selection().addSelection(docname,objname,subname.c_str())) { item->selected = 0; item->setSelected(false); + item->setCheckState(false); return; } } @@ -4221,6 +4268,7 @@ void DocumentItem::selectItems(SelectionReason reason) { item->selected = 0; item->mySubs.clear(); item->setSelected(false); + item->setCheckState(false); }else if(item->selected) { if(sync) { if(item->selected==2 && showItem(item,false,reason==SR_FORCE_EXPAND)) { @@ -4242,6 +4290,7 @@ void DocumentItem::selectItems(SelectionReason reason) { } item->selected = 1; item->setSelected(true); + item->setCheckState(true); } END_FOREACH_ITEM; @@ -4338,8 +4387,10 @@ bool DocumentItem::showItem(DocumentObjectItem *item, bool select, bool force) { }else parent->setExpanded(true); - if(select) + if(select) { item->setSelected(true); + item->setCheckState(true); + } return true; } @@ -4366,7 +4417,9 @@ DocumentObjectItem::DocumentObjectItem(DocumentItem *ownerDocItem, DocumentObjec : QTreeWidgetItem(TreeWidget::ObjectType) , myOwner(ownerDocItem), myData(data), previousStatus(-1),selected(0),populated(false) { - setFlags(flags()|Qt::ItemIsEditable); + setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsUserCheckable); + setCheckState(false); + myData->items.insert(this); ++countItems; TREE_LOG("Create item: " << countItems << ", " << object()->getObject()->getFullName()); @@ -4948,6 +5001,13 @@ App::DocumentObject *DocumentObjectItem::getRelativeParent( return 0; } +void DocumentObjectItem::setCheckState(bool checked) { + if (isSelectionCheckBoxesEnabled()) + QTreeWidgetItem::setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); + else + setData(0, Qt::CheckStateRole, QVariant()); +} + DocumentItem *DocumentObjectItem::getParentDocument() const { return getTree()->getDocumentItem(object()->getDocument()); } diff --git a/src/Gui/Tree.h b/src/Gui/Tree.h index 8f9b2a5a76..aa9e4cc9e2 100644 --- a/src/Gui/Tree.h +++ b/src/Gui/Tree.h @@ -129,6 +129,8 @@ public: void startItemSearch(QLineEdit*); void itemSearch(const QString &text, bool select); + void synchronizeSelectionCheckBoxes(); + protected: /// Observer message from the Selection void onSelectionChanged(const SelectionChanges& msg) override; @@ -176,6 +178,7 @@ protected Q_SLOTS: private Q_SLOTS: void onItemSelectionChanged(void); + void onItemChanged(QTreeWidgetItem*, int); void onItemEntered(QTreeWidgetItem * item); void onItemCollapsed(QTreeWidgetItem * item); void onItemExpanded(QTreeWidgetItem * item); @@ -436,6 +439,8 @@ public: TreeWidget *getTree() const; private: + void setCheckState(bool checked); + QBrush bgBrush; DocumentItem *myOwner; DocumentObjectDataPtr myData; @@ -521,6 +526,7 @@ public: #define FC_TREEPARAM_DEFS \ FC_TREEPARAM_DEF2(SyncSelection,bool,Bool,true) \ + FC_TREEPARAM_DEF2(CheckBoxesSelection,bool,Bool,false) \ FC_TREEPARAM_DEF(SyncView,bool,Bool,true) \ FC_TREEPARAM_DEF(PreSelection,bool,Bool,true) \ FC_TREEPARAM_DEF(SyncPlacement,bool,Bool,false) \ diff --git a/src/Gui/View3DInventor.cpp b/src/Gui/View3DInventor.cpp index 396da77057..43a923a66b 100644 --- a/src/Gui/View3DInventor.cpp +++ b/src/Gui/View3DInventor.cpp @@ -774,47 +774,53 @@ bool View3DInventor::setCamera(const char* pCamera) SoNode * Cam; SoDB::read(&in,Cam); - if (!Cam){ + if (!Cam || !Cam->isOfType(SoCamera::getClassTypeId())) { throw Base::RuntimeError("Camera settings failed to read"); } + // this is to make sure to reliably delete the node + CoinPtr camPtr(Cam, true); + // toggle between perspective and orthographic camera - if (Cam->getTypeId() != CamViewer->getTypeId()) - { + if (Cam->getTypeId() != CamViewer->getTypeId()) { _viewer->setCameraType(Cam->getTypeId()); CamViewer = _viewer->getSoRenderManager()->getCamera(); } - SoPerspectiveCamera * CamViewerP = 0; - SoOrthographicCamera * CamViewerO = 0; + SoPerspectiveCamera * CamViewerP = nullptr; + SoOrthographicCamera * CamViewerO = nullptr; if (CamViewer->getTypeId() == SoPerspectiveCamera::getClassTypeId()) { - CamViewerP = (SoPerspectiveCamera *)CamViewer; // safe downward cast, knows the type - } else if (CamViewer->getTypeId() == SoOrthographicCamera::getClassTypeId()) { - CamViewerO = (SoOrthographicCamera *)CamViewer; // safe downward cast, knows the type + CamViewerP = static_cast(CamViewer); // safe downward cast, knows the type + } + else if (CamViewer->getTypeId() == SoOrthographicCamera::getClassTypeId()) { + CamViewerO = static_cast(CamViewer); // safe downward cast, knows the type } if (Cam->getTypeId() == SoPerspectiveCamera::getClassTypeId()) { if (CamViewerP){ - CamViewerP->position = ((SoPerspectiveCamera *)Cam)->position; - CamViewerP->orientation = ((SoPerspectiveCamera *)Cam)->orientation; - CamViewerP->nearDistance = ((SoPerspectiveCamera *)Cam)->nearDistance; - CamViewerP->farDistance = ((SoPerspectiveCamera *)Cam)->farDistance; - CamViewerP->focalDistance = ((SoPerspectiveCamera *)Cam)->focalDistance; - } else { + CamViewerP->position = static_cast(Cam)->position; + CamViewerP->orientation = static_cast(Cam)->orientation; + CamViewerP->nearDistance = static_cast(Cam)->nearDistance; + CamViewerP->farDistance = static_cast(Cam)->farDistance; + CamViewerP->focalDistance = static_cast(Cam)->focalDistance; + } + else { throw Base::TypeError("Camera type mismatch"); } - } else if (Cam->getTypeId() == SoOrthographicCamera::getClassTypeId()) { + } + else if (Cam->getTypeId() == SoOrthographicCamera::getClassTypeId()) { if (CamViewerO){ - CamViewerO->viewportMapping = ((SoOrthographicCamera *)Cam)->viewportMapping; - CamViewerO->position = ((SoOrthographicCamera *)Cam)->position; - CamViewerO->orientation = ((SoOrthographicCamera *)Cam)->orientation; - CamViewerO->nearDistance = ((SoOrthographicCamera *)Cam)->nearDistance; - CamViewerO->farDistance = ((SoOrthographicCamera *)Cam)->farDistance; - CamViewerO->focalDistance = ((SoOrthographicCamera *)Cam)->focalDistance; - CamViewerO->aspectRatio = ((SoOrthographicCamera *)Cam)->aspectRatio ; - CamViewerO->height = ((SoOrthographicCamera *)Cam)->height; - } else { + CamViewerO->viewportMapping = static_cast(Cam)->viewportMapping; + CamViewerO->position = static_cast(Cam)->position; + CamViewerO->orientation = static_cast(Cam)->orientation; + CamViewerO->nearDistance = static_cast(Cam)->nearDistance; + CamViewerO->farDistance = static_cast(Cam)->farDistance; + CamViewerO->focalDistance = static_cast(Cam)->focalDistance; + CamViewerO->aspectRatio = static_cast(Cam)->aspectRatio ; + CamViewerO->height = static_cast(Cam)->height; + } + else { throw Base::TypeError("Camera type mismatch"); } } diff --git a/src/Gui/ViewProviderExtension.h b/src/Gui/ViewProviderExtension.h index dd6ce09bfd..27af83db83 100644 --- a/src/Gui/ViewProviderExtension.h +++ b/src/Gui/ViewProviderExtension.h @@ -130,13 +130,9 @@ public: ViewProviderExtensionPythonT() { ExtensionT::m_isPythonExtension = true; ExtensionT::initExtensionType(ViewProviderExtensionPythonT::getExtensionClassTypeId()); - - EXTENSION_ADD_PROPERTY(ExtensionProxy,(Py::Object())); } virtual ~ViewProviderExtensionPythonT() { } - - App::PropertyPythonObject ExtensionProxy; }; typedef ViewProviderExtensionPythonT ViewProviderExtensionPython; diff --git a/src/Gui/ViewProviderLine.cpp b/src/Gui/ViewProviderLine.cpp index 6294d73bf9..bdae7a5f8b 100644 --- a/src/Gui/ViewProviderLine.cpp +++ b/src/Gui/ViewProviderLine.cpp @@ -68,13 +68,11 @@ void ViewProviderLine::attach ( App::DocumentObject *obj ) { sep->addChild ( pCoords ); SoIndexedLineSet *pLines = new SoIndexedLineSet (); - pLines->ref(); pLines->coordIndex.setNum(3); pLines->coordIndex.setValues(0, 3, lines); sep->addChild ( pLines ); SoTranslation *textTranslation = new SoTranslation (); - textTranslation->ref (); textTranslation->translation.setValue ( SbVec3f ( -size * 49. / 50., size / 30., 0 ) ); sep->addChild ( textTranslation ); diff --git a/src/Gui/ViewProviderPlane.cpp b/src/Gui/ViewProviderPlane.cpp index c93e1b8fd9..02fa1b550e 100644 --- a/src/Gui/ViewProviderPlane.cpp +++ b/src/Gui/ViewProviderPlane.cpp @@ -70,13 +70,11 @@ void ViewProviderPlane::attach ( App::DocumentObject *obj ) { sep->addChild ( pCoords ); SoIndexedLineSet *pLines = new SoIndexedLineSet (); - pLines->ref(); pLines->coordIndex.setNum(6); pLines->coordIndex.setValues(0, 6, lines); sep->addChild ( pLines ); SoTranslation *textTranslation = new SoTranslation (); - textTranslation->ref (); textTranslation->translation.setValue ( SbVec3f ( -size * 49. / 50., size * 9./10., 0 ) ); sep->addChild ( textTranslation ); diff --git a/src/Gui/resource.cpp b/src/Gui/resource.cpp index 4449b09b3b..099539a5a1 100644 --- a/src/Gui/resource.cpp +++ b/src/Gui/resource.cpp @@ -27,11 +27,12 @@ #include "WidgetFactory.h" #include "Workbench.h" -// INCLUDE YOUR PREFERENCFE PAGES HERE +// INCLUDE YOUR PREFERENCE PAGES HERE // #include "DlgPreferencesImp.h" #include "DlgSettings3DViewImp.h" #include "DlgSettingsNavigation.h" +#include "DlgSettingsSelection.h" #include "DlgSettingsViewColor.h" #include "DlgGeneralImp.h" #include "DlgEditorImp.h" @@ -40,6 +41,7 @@ #include "DlgSettingsDocumentImp.h" //#include "DlgOnlineHelpImp.h" #include "DlgReportViewImp.h" +#include "DlgSettingsLazyLoadedImp.h" #include "DlgToolbarsImp.h" #include "DlgWorkbenchesImp.h" @@ -63,16 +65,18 @@ WidgetFactorySupplier::WidgetFactorySupplier() // ADD YOUR PREFERENCE PAGES HERE // // - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); - //new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); - new PrefPageProducer( QT_TRANSLATE_NOOP("QObject","General") ); - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Display") ); - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Display") ); - new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Display") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + //new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","General") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Display") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Display") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Display") ); + new PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Workbenches") ); // ADD YOUR CUSTOMIZE PAGES HERE // diff --git a/src/MacAppBundle/CMakeLists.txt b/src/MacAppBundle/CMakeLists.txt index c8bd761b73..c0b6ccf09f 100644 --- a/src/MacAppBundle/CMakeLists.txt +++ b/src/MacAppBundle/CMakeLists.txt @@ -63,16 +63,17 @@ if(BUILD_QT5) endif(BUILD_QT5) # add QtWebEngineProcess to bundle + if(BUILD_WEB) install(PROGRAMS "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess" DESTINATION ${CMAKE_INSTALL_PREFIX}/MacOS) # add locales to bundle - file(GLOB _locales_files RELATIVE "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/qtwebengine_locales" "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/qtwebengine_locales/*") - foreach(_locales_file ${_locales_files}) + file(GLOB _locales_files RELATIVE "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/qtwebengine_locales" "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/qtwebengine_locales/*") + foreach(_locales_file ${_locales_files}) get_filename_component(_resolved_file "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/qtwebengine_locales/${_locales_file}" REALPATH) list(APPEND _locales_resolved_files ${_resolved_file}) - endforeach() - install(FILES ${_locales_resolved_files} DESTINATION "${CMAKE_INSTALL_PREFIX}/MacOS/qtwebengine_locales") - + endforeach() + install(FILES ${_locales_resolved_files} DESTINATION "${CMAKE_INSTALL_PREFIX}/MacOS/qtwebengine_locales") + # add pak file(GLOB _pak_files RELATIVE "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/" "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/*.pak") foreach(_pak_file ${_pak_files}) @@ -82,9 +83,10 @@ endif(BUILD_QT5) install(FILES ${_pak_resolved_files} DESTINATION "${CMAKE_INSTALL_PREFIX}/") # add icudtl.dat + install(PROGRAMS "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/icudtl.dat" DESTINATION ${CMAKE_INSTALL_PREFIX}/) - install(PROGRAMS "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/QtWebEngineCore.prl" DESTINATION ${CMAKE_INSTALL_PREFIX}/) - + install(PROGRAMS "${Qt5Core_DIR}/../../../lib/QtWebEngineCore.framework/Versions/5/Resources/QtWebEngineCore.prl" DESTINATION ${CMAKE_INSTALL_PREFIX}/) + endif(BUILD_WEB) # Ensure the actual plugin files are installed instead of symlinks. diff --git a/src/Mod/Arch/ArchBuildingPart.py b/src/Mod/Arch/ArchBuildingPart.py index ec9a8274ce..8b929ecbc7 100644 --- a/src/Mod/Arch/ArchBuildingPart.py +++ b/src/Mod/Arch/ArchBuildingPart.py @@ -323,8 +323,8 @@ class BuildingPart(ArchIFC.IfcProduct): def __init__(self,obj): obj.Proxy = self - obj.addExtension('App::GroupExtensionPython', self) - #obj.addExtension('App::OriginGroupExtensionPython', self) + obj.addExtension('App::GroupExtensionPython') + #obj.addExtension('App::OriginGroupExtensionPython') self.setProperties(obj) def setProperties(self,obj): @@ -488,8 +488,8 @@ class ViewProviderBuildingPart: def __init__(self,vobj): - vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self) - #vobj.addExtension("Gui::ViewProviderGeoFeatureGroupExtensionPython", self) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + #vobj.addExtension("Gui::ViewProviderGeoFeatureGroupExtensionPython") vobj.Proxy = self self.setProperties(vobj) vobj.ShapeColor = ArchCommands.getDefaultColor("Helpers") diff --git a/src/Mod/Arch/ArchPanel.py b/src/Mod/Arch/ArchPanel.py index 7da9e755a0..14677a76e3 100644 --- a/src/Mod/Arch/ArchPanel.py +++ b/src/Mod/Arch/ArchPanel.py @@ -29,7 +29,7 @@ if FreeCAD.GuiUp: import draftguitools.gui_trackers as DraftTrackers else: # \cond - def translate(ctxt,txt): + def translate(ctxt,txt,utf8_decode=False): return txt def QT_TRANSLATE_NOOP(ctxt,txt): return txt diff --git a/src/Mod/Arch/ArchProject.py b/src/Mod/Arch/ArchProject.py index 863e15f88c..ee2bb2920a 100644 --- a/src/Mod/Arch/ArchProject.py +++ b/src/Mod/Arch/ArchProject.py @@ -158,7 +158,7 @@ class _Project(ArchIFC.IfcContext): ArchIFC.IfcContext.setProperties(self, obj) pl = obj.PropertiesList if not hasattr(obj,"Group"): - obj.addExtension("App::GroupExtensionPython", self) + obj.addExtension("App::GroupExtensionPython") self.Type = "Project" def onDocumentRestored(self, obj): @@ -185,7 +185,7 @@ class _ViewProviderProject(ArchIFCView.IfcContextView): def __init__(self,vobj): vobj.Proxy = self - vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") def getIcon(self): """Return the path to the appropriate icon. diff --git a/src/Mod/Arch/ArchSchedule.py b/src/Mod/Arch/ArchSchedule.py index 9a11a60e5f..57003b1441 100644 --- a/src/Mod/Arch/ArchSchedule.py +++ b/src/Mod/Arch/ArchSchedule.py @@ -220,7 +220,7 @@ class _ArchSchedule: prop = args[0].upper() fval = args[1].upper() if prop == "TYPE": - prop == "IFCTYPE" + prop = "IFCTYPE" if inv: if prop in props: csprop = o.PropertiesList[props.index(prop)] diff --git a/src/Mod/Arch/ArchSite.py b/src/Mod/Arch/ArchSite.py index b75ba6c665..486cea4d05 100644 --- a/src/Mod/Arch/ArchSite.py +++ b/src/Mod/Arch/ArchSite.py @@ -631,7 +631,7 @@ class _Site(ArchIFC.IfcProduct): if not "OriginOffset" in pl: obj.addProperty("App::PropertyVector","OriginOffset","Site",QT_TRANSLATE_NOOP("App::Property","An optional offset between the model (0,0,0) origin and the point indicated by the geocoordinates")) if not hasattr(obj,"Group"): - obj.addExtension("App::GroupExtensionPython", self) + obj.addExtension("App::GroupExtensionPython") if not "IfcType" in pl: obj.addProperty("App::PropertyEnumeration","IfcType","IFC",QT_TRANSLATE_NOOP("App::Property","The type of this object")) obj.IfcType = ArchIFC.IfcTypes @@ -819,7 +819,7 @@ class _ViewProviderSite: def __init__(self,vobj): vobj.Proxy = self - vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") self.setProperties(vobj) def setProperties(self,vobj): diff --git a/src/Mod/Draft/draftfunctions/draftify.py b/src/Mod/Draft/draftfunctions/draftify.py index cb8355b55d..4d1bda8f6d 100644 --- a/src/Mod/Draft/draftfunctions/draftify.py +++ b/src/Mod/Draft/draftfunctions/draftify.py @@ -70,7 +70,7 @@ def draftify(objectslist, makeblock=False, delete=True): for cluster in Part.sortEdges(obj.Shape.Edges): w = Part.Wire(cluster) nobj = draftify_shape(w) - if nobj == None: + if nobj is None: nobj = App.ActiveDocument.addObject("Part::Feature", obj.Name) nobj.Shape = w newobjlist.append(nobj) diff --git a/src/Mod/Draft/draftgeoutils/cuboids.py b/src/Mod/Draft/draftgeoutils/cuboids.py index 9de283a152..833e46634c 100644 --- a/src/Mod/Draft/draftgeoutils/cuboids.py +++ b/src/Mod/Draft/draftgeoutils/cuboids.py @@ -99,6 +99,8 @@ def getCubicDimensions(shape): # getting length and width vx = vec(base.Edges[0]) vy = vec(base.Edges[1]) + if round(vx.Length) == round(vy.Length): + vy = vec(base.Edges[2]) # getting rotations rotZ = DraftVecUtils.angle(vx) diff --git a/src/Mod/Draft/draftgeoutils/geometry.py b/src/Mod/Draft/draftgeoutils/geometry.py index 1695e2bec4..00b5eccd44 100644 --- a/src/Mod/Draft/draftgeoutils/geometry.py +++ b/src/Mod/Draft/draftgeoutils/geometry.py @@ -465,7 +465,7 @@ def calculatePlacement(shape): pos = shape.BoundBox.Center norm = get_normal(shape) # for backward compatibility with previous getNormal implementation - if norm == None: + if norm is None: norm = App.Vector(0, 0, 1) pla = App.Placement() pla.Base = pos diff --git a/src/Mod/Draft/draftgeoutils/offsets.py b/src/Mod/Draft/draftgeoutils/offsets.py index a7ba52ff1c..23fe36c998 100644 --- a/src/Mod/Draft/draftgeoutils/offsets.py +++ b/src/Mod/Draft/draftgeoutils/offsets.py @@ -233,7 +233,7 @@ def offsetWire(wire, dvec, bind=False, occ=False, else: norm = get_normal(wire) # norm = Vector(0, 0, 1) # for backward compatibility with previous getNormal implementation - if norm == None: + if norm is None: norm = App.Vector(0, 0, 1) closed = isReallyClosed(wire) diff --git a/src/Mod/Draft/draftgeoutils/wires.py b/src/Mod/Draft/draftgeoutils/wires.py index 8c6f9dfed9..9ee4a5a1ee 100644 --- a/src/Mod/Draft/draftgeoutils/wires.py +++ b/src/Mod/Draft/draftgeoutils/wires.py @@ -160,7 +160,7 @@ def flattenWire(wire): """Force a wire to get completely flat along its normal.""" n = get_normal(wire) # for backward compatibility with previous getNormal implementation - if n == None: + if n is None: n = App.Vector(0, 0, 1) o = wire.Vertexes[0].Point diff --git a/src/Mod/Draft/draftguitools/gui_snapper.py b/src/Mod/Draft/draftguitools/gui_snapper.py index 71692219ac..bd880e6030 100644 --- a/src/Mod/Draft/draftguitools/gui_snapper.py +++ b/src/Mod/Draft/draftguitools/gui_snapper.py @@ -410,14 +410,8 @@ class Snapper: if (not self.maxEdges) or (len(shape.Edges) <= self.maxEdges): if "Edge" in comp: # we are snapping to an edge - edge = None if shape.ShapeType == "Edge": edge = shape - else: - en = int(comp[4:])-1 - if len(shape.Edges) > en: - edge = shape.Edges[en] - if edge: snaps.extend(self.snapToEndpoints(edge)) snaps.extend(self.snapToMidpoint(edge)) snaps.extend(self.snapToPerpendicular(edge, lastpoint)) @@ -433,9 +427,9 @@ class Snapper: # extra ellipse options snaps.extend(self.snapToCenter(edge)) elif "Face" in comp: - en = int(comp[4:])-1 - if len(shape.Faces) > en: - face = shape.Faces[en] + # we are snapping to a face + if shape.ShapeType == "Face": + face = shape snaps.extend(self.snapToFace(face)) elif "Vertex" in comp: # directly snapped to a vertex diff --git a/src/Mod/Draft/draftmake/make_clone.py b/src/Mod/Draft/draftmake/make_clone.py index 4f9779181f..76e1f3de35 100644 --- a/src/Mod/Draft/draftmake/make_clone.py +++ b/src/Mod/Draft/draftmake/make_clone.py @@ -108,7 +108,7 @@ def make_clone(obj, delta=None, forcedraft=False): # fall back to Draft clone mode if not cl: cl = App.ActiveDocument.addObject("Part::FeaturePython","Clone") - cl.addExtension("Part::AttachExtensionPython", None) + cl.addExtension("Part::AttachExtensionPython") cl.Label = prefix + obj[0].Label Clone(cl) if App.GuiUp: diff --git a/src/Mod/Draft/draftmake/make_line.py b/src/Mod/Draft/draftmake/make_line.py index e8c1068642..eec782f38b 100644 --- a/src/Mod/Draft/draftmake/make_line.py +++ b/src/Mod/Draft/draftmake/make_line.py @@ -39,7 +39,7 @@ def make_line(first_param, last_param=None): Parameters ---------- first_param : - Base.Vector -> First point of the line (if p2 == None) + Base.Vector -> First point of the line (if p2 is None) Part.LineSegment -> Line is created from the given Linesegment Shape -> Line is created from the give Shape diff --git a/src/Mod/Draft/draftobjects/draftlink.py b/src/Mod/Draft/draftobjects/draftlink.py index 0ad659b241..dfaa0d4dc4 100644 --- a/src/Mod/Draft/draftobjects/draftlink.py +++ b/src/Mod/Draft/draftobjects/draftlink.py @@ -79,7 +79,7 @@ class DraftLink(DraftObject): def attach(self, obj): """Set up the properties when the object is attached.""" if self.use_link: - obj.addExtension('App::LinkExtensionPython', None) + obj.addExtension('App::LinkExtensionPython') self.linkSetup(obj) def canLinkProperties(self, _obj): diff --git a/src/Mod/Draft/draftobjects/patharray.py b/src/Mod/Draft/draftobjects/patharray.py index 59b1e0a438..fe7c0355a5 100644 --- a/src/Mod/Draft/draftobjects/patharray.py +++ b/src/Mod/Draft/draftobjects/patharray.py @@ -446,7 +446,7 @@ def placements_on_path(shapeRotation, pathwire, count, xlate, align, closedpath = DraftGeomUtils.isReallyClosed(pathwire) normal = DraftGeomUtils.get_normal(pathwire) # for backward compatibility with previous getNormal implementation - if normal == None: + if normal is None: normal = App.Vector(0, 0, 1) if forceNormal and normalOverride: diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 49430955ba..fac0493aa3 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -2220,7 +2220,7 @@ def processdxf(document, filename, getShapes=False, reComputeFlag=True): drawstyle = "Dashdot" locateLayer(name, color, drawstyle) else: - locateLayer("0", [0.0, 0.0, 0.0], "Solid") + locateLayer("0", (0.0, 0.0, 0.0), "Solid") # Draw lines lines = drawing.entities.get_type("line") diff --git a/src/Mod/Fem/App/FemMesh.cpp b/src/Mod/Fem/App/FemMesh.cpp index 601df01c6b..8a57f250cf 100644 --- a/src/Mod/Fem/App/FemMesh.cpp +++ b/src/Mod/Fem/App/FemMesh.cpp @@ -1280,6 +1280,430 @@ void FemMesh::readNastran(const std::string &Filename) } +void FemMesh::readNastran95(const std::string &Filename) +{ + Base::TimeInfo Start; + Base::Console().Log("Start: FemMesh::readNastran95() =================================\n"); + + _Mtrx = Base::Matrix4D(); + + std::ifstream inputfile; + inputfile.open(Filename.c_str()); + inputfile.seekg(std::ifstream::beg); + std::string line1,line2,tcard; + float cx, cy, cz; + std::vector token_results; + token_results.clear(); + Base::Vector3d current_node; + std::vector vertices; + vertices.clear(); + std::vector nodal_id; + + nodal_id.clear(); + + std::vector bar_element; + std::vector tri_element; + std::vector quad_element; + std::vector tetra_element; + std::vector wedge_element; + std::vector hexa_element; + + std::vector > all_elements; + + std::vector element_id; + std::vector element_type; + + element_id.clear(); + element_type.clear(); + + bool nastran_free_format = false; + do + { + std::getline(inputfile,line1); + //cout << line1 << endl; + if (line1.size() == 0) continue; + //if (!nastran_free_format && line1.find(',')!= std::string::npos) + // nastran_free_format = true; + tcard = line1.substr(0, 8).c_str(); + //boost::algorithm::trim(tcard); + if (!nastran_free_format && line1.find("GRID*")!= std::string::npos ) //We found a Grid line + { + //Now lets extract the GRID Points = Nodes + //As each GRID Line consists of two subsequent lines we have to + //take care of that as well + std::getline(inputfile,line2); + //Get the Nodal ID + nodal_id.push_back(atoi(line1.substr(8,24).c_str())); + //Extract X Value + current_node.x = atof(line1.substr(40,56).c_str()); + //Extract Y Value + current_node.y = atof(line1.substr(56,72).c_str()); + //Extract Z Value + current_node.z = atof(line2.substr(8,24).c_str()); + + vertices.push_back(current_node); + } + else if (!nastran_free_format && line1.find("GRID") != std::string::npos) //We found a Grid line + { + //Base::Console().Log("Found a GRID\n"); + //D06.inp + //GRID 109 .9 .7 + //Now lets extract the GRID Points = Nodes + //Get the Nodal ID + unsigned int id = atoi(line1.substr(8, 16).c_str()); + + //Extract X Value + cx = atof(line1.substr(24, 32).c_str()); + current_node.x = cx; + //Extract Y Value + cy = atof(line1.substr(32, 40).c_str()); + current_node.y = cy; + //Extract Z Value + cz = atof(line1.substr(40, 48).c_str()); + current_node.z = cz; + // atof(line1.substr(40, 48).c_str()); + //Base::Console().Log("nid = %d %f %f %f\n", id, cx, cy, cz); + + nodal_id.push_back(id); + vertices.push_back(current_node); + } + + //1D + else if (line1.substr(0,6)=="CBAR") + { + //Base::Console().Log("Found a CTRMEM\n"); + bar_element.clear(); + unsigned int id = atoi(line1.substr(8,16).c_str()); + + element_type.push_back(100); + element_id.push_back(id); + bar_element.push_back(atoi(line1.substr(24,32).c_str())); + bar_element.push_back(atoi(line1.substr(32,40).c_str())); + + all_elements.push_back(bar_element); + } + //2d +// else if (!nastran_free_format && line1.find("CTRMEM")!= std::string::npos) + else if (line1.substr(0,6)=="CTRMEM") + { + //Base::Console().Log("Found a CTRMEM\n"); + tri_element.clear(); + unsigned int id = atoi(line1.substr(8,16).c_str()); + + //D06 + //CTRMEM 322 1 179 180 185 + element_type.push_back(230); + element_id.push_back(id); + tri_element.push_back(atoi(line1.substr(24,32).c_str())); + tri_element.push_back(atoi(line1.substr(32,40).c_str())); + tri_element.push_back(atoi(line1.substr(40,48).c_str())); + + all_elements.push_back(tri_element); + } + else if (line1.substr(0, 6) == "CTRIA1") + { + //Base::Console().Log("Found a CTRMEM\n"); + tri_element.clear(); + unsigned int id = atoi(line1.substr(8, 16).c_str()); + + //D06 + //CTRMEM 322 1 179 180 185 + element_type.push_back(231); + element_id.push_back(id); + tri_element.push_back(atoi(line1.substr(24, 32).c_str())); + tri_element.push_back(atoi(line1.substr(32, 40).c_str())); + tri_element.push_back(atoi(line1.substr(40, 48).c_str())); + + all_elements.push_back(tri_element); + } + else if (line1.substr(0, 6) == "CQUAD1") + { + //Base::Console().Log("Found a CQUAD1\n"); + quad_element.clear(); + unsigned int id = atoi(line1.substr(8, 16).c_str()); + + //D06 + //CTRMEM 322 1 179 180 185 + element_type.push_back(241); + element_id.push_back(id); + quad_element.push_back(atoi(line1.substr(24, 32).c_str())); + quad_element.push_back(atoi(line1.substr(32, 40).c_str())); + quad_element.push_back(atoi(line1.substr(40, 48).c_str())); + quad_element.push_back(atoi(line1.substr(48, 56).c_str())); + + all_elements.push_back(quad_element); + } + + //3d element + else if (!nastran_free_format && line1.find("CTETRA")!= std::string::npos) + { + //d011121a.inp + //CTETRA 3 200 104 114 3 103 + tetra_element.clear(); + unsigned int id = atoi(line1.substr(8,16).c_str()); + element_type.push_back(340); + + element_id.push_back(id); + tetra_element.push_back(atoi(line1.substr(24,32).c_str())); + tetra_element.push_back(atoi(line1.substr(32,40).c_str())); + tetra_element.push_back(atoi(line1.substr(40,48).c_str())); + tetra_element.push_back(atoi(line1.substr(48,56).c_str())); + //tetra_element.push_back(atoi(line1.substr(56,64).c_str())); + //tetra_element.push_back(atoi(line1.substr(64,72).c_str())); + + all_elements.push_back(tetra_element); + } + else if (!nastran_free_format && line1.find("CWEDGE")!= std::string::npos) + { + //d011121a.inp + //CWEDGE 11 200 6 17 16 106 117 116 + tetra_element.clear(); + unsigned int id = atoi(line1.substr(8,16).c_str()); + element_type.push_back(360); + + element_id.push_back(id); + wedge_element.push_back(atoi(line1.substr(24,32).c_str())); + wedge_element.push_back(atoi(line1.substr(32,40).c_str())); + wedge_element.push_back(atoi(line1.substr(40,48).c_str())); + wedge_element.push_back(atoi(line1.substr(48,56).c_str())); + wedge_element.push_back(atoi(line1.substr(56,64).c_str())); + wedge_element.push_back(atoi(line1.substr(64,72).c_str())); + + all_elements.push_back(wedge_element); + } + else if (!nastran_free_format && line1.find("CHEXA1")!= std::string::npos) + { + //d011121a.inp + //CHEXA1 1 200 1 2 13 12 101 102 +SOL1 + //+SOL1 113 112 + tetra_element.clear(); + std::getline(inputfile,line2); + unsigned int id = atoi(line1.substr(8,16).c_str()); + element_type.push_back(381); + + element_id.push_back(id); + hexa_element.push_back(atoi(line1.substr(24,32).c_str())); + hexa_element.push_back(atoi(line1.substr(32,40).c_str())); + hexa_element.push_back(atoi(line1.substr(40,48).c_str())); + hexa_element.push_back(atoi(line1.substr(48,56).c_str())); + hexa_element.push_back(atoi(line1.substr(56,64).c_str())); + hexa_element.push_back(atoi(line1.substr(64,72).c_str())); + + hexa_element.push_back(atoi(line2.substr(8,16).c_str())); + hexa_element.push_back(atoi(line2.substr(16,24).c_str())); + + all_elements.push_back(hexa_element); + } + else if (!nastran_free_format && line1.find("CHEXA2")!= std::string::npos) + { + //d011121a.inp + //CHEXA1 1 200 1 2 13 12 101 102 +SOL1 + //+SOL1 113 112 + tetra_element.clear(); + std::getline(inputfile,line2); + unsigned int id = atoi(line1.substr(8,16).c_str()); + element_type.push_back(382); + + element_id.push_back(id); + hexa_element.push_back(atoi(line1.substr(24,32).c_str())); + hexa_element.push_back(atoi(line1.substr(32,40).c_str())); + hexa_element.push_back(atoi(line1.substr(40,48).c_str())); + hexa_element.push_back(atoi(line1.substr(48,56).c_str())); + hexa_element.push_back(atoi(line1.substr(56,64).c_str())); + hexa_element.push_back(atoi(line1.substr(64,72).c_str())); + + hexa_element.push_back(atoi(line2.substr(8,16).c_str())); + hexa_element.push_back(atoi(line2.substr(16,24).c_str())); + + all_elements.push_back(hexa_element); + } + +// free format + else if (nastran_free_format && line1.find("GRID")!= std::string::npos ) //We found a Grid line + { + //Base::Console().Log("Found a free format GRID\n"); + char_separator sep(","); + tokenizer > tokens(line1, sep); + token_results.assign(tokens.begin(),tokens.end()); + if (token_results.size() < 3) + continue;//Line does not include Nodal coordinates + nodal_id.push_back(atoi(token_results[1].c_str())); + current_node.x = atof(token_results[3].c_str()); + current_node.y = atof(token_results[4].c_str()); + current_node.z = atof(token_results[5].c_str()); + vertices.push_back(current_node); + } + else if (nastran_free_format && line1.find("CTETRA")!= std::string::npos) + { + //Quadratic Tetrahedral Elements + //Base::Console().Log("Found a CTETRA\n"); + tetra_element.clear(); + std::getline(inputfile,line2); + char_separator sep(","); + tokenizer > tokens(line1.append(line2), sep); + token_results.assign(tokens.begin(),tokens.end()); + if (token_results.size() < 11) + continue;//Line does not include enough nodal IDs + element_id.push_back(atoi(token_results[1].c_str())); + tetra_element.push_back(atoi(token_results[3].c_str())); + tetra_element.push_back(atoi(token_results[4].c_str())); + tetra_element.push_back(atoi(token_results[5].c_str())); + tetra_element.push_back(atoi(token_results[6].c_str())); + tetra_element.push_back(atoi(token_results[7].c_str())); + tetra_element.push_back(atoi(token_results[8].c_str())); + tetra_element.push_back(atoi(token_results[10].c_str())); + tetra_element.push_back(atoi(token_results[11].c_str())); + tetra_element.push_back(atoi(token_results[12].c_str())); + tetra_element.push_back(atoi(token_results[13].c_str())); + + all_elements.push_back(tetra_element); + } + + } + while (inputfile.good()); + inputfile.close(); + +// Base::Console().Log("Done saving node.\n"); + Base::Console().Log(" %f: File read, start building mesh\n",Base::TimeInfo::diffTimeF(Start,Base::TimeInfo())); + + //Now fill the SMESH datastructure + std::vector::const_iterator anodeiterator; + SMESHDS_Mesh* meshds = this->myMesh->GetMeshDS(); + meshds->ClearMesh(); + unsigned int j=0; + for(anodeiterator=vertices.begin(); anodeiterator!=vertices.end(); anodeiterator++) + { + meshds->AddNodeWithID((*anodeiterator).x,(*anodeiterator).y,(*anodeiterator).z,nodal_id[j]); + //Base::Console().Log("nid = %d %f %f %f\n", nodal_id[j], (*anodeiterator).x, (*anodeiterator).y, (*anodeiterator).z); + j++; + } + + + for(size_t i=0;iAddVolumeWithID + ( + //tetra 10 point + meshds->FindNode(all_elements[i][1]), + meshds->FindNode(all_elements[i][0]), + meshds->FindNode(all_elements[i][2]), + meshds->FindNode(all_elements[i][3]), + meshds->FindNode(all_elements[i][4]), + meshds->FindNode(all_elements[i][6]), + meshds->FindNode(all_elements[i][5]), + meshds->FindNode(all_elements[i][8]), + meshds->FindNode(all_elements[i][7]), + meshds->FindNode(all_elements[i][9]), + element_id[i] + ); + + } + //1D element + else if (element_type[i] == 100) + { + //Base::Console().Log("eid = %d %d %d %d\n", element_id[i], all_elements[i][0], all_elements[i][1], all_elements[i][2]); + //cbar + meshds->AddEdgeWithID( + all_elements[i][0], + all_elements[i][1], + element_id[i] + ); + } + //2d element + else if (element_type[i] == 230) + { + //Base::Console().Log("eid = %d %d %d %d\n", element_id[i], all_elements[i][0], all_elements[i][1], all_elements[i][2]); + //ctramem + meshds->AddFaceWithID( + all_elements[i][0], + all_elements[i][1], + all_elements[i][2], + element_id[i] + ); + } + else if (element_type[i] == 231) + { + //ctria1 + meshds->AddFaceWithID( + all_elements[i][0], + all_elements[i][1], + all_elements[i][2], + element_id[i] + ); + } + else if (element_type[i] == 241) + { + //cquad1 + meshds->AddFaceWithID( + all_elements[i][0], + all_elements[i][1], + all_elements[i][2], + all_elements[i][3], + element_id[i] + ); + } + + //3d element + else if (element_type[i] == 340) + { + //ctetra + meshds->AddVolumeWithID( + all_elements[i][0], + all_elements[i][1], + all_elements[i][2], + all_elements[i][3], + element_id[i] + ); + } + else if (element_type[i] == 360) + { + //cwedge + meshds->AddVolumeWithID( + all_elements[i][0], + all_elements[i][1], + all_elements[i][2], + all_elements[i][3], + all_elements[i][4], + all_elements[i][5], + element_id[i] + ); + } + else if (element_type[i] == 381) + { + //chexa1 + meshds->AddVolumeWithID( + all_elements[i][0], + all_elements[i][1], + all_elements[i][2], + all_elements[i][3], + all_elements[i][4], + all_elements[i][5], + all_elements[i][6], + all_elements[i][7], + element_id[i] + ); + } + else if (element_type[i] == 382) + { + //chexa2 + meshds->AddVolumeWithID( + all_elements[i][0], + all_elements[i][1], + all_elements[i][2], + all_elements[i][3], + all_elements[i][4], + all_elements[i][5], + all_elements[i][6], + all_elements[i][7], + element_id[i] + ); + } + } + Base::Console().Log(" %f: Done \n",Base::TimeInfo::diffTimeF(Start,Base::TimeInfo())); +} + void FemMesh::readAbaqus(const std::string &FileName) { Base::TimeInfo Start; @@ -1371,6 +1795,11 @@ void FemMesh::read(const char *FileName) else if (File.hasExtension("inp") ) { // read Abaqus inp mesh file readAbaqus(File.filePath()); + + // if the file doesn't contain supported geometries try Nastran95 + SMESHDS_Mesh* meshds = this->myMesh->GetMeshDS(); + if (meshds->NbNodes() == 0) + readNastran95(File.filePath()); } else if (File.hasExtension("stl") ) { // read brep-file diff --git a/src/Mod/Fem/App/FemMesh.h b/src/Mod/Fem/App/FemMesh.h index a46c6b7777..ce1bd8d88f 100644 --- a/src/Mod/Fem/App/FemMesh.h +++ b/src/Mod/Fem/App/FemMesh.h @@ -169,6 +169,7 @@ public: private: void copyMeshData(const FemMesh&); void readNastran(const std::string &Filename); + void readNastran95(const std::string &Filename); void readZ88(const std::string &Filename); void readAbaqus(const std::string &Filename); diff --git a/src/Mod/Fem/femsolver/solverbase.py b/src/Mod/Fem/femsolver/solverbase.py index 384bfc8914..2edd5a096d 100644 --- a/src/Mod/Fem/femsolver/solverbase.py +++ b/src/Mod/Fem/femsolver/solverbase.py @@ -48,7 +48,7 @@ class Proxy(object): def __init__(self, obj): obj.Proxy = self - obj.addExtension("App::GroupExtensionPython", self) + obj.addExtension("App::GroupExtensionPython") def createMachine(self, obj, directory, testmode): raise NotImplementedError() @@ -78,7 +78,7 @@ class ViewProxy(object): def __init__(self, vobj): vobj.Proxy = self - vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") def setEdit(self, vobj, mode=0): try: diff --git a/src/Mod/Import/Gui/AppImportGuiPy.cpp b/src/Mod/Import/Gui/AppImportGuiPy.cpp index d6054176fe..e029d2db61 100644 --- a/src/Mod/Import/Gui/AppImportGuiPy.cpp +++ b/src/Mod/Import/Gui/AppImportGuiPy.cpp @@ -642,11 +642,10 @@ private: Base::FileInfo file(Utf8Name.c_str()); if (file.hasExtension("stp") || file.hasExtension("step")) { ParameterGrp::handle hGrp_stp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Part/STEP"); - std::string scheme = hGrp_stp->GetASCII("Scheme", "AP214IS"); - if (scheme == "AP203") - Interface_Static::SetCVal("write.step.schema", "AP203"); - else if (scheme == "AP214IS") - Interface_Static::SetCVal("write.step.schema", "AP214IS"); + std::string scheme = hGrp_stp->GetASCII("Scheme", Interface_Static::CVal("write.step.schema")); + std::list supported = Part::supportedSTEPSchemes(); + if (std::find(supported.begin(), supported.end(), scheme) != supported.end()) + Interface_Static::SetCVal("write.step.schema", scheme.c_str()); STEPCAFControl_Writer writer; Interface_Static::SetIVal("write.step.assembly",1); diff --git a/src/Mod/MeshPart/App/Mesher.cpp b/src/Mod/MeshPart/App/Mesher.cpp index ae31e09135..538b30486e 100644 --- a/src/Mod/MeshPart/App/Mesher.cpp +++ b/src/Mod/MeshPart/App/Mesher.cpp @@ -368,7 +368,7 @@ Mesh::MeshObject* Mesher::createMesh() const if (maxLen > 0) hyp2d->SetMaxSize(maxLen); if (minLen > 0) - hyp2d->SetMinSize(maxLen); + hyp2d->SetMinSize(minLen); hyp2d->SetQuadAllowed(allowquad); hyp2d->SetOptimize(optimize); diff --git a/src/Mod/OpenSCAD/OpenSCAD2Dgeom.py b/src/Mod/OpenSCAD/OpenSCAD2Dgeom.py index 378eea52fb..f65e59034b 100644 --- a/src/Mod/OpenSCAD/OpenSCAD2Dgeom.py +++ b/src/Mod/OpenSCAD/OpenSCAD2Dgeom.py @@ -500,7 +500,9 @@ def importDXFface(filename,layer=None,doc=None): #shapeobj.Document.removeObject(shapeobj.Name) #groupobj[0].Document.removeObject(groupobj[0].Name) for layer in layers: #remove everything that has been imported - layer.removeObjectsFromDocument() + removeOp = getattr(layer, "removeObjectsFromDocument", None) + if callable(removeOp): + layer.removeObjectsFromDocument() #for obj in layer.Group: # obj.Document.removeObject(obj.Name) layer.Document.removeObject(layer.Name) diff --git a/src/Mod/Part/App/ImportStep.h b/src/Mod/Part/App/ImportStep.h index f3dd1bdf6c..4d736cf0c1 100644 --- a/src/Mod/Part/App/ImportStep.h +++ b/src/Mod/Part/App/ImportStep.h @@ -41,7 +41,15 @@ namespace Part */ PartExport int ImportStepParts(App::Document *pcDoc, const char* Name); - +inline std::list supportedSTEPSchemes() { + std::list schemes; + schemes.emplace_back("AP203"); + schemes.emplace_back("AP214CD"); + schemes.emplace_back("AP214DIS"); + schemes.emplace_back("AP214IS"); + schemes.emplace_back("AP242DIS"); + return schemes; +} } //namespace Part diff --git a/src/Mod/Part/App/PropertyGeometryList.cpp b/src/Mod/Part/App/PropertyGeometryList.cpp index 432d3610df..3061c3d60d 100644 --- a/src/Mod/Part/App/PropertyGeometryList.cpp +++ b/src/Mod/Part/App/PropertyGeometryList.cpp @@ -226,7 +226,7 @@ void PropertyGeometryList::Restore(Base::XMLReader &reader) reader.readEndElement("GeometryList"); // assignment - setValues(values); + setValues(std::move(values)); } App::Property *PropertyGeometryList::Copy(void) const diff --git a/src/Mod/Part/App/TopoShape.cpp b/src/Mod/Part/App/TopoShape.cpp index 847e0207e0..356f116e00 100644 --- a/src/Mod/Part/App/TopoShape.cpp +++ b/src/Mod/Part/App/TopoShape.cpp @@ -704,7 +704,10 @@ Base::Matrix4D TopoShape::getTransform(void) const return mtrx; } -void TopoShape::setPlacement(const Base::Placement& rclTrf) +/*! + * \obsolete + */ +void TopoShape::setShapePlacement(const Base::Placement& rclTrf) { const Base::Vector3d& pos = rclTrf.getPosition(); Base::Vector3d axis; @@ -718,7 +721,10 @@ void TopoShape::setPlacement(const Base::Placement& rclTrf) _Shape.Location(loc); } -Base::Placement TopoShape::getPlacemet(void) const +/*! + * \obsolete + */ +Base::Placement TopoShape::getShapePlacement(void) const { TopLoc_Location loc = _Shape.Location(); gp_Trsf trsf = loc.Transformation(); @@ -2431,7 +2437,7 @@ TopoDS_Shape TopoShape::makeLongHelix(Standard_Real pitch, Standard_Real height, Handle(Geom_Surface) surf; Standard_Boolean isCylinder; - if (angle < Precision::Confusion()) { // Cylindrical helix + if (std::fabs(angle) < Precision::Confusion()) { // Cylindrical helix if (radius < Precision::Confusion()) Standard_Failure::Raise("Radius of helix too small"); surf= new Geom_CylindricalSurface(cylAx2, radius); @@ -2439,8 +2445,6 @@ TopoDS_Shape TopoShape::makeLongHelix(Standard_Real pitch, Standard_Real height, } else { // Conical helix angle = Base::toRadians(angle); - if (angle < Precision::Confusion()) - Standard_Failure::Raise("Angle of helix too small"); surf = new Geom_ConicalSurface(gp_Ax3(cylAx2), angle, radius); isCylinder = false; } diff --git a/src/Mod/Part/App/TopoShape.h b/src/Mod/Part/App/TopoShape.h index 2ef6027191..4fd7a5b623 100644 --- a/src/Mod/Part/App/TopoShape.h +++ b/src/Mod/Part/App/TopoShape.h @@ -110,11 +110,11 @@ public: /// set the transformation of the CasCade Shape void setTransform(const Base::Matrix4D& rclTrf); /// set the transformation of the CasCade Shape - void setPlacement(const Base::Placement& rclTrf); + void setShapePlacement(const Base::Placement& rclTrf); + /// get the transformation of the CasCade Shape + Base::Placement getShapePlacement(void) const; /// get the transformation of the CasCade Shape Base::Matrix4D getTransform(void) const; - /// get the transformation of the CasCade Shape - Base::Placement getPlacemet(void) const; /// Bound box from the CasCade shape Base::BoundBox3d getBoundBox(void)const; virtual bool getCenterOfGravity(Base::Vector3d& center) const; diff --git a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py index 7950ddba8d..cd33e428a5 100644 --- a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py +++ b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py @@ -224,7 +224,7 @@ class AttachmentEditorTaskPanel(FrozenClass): import os self.form=uic.loadUi(os.path.dirname(__file__) + os.path.sep + 'TaskAttachmentEditor.ui') - self.form.setWindowIcon(QtGui.QIcon(':/icons/Part_Attachment.svg')) + self.form.setWindowIcon(QtGui.QIcon(':/icons/tools/Part_Attachment.svg')) self.form.setWindowTitle(_translate('AttachmentEditor',"Attachment",None)) self.form.attachmentOffsetX.setProperty("unit", "mm") diff --git a/src/Mod/Part/BasicShapes/Shapes.py b/src/Mod/Part/BasicShapes/Shapes.py index a8140b6074..b4bbb5167a 100644 --- a/src/Mod/Part/BasicShapes/Shapes.py +++ b/src/Mod/Part/BasicShapes/Shapes.py @@ -43,7 +43,7 @@ class TubeFeature: obj.addProperty("App::PropertyLength","OuterRadius","Tube","Outer radius").OuterRadius = 5.0 obj.addProperty("App::PropertyLength","InnerRadius","Tube","Inner radius").InnerRadius = 2.0 obj.addProperty("App::PropertyLength","Height","Tube", "Height of the tube").Height = 10.0 - obj.addExtension("Part::AttachExtensionPython", self) + obj.addExtension("Part::AttachExtensionPython") def execute(self, fp): if fp.InnerRadius >= fp.OuterRadius: diff --git a/src/Mod/Part/BasicShapes/ViewProviderShapes.py b/src/Mod/Part/BasicShapes/ViewProviderShapes.py index 2933eca80a..881df6c988 100644 --- a/src/Mod/Part/BasicShapes/ViewProviderShapes.py +++ b/src/Mod/Part/BasicShapes/ViewProviderShapes.py @@ -36,7 +36,7 @@ class ViewProviderTube: def __init__(self, obj): ''' Set this object to the proxy object of the actual view provider ''' obj.Proxy = self - obj.addExtension("PartGui::ViewProviderAttachExtensionPython", self) + obj.addExtension("PartGui::ViewProviderAttachExtensionPython") obj.setIgnoreOverlayIcon(True, "PartGui::ViewProviderAttachExtensionPython") def attach(self, obj): diff --git a/src/Mod/Part/Gui/CrossSections.cpp b/src/Mod/Part/Gui/CrossSections.cpp index 0703dfb275..ab9adb6631 100644 --- a/src/Mod/Part/Gui/CrossSections.cpp +++ b/src/Mod/Part/Gui/CrossSections.cpp @@ -124,9 +124,10 @@ private: } CrossSections::CrossSections(const Base::BoundBox3d& bb, QWidget* parent, Qt::WindowFlags fl) - : QDialog(parent, fl), bbox(bb) + : QDialog(parent, fl) + , ui(new Ui_CrossSections) + , bbox(bb) { - ui = new Ui_CrossSections(); ui->setupUi(this); ui->position->setRange(-DBL_MAX, DBL_MAX); ui->position->setUnit(Base::Unit::Length); @@ -151,7 +152,6 @@ CrossSections::CrossSections(const Base::BoundBox3d& bb, QWidget* parent, Qt::Wi CrossSections::~CrossSections() { // no need to delete child widgets, Qt does it all for us - delete ui; if (view) { view->getViewer()->removeViewProvider(vp); } diff --git a/src/Mod/Part/Gui/CrossSections.h b/src/Mod/Part/Gui/CrossSections.h index d73947bea8..089b0bd012 100644 --- a/src/Mod/Part/Gui/CrossSections.h +++ b/src/Mod/Part/Gui/CrossSections.h @@ -71,7 +71,7 @@ private: Plane plane() const; private: - Ui_CrossSections* ui; + std::unique_ptr ui; Base::BoundBox3d bbox; ViewProviderCrossSections* vp; QPointer view; diff --git a/src/Mod/Part/Gui/DlgBooleanOperation.cpp b/src/Mod/Part/Gui/DlgBooleanOperation.cpp index 5a0b478ee0..027a822bb1 100644 --- a/src/Mod/Part/Gui/DlgBooleanOperation.cpp +++ b/src/Mod/Part/Gui/DlgBooleanOperation.cpp @@ -102,7 +102,6 @@ DlgBooleanOperation::DlgBooleanOperation(QWidget* parent) DlgBooleanOperation::~DlgBooleanOperation() { // no need to delete child widgets, Qt does it all for us - delete ui; this->connectNewObject.disconnect(); this->connectModObject.disconnect(); } diff --git a/src/Mod/Part/Gui/DlgBooleanOperation.h b/src/Mod/Part/Gui/DlgBooleanOperation.h index 0a5a624a8c..53c915cce4 100644 --- a/src/Mod/Part/Gui/DlgBooleanOperation.h +++ b/src/Mod/Part/Gui/DlgBooleanOperation.h @@ -63,7 +63,7 @@ private Q_SLOTS: void currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*); private: - Ui_DlgBooleanOperation* ui; + std::unique_ptr ui; Connection connectNewObject; Connection connectModObject; std::list observe; diff --git a/src/Mod/Part/Gui/DlgExtrusion.cpp b/src/Mod/Part/Gui/DlgExtrusion.cpp index 2ebc8bc113..1982aa7b42 100644 --- a/src/Mod/Part/Gui/DlgExtrusion.cpp +++ b/src/Mod/Part/Gui/DlgExtrusion.cpp @@ -139,7 +139,6 @@ DlgExtrusion::~DlgExtrusion() } // no need to delete child widgets, Qt does it all for us - delete ui; } void DlgExtrusion::changeEvent(QEvent *e) diff --git a/src/Mod/Part/Gui/DlgExtrusion.h b/src/Mod/Part/Gui/DlgExtrusion.h index d1db3d860e..a4baae7238 100644 --- a/src/Mod/Part/Gui/DlgExtrusion.h +++ b/src/Mod/Part/Gui/DlgExtrusion.h @@ -91,7 +91,7 @@ private: void autoSolid(); private: - Ui_DlgExtrusion* ui; + std::unique_ptr ui; std::string document, label; class EdgeSelection; EdgeSelection* filter; diff --git a/src/Mod/Part/Gui/DlgImportExportStep.ui b/src/Mod/Part/Gui/DlgImportExportStep.ui index 915713a7d2..da3d331cc7 100644 --- a/src/Mod/Part/Gui/DlgImportExportStep.ui +++ b/src/Mod/Part/Gui/DlgImportExportStep.ui @@ -7,7 +7,7 @@ 0 0 445 - 637 + 699 @@ -27,20 +27,32 @@ - - - AP 203 - - - true - - - - - - - AP 214 - + + + + AP203 + + + + + AP214 Committee Draft + + + + + AP214 Draft International Standard + + + + + AP214 International Standard + + + + + AP242 Draft International Standard + + @@ -113,12 +125,12 @@ Use legacy exporter - - Mod/Import - ExportLegacy + + Mod/Import + @@ -393,8 +405,6 @@ during file reading (slower but higher details). comboBoxUnits checkBoxPcurves checkBoxExportHiddenObj - radioButtonAP203 - radioButtonAP214 checkBoxMergeCompound checkBoxUseLinkGroup checkBoxImportHiddenObj diff --git a/src/Mod/Part/Gui/DlgRevolution.cpp b/src/Mod/Part/Gui/DlgRevolution.cpp index 86b2b14a73..a30ac5a1d3 100644 --- a/src/Mod/Part/Gui/DlgRevolution.cpp +++ b/src/Mod/Part/Gui/DlgRevolution.cpp @@ -101,10 +101,10 @@ public: }; DlgRevolution::DlgRevolution(QWidget* parent, Qt::WindowFlags fl) - : QDialog(parent, fl), filter(0) + : QDialog(parent, fl) + , ui(new Ui_DlgRevolution) + , filter(0) { - ui = new Ui_DlgRevolution(); - ui->setupUi(this); ui->xPos->setRange(-DBL_MAX,DBL_MAX); @@ -141,7 +141,6 @@ DlgRevolution::~DlgRevolution() { // no need to delete child widgets, Qt does it all for us Gui::Selection().rmvSelectionGate(); - delete ui; } Base::Vector3d DlgRevolution::getDirection() const diff --git a/src/Mod/Part/Gui/DlgRevolution.h b/src/Mod/Part/Gui/DlgRevolution.h index 2a8bf2bc8b..9151088aef 100644 --- a/src/Mod/Part/Gui/DlgRevolution.h +++ b/src/Mod/Part/Gui/DlgRevolution.h @@ -76,7 +76,7 @@ private: void autoSolid(); private: - Ui_DlgRevolution* ui; + std::unique_ptr ui; class EdgeSelection; EdgeSelection* filter; }; diff --git a/src/Mod/Part/Gui/DlgSettingsGeneral.cpp b/src/Mod/Part/Gui/DlgSettingsGeneral.cpp index 783f954c2f..fbd6762478 100644 --- a/src/Mod/Part/Gui/DlgSettingsGeneral.cpp +++ b/src/Mod/Part/Gui/DlgSettingsGeneral.cpp @@ -40,9 +40,8 @@ using namespace PartGui; DlgSettingsGeneral::DlgSettingsGeneral(QWidget* parent) - : PreferencePage(parent) + : PreferencePage(parent), ui(new Ui_DlgSettingsGeneral) { - ui = new Ui_DlgSettingsGeneral(); ui->setupUi(this); } @@ -52,7 +51,6 @@ DlgSettingsGeneral::DlgSettingsGeneral(QWidget* parent) DlgSettingsGeneral::~DlgSettingsGeneral() { // no need to delete child widgets, Qt does it all for us - delete ui; } void DlgSettingsGeneral::saveSettings() @@ -87,9 +85,8 @@ void DlgSettingsGeneral::changeEvent(QEvent *e) // ---------------------------------------------------------------------------- DlgImportExportIges::DlgImportExportIges(QWidget* parent) - : PreferencePage(parent) + : PreferencePage(parent), ui(new Ui_DlgImportExportIges) { - ui = new Ui_DlgImportExportIges(); ui->setupUi(this); ui->lineEditProduct->setReadOnly(true); @@ -113,7 +110,6 @@ DlgImportExportIges::DlgImportExportIges(QWidget* parent) DlgImportExportIges::~DlgImportExportIges() { // no need to delete child widgets, Qt does it all for us - delete ui; } void DlgImportExportIges::saveSettings() @@ -193,13 +189,19 @@ void DlgImportExportIges::changeEvent(QEvent *e) // ---------------------------------------------------------------------------- DlgImportExportStep::DlgImportExportStep(QWidget* parent) - : PreferencePage(parent) + : PreferencePage(parent), ui(new Ui_DlgImportExportStep) { - ui = new Ui_DlgImportExportStep(); ui->setupUi(this); + + ui->comboBoxSchema->setItemData(0, QByteArray("AP203")); + ui->comboBoxSchema->setItemData(1, QByteArray("AP214CD")); + ui->comboBoxSchema->setItemData(2, QByteArray("AP214DIS")); + ui->comboBoxSchema->setItemData(3, QByteArray("AP214IS")); + ui->comboBoxSchema->setItemData(4, QByteArray("AP242DIS")); + ui->lineEditProduct->setReadOnly(true); - ui->radioButtonAP203->setToolTip(tr("Configuration controlled 3D designs of mechanical parts and assemblies")); - ui->radioButtonAP214->setToolTip(tr("Core data for automotive mechanical design processes")); + //ui->radioButtonAP203->setToolTip(tr("Configuration controlled 3D designs of mechanical parts and assemblies")); + //ui->radioButtonAP214->setToolTip(tr("Core data for automotive mechanical design processes")); // https://tracker.dev.opencascade.org/view.php?id=25654 ui->checkBoxPcurves->setToolTip(tr("This parameter indicates whether parametric curves (curves in parametric space of surface)\n" @@ -222,7 +224,6 @@ DlgImportExportStep::DlgImportExportStep(QWidget* parent) DlgImportExportStep::~DlgImportExportStep() { // no need to delete child widgets, Qt does it all for us - delete ui; } void DlgImportExportStep::saveSettings() @@ -253,15 +254,10 @@ void DlgImportExportStep::saveSettings() } // scheme - if (ui->radioButtonAP203->isChecked()) { - Interface_Static::SetCVal("write.step.schema","AP203"); - hStepGrp->SetASCII("Scheme", "AP203"); - } - else { - // possible values: AP214CD (1996), AP214DIS (1998), AP214IS (2002) - Interface_Static::SetCVal("write.step.schema","AP214IS"); - hStepGrp->SetASCII("Scheme", "AP214IS"); - } + // possible values: AP214CD (1996), AP214DIS (1998), AP214IS (2002), AP242DIS + QByteArray schema = ui->comboBoxSchema->itemData(ui->comboBoxSchema->currentIndex()).toByteArray(); + Interface_Static::SetCVal("write.step.schema",schema); + hStepGrp->SetASCII("Scheme", schema); // header info hStepGrp->SetASCII("Company", ui->lineEditCompany->text().toLatin1()); @@ -299,12 +295,10 @@ void DlgImportExportStep::loadSettings() ui->comboBoxUnits->setCurrentIndex(unit); // scheme - QString ap = QString::fromStdString(hStepGrp->GetASCII("Scheme", - Interface_Static::CVal("write.step.schema"))); - if (ap.startsWith(QLatin1String("AP203"))) - ui->radioButtonAP203->setChecked(true); - else - ui->radioButtonAP214->setChecked(true); + QByteArray ap(hStepGrp->GetASCII("Scheme", Interface_Static::CVal("write.step.schema")).c_str()); + int index = ui->comboBoxSchema->findData(QVariant(ap)); + if (index >= 0) + ui->comboBoxSchema->setCurrentIndex(index); // header info ui->lineEditCompany->setText(QString::fromStdString(hStepGrp->GetASCII("Company"))); diff --git a/src/Mod/Part/Gui/DlgSettingsGeneral.h b/src/Mod/Part/Gui/DlgSettingsGeneral.h index fc995cce3f..0054d24f16 100644 --- a/src/Mod/Part/Gui/DlgSettingsGeneral.h +++ b/src/Mod/Part/Gui/DlgSettingsGeneral.h @@ -45,7 +45,7 @@ protected: void changeEvent(QEvent *e); private: - Ui_DlgSettingsGeneral* ui; + std::unique_ptr ui; }; class Ui_DlgImportExportIges; @@ -63,7 +63,7 @@ protected: void changeEvent(QEvent *e); private: - Ui_DlgImportExportIges* ui; + std::unique_ptr ui; QButtonGroup* bg; }; @@ -82,7 +82,7 @@ protected: void changeEvent(QEvent *e); private: - Ui_DlgImportExportStep* ui; + std::unique_ptr ui; }; } // namespace Gui diff --git a/src/Mod/Part/Gui/Mirroring.cpp b/src/Mod/Part/Gui/Mirroring.cpp index 94beb847b2..14f413f199 100644 --- a/src/Mod/Part/Gui/Mirroring.cpp +++ b/src/Mod/Part/Gui/Mirroring.cpp @@ -77,7 +77,6 @@ Mirroring::Mirroring(QWidget* parent) Mirroring::~Mirroring() { // no need to delete child widgets, Qt does it all for us - delete ui; } void Mirroring::changeEvent(QEvent *e) diff --git a/src/Mod/Part/Gui/Mirroring.h b/src/Mod/Part/Gui/Mirroring.h index 2f8cad91e9..768704b450 100644 --- a/src/Mod/Part/Gui/Mirroring.h +++ b/src/Mod/Part/Gui/Mirroring.h @@ -52,7 +52,7 @@ private: private: QString document; - Ui_Mirroring* ui; + std::unique_ptr ui; }; class TaskMirroring : public Gui::TaskView::TaskDialog diff --git a/src/Mod/Part/Gui/TaskAttacher.cpp b/src/Mod/Part/Gui/TaskAttacher.cpp index 682443e34d..fcb46fddef 100644 --- a/src/Mod/Part/Gui/TaskAttacher.cpp +++ b/src/Mod/Part/Gui/TaskAttacher.cpp @@ -111,10 +111,11 @@ void TaskAttacher::makeRefStrings(std::vector& refstrings, std::vector< TaskAttacher::TaskAttacher(Gui::ViewProviderDocumentObject *ViewProvider, QWidget *parent, QString picture, QString text, TaskAttacher::VisibilityFunction visFunc) - : TaskBox(Gui::BitmapFactory().pixmap(picture.toLatin1()), text, true, parent), - SelectionObserver(ViewProvider), - ViewProvider(ViewProvider), - visibilityFunc(visFunc) + : TaskBox(Gui::BitmapFactory().pixmap(picture.toLatin1()), text, true, parent) + , SelectionObserver(ViewProvider) + , ViewProvider(ViewProvider) + , ui(new Ui_TaskAttacher) + , visibilityFunc(visFunc) { //check if we are attachable if (!ViewProvider->getObject()->hasExtension(Part::AttachExtension::getExtensionClassTypeId())) @@ -122,7 +123,6 @@ TaskAttacher::TaskAttacher(Gui::ViewProviderDocumentObject *ViewProvider, QWidge // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskAttacher(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -235,7 +235,6 @@ TaskAttacher::~TaskAttacher() connectDelObject.disconnect(); connectDelDocument.disconnect(); - delete ui; } void TaskAttacher::objectDeleted(const Gui::ViewProviderDocumentObject& view) diff --git a/src/Mod/Part/Gui/TaskAttacher.h b/src/Mod/Part/Gui/TaskAttacher.h index ee1a4a3abc..bdbaa9ad90 100644 --- a/src/Mod/Part/Gui/TaskAttacher.h +++ b/src/Mod/Part/Gui/TaskAttacher.h @@ -130,7 +130,7 @@ protected: private: QWidget* proxy; - Ui_TaskAttacher* ui; + std::unique_ptr ui; VisibilityFunction visibilityFunc; // TODO fix documentation here (2015-11-10, Fat-Zer) diff --git a/src/Mod/Part/Gui/ViewProviderExt.cpp b/src/Mod/Part/Gui/ViewProviderExt.cpp index 909049fdbf..5dcfd97135 100644 --- a/src/Mod/Part/Gui/ViewProviderExt.cpp +++ b/src/Mod/Part/Gui/ViewProviderExt.cpp @@ -1133,7 +1133,10 @@ void ViewProviderPartExt::updateVisual() const TopoDS_Face &actFace = TopoDS::Face(faceMap(i)); // get the mesh of the shape Handle (Poly_Triangulation) mesh = BRep_Tool::Triangulation(actFace,aLoc); - if (mesh.IsNull()) continue; + if (mesh.IsNull()) { + parts[ii] = 0; + continue; + } // getting the transformation of the shape/face gp_Trsf myTransf; diff --git a/src/Mod/PartDesign/App/AppPartDesign.cpp b/src/Mod/PartDesign/App/AppPartDesign.cpp index 3cae7878dd..ce39347034 100644 --- a/src/Mod/PartDesign/App/AppPartDesign.cpp +++ b/src/Mod/PartDesign/App/AppPartDesign.cpp @@ -59,6 +59,7 @@ #include "FeatureLoft.h" #include "ShapeBinder.h" #include "FeatureBase.h" +#include "FeatureHelix.h" namespace PartDesign { extern PyObject* initModule(); @@ -116,6 +117,9 @@ PyMOD_INIT_FUNC(_PartDesign) PartDesign::Loft ::init(); PartDesign::AdditiveLoft ::init(); PartDesign::SubtractiveLoft ::init(); + PartDesign::Helix ::init(); + PartDesign::AdditiveHelix ::init(); + PartDesign::SubtractiveHelix ::init(); PartDesign::ShapeBinder ::init(); PartDesign::SubShapeBinder ::init(); PartDesign::Plane ::init(); diff --git a/src/Mod/PartDesign/App/Body.cpp b/src/Mod/PartDesign/App/Body.cpp index d29e5b90d6..face2936eb 100644 --- a/src/Mod/PartDesign/App/Body.cpp +++ b/src/Mod/PartDesign/App/Body.cpp @@ -140,7 +140,7 @@ App::DocumentObject* Body::getPrevSolidFeature(App::DocumentObject *start) if (rvIt != features.rend()) { // the solid found in model list return *rvIt; } - + return nullptr; } @@ -197,7 +197,7 @@ bool Body::isMemberOfMultiTransform(const App::DocumentObject* f) // This can be recognized because the Originals property is empty (it is contained // in the MultiTransform instead) // COMMENT ON THE COMMENT: - // This is wrong because at the creation (addObject) and before assigning the originals, that + // This is wrong because at the creation (addObject) and before assigning the originals, that // is when this code is executed, the originals property is indeed empty. // // However, for the purpose of setting the base feature, the transform feature has been modified @@ -244,7 +244,7 @@ Body* Body::findBodyOf(const App::DocumentObject* feature) { if(!feature) return nullptr; - + return static_cast(BodyBase::findBodyOf(feature)); } @@ -253,15 +253,15 @@ std::vector Body::addObject(App::DocumentObject *feature) { if(!isAllowed(feature)) throw Base::ValueError("Body: object is not allowed"); - + //TODO: features should not add all links - + //only one group per object. If it is in a body the single feature will be removed auto *group = App::GroupExtension::getGroupOfObject(feature); if(group && group != getExtendedObject()) group->getExtensionByType()->removeObject(feature); - - + + insertObject (feature, getNextSolidFeature (), /*after = */ false); // Move the Tip if we added a solid if (isSolidFeature(feature)) { @@ -272,22 +272,22 @@ std::vector Body::addObject(App::DocumentObject *feature) && feature->isDerivedFrom(PartDesign::Feature::getClassTypeId())) { for(auto obj : Group.getValues()) { - if(obj->Visibility.getValue() - && obj!=feature + if(obj->Visibility.getValue() + && obj!=feature && obj->isDerivedFrom(PartDesign::Feature::getClassTypeId())) obj->Visibility.setValue(false); } } - + std::vector result = {feature}; return result; } std::vector< App::DocumentObject* > Body::addObjects(std::vector< App::DocumentObject* > objs) { - + for(auto obj : objs) addObject(obj); - + return objs; } @@ -298,7 +298,7 @@ void Body::insertObject(App::DocumentObject* feature, App::DocumentObject* targe if (target && !hasObject (target)) { throw Base::ValueError("Body: the feature we should insert relative to is not part of that body"); } - + //ensure that all origin links are ok relinkToOrigin(feature); @@ -445,7 +445,7 @@ void Body::onSettingDocument() { void Body::onChanged (const App::Property* prop) { // we neither load a project nor perform undo/redo - if (!this->isRestoring() + if (!this->isRestoring() && this->getDocument() && !this->getDocument()->isPerformingTransaction()) { if (prop == &BaseFeature) { @@ -506,7 +506,7 @@ std::vector Body::getSubObjects(int reason) const { return {}; } -App::DocumentObject *Body::getSubObject(const char *subname, +App::DocumentObject *Body::getSubObject(const char *subname, PyObject **pyObj, Base::Matrix4D *pmat, bool transform, int depth) const { #if 1 @@ -525,8 +525,8 @@ App::DocumentObject *Body::getSubObject(const char *subname, // We return the shape only if there are feature visible inside for(auto obj : Group.getValues()) { - if(obj->Visibility.getValue() && - obj->isDerivedFrom(PartDesign::Feature::getClassTypeId())) + if(obj->Visibility.getValue() && + obj->isDerivedFrom(PartDesign::Feature::getClassTypeId())) { return Part::BodyBase::getSubObject(subname,pyObj,pmat,transform,depth); } @@ -546,3 +546,14 @@ void Body::onDocumentRestored() _GroupTouched.setStatus(App::Property::Output,true); DocumentObject::onDocumentRestored(); } + +// a body is solid if it has features that are solid +bool Body::isSolid() +{ + std::vector features = getFullModel(); + for (auto it = features.begin(); it!=features.end(); ++it){ + if (isSolidFeature((*it))) + return true; + } + return false; +} diff --git a/src/Mod/PartDesign/App/Body.h b/src/Mod/PartDesign/App/Body.h index 3ba7bfbbc7..d26852987d 100644 --- a/src/Mod/PartDesign/App/Body.h +++ b/src/Mod/PartDesign/App/Body.h @@ -118,7 +118,7 @@ public: PyObject *getPyObject(void) override; virtual std::vector getSubObjects(int reason=0) const override; - virtual App::DocumentObject *getSubObject(const char *subname, + virtual App::DocumentObject *getSubObject(const char *subname, PyObject **pyObj, Base::Matrix4D *pmat, bool transform, int depth) const override; void setShowTip(bool enable) { @@ -137,6 +137,9 @@ public: */ App::DocumentObject *getNextSolidFeature(App::DocumentObject* start = NULL); + // a body is solid if it has features that are solid according to member isSolidFeature. + bool isSolid(void); + protected: virtual void onSettingDocument() override; diff --git a/src/Mod/PartDesign/App/CMakeLists.txt b/src/Mod/PartDesign/App/CMakeLists.txt index 112a4ab12b..a623563930 100644 --- a/src/Mod/PartDesign/App/CMakeLists.txt +++ b/src/Mod/PartDesign/App/CMakeLists.txt @@ -109,6 +109,8 @@ SET(FeaturesSketchBased_SRCS FeaturePipe.cpp FeatureLoft.h FeatureLoft.cpp + FeatureHelix.h + FeatureHelix.cpp ) SOURCE_GROUP("SketchBasedFeatures" FILES ${FeaturesSketchBased_SRCS}) diff --git a/src/Mod/PartDesign/App/FeatureHelix.cpp b/src/Mod/PartDesign/App/FeatureHelix.cpp new file mode 100644 index 0000000000..b571df9a78 --- /dev/null +++ b/src/Mod/PartDesign/App/FeatureHelix.cpp @@ -0,0 +1,536 @@ +/*************************************************************************** + * Copyright (c) 2010 Juergen Riegel * + * 2020 David Österberg * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" +#ifndef _PreComp_ +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#endif + +# include +# include +# include +# include +# include +# include + +# include +# include + +# include "FeatureHelix.h" + +const double PI = 3.14159265359; + +using namespace PartDesign; + +const char* Helix::ModeEnums[] = {"pitch-height", "pitch-turns", "height-turns", NULL}; + +PROPERTY_SOURCE(PartDesign::Helix, PartDesign::ProfileBased) + +Helix::Helix() +{ + addSubType = FeatureAddSub::Additive; + + ADD_PROPERTY_TYPE(Base,(Base::Vector3d(0.0,0.0,0.0)),"Helix", App::Prop_ReadOnly, "Base"); + ADD_PROPERTY_TYPE(Axis,(Base::Vector3d(0.0,1.0,0.0)),"Helix", App::Prop_ReadOnly, "Axis"); + ADD_PROPERTY_TYPE(Pitch,(10.),"Helix", App::Prop_None, "Pitch"); + ADD_PROPERTY_TYPE(Height,(30.0),"Helix", App::Prop_None, "Height"); + ADD_PROPERTY_TYPE(Turns,(3.0),"Helix", App::Prop_None, "Turns"); + ADD_PROPERTY_TYPE(LeftHanded,(long(0)),"Helix", App::Prop_None, "LeftHanded"); + ADD_PROPERTY_TYPE(Reversed,(long(0)),"Helix", App::Prop_None, "Reversed"); + ADD_PROPERTY_TYPE(Angle,(0.0),"Helix", App::Prop_None, "Angle"); + ADD_PROPERTY_TYPE(ReferenceAxis,(0),"Helix", App::Prop_None, "Reference axis of revolution"); + ADD_PROPERTY_TYPE(Mode, (long(0)), "Helix", App::Prop_None, "Helix input mode"); + ADD_PROPERTY_TYPE(Outside,(long(0)),"Helix", App::Prop_None, "Outside"); + ADD_PROPERTY_TYPE(HasBeenEdited,(long(0)),"Helix", App::Prop_None, "HasBeenEdited"); + Mode.setEnums(ModeEnums); + +} + +short Helix::mustExecute() const +{ + if (Placement.isTouched() || + ReferenceAxis.isTouched() || + Axis.isTouched() || + Base.isTouched() || + Angle.isTouched()) + return 1; + return ProfileBased::mustExecute(); +} + +App::DocumentObjectExecReturn *Helix::execute(void) +{ + // Validate and normalize parameters + switch (Mode.getValue()) { + case 0: // pitch - height + if (Pitch.getValue() < Precision::Confusion()) + return new App::DocumentObjectExecReturn("Error: Pitch too small"); + if (Height.getValue() < Precision::Confusion()) + return new App::DocumentObjectExecReturn("Error: height too small!"); + break; + case 1: // pitch - turns + if (Pitch.getValue() < Precision::Confusion()) + return new App::DocumentObjectExecReturn("Error: pitch too small!"); + if (Turns.getValue() < Precision::Confusion()) + return new App::DocumentObjectExecReturn("Error: turns too small!"); + Height.setValue(Turns.getValue()*Pitch.getValue()); + break; + case 2: // height - turns + if (Height.getValue() < Precision::Confusion()) + return new App::DocumentObjectExecReturn("Error: height too small!"); + if (Turns.getValue() < Precision::Confusion()) + return new App::DocumentObjectExecReturn("Error turns too small!"); + Pitch.setValue(Height.getValue()/Turns.getValue()); + break; + default: + return new App::DocumentObjectExecReturn("Error: unsupported mode"); + } + + TopoDS_Shape sketchshape; + try { + sketchshape = getVerifiedFace(); + } catch (const Base::Exception& e) { + return new App::DocumentObjectExecReturn(e.what()); + } + + if (sketchshape.IsNull()) + return new App::DocumentObjectExecReturn("Error: No valid sketch or face"); + else { + //TODO: currently we only allow planar faces. the reason for this is that with other faces in front, we could + //not use the current simulate approach and build the start and end face from the wires. As the shell + //begins always at the spine and not the profile, the sketchshape cannot be used directly as front face. + //We would need a method to translate the front shape to match the shell starting position somehow... + TopoDS_Face face = TopoDS::Face(sketchshape); + BRepAdaptor_Surface adapt(face); + if(adapt.GetType() != GeomAbs_Plane) + return new App::DocumentObjectExecReturn("Error: Face must be planar"); + } + + // if the Base property has a valid shape, fuse the AddShape into it + TopoDS_Shape base; + try { + base = getBaseShape(); + } catch (const Base::Exception&) { + // fall back to support (for legacy features) + base = TopoDS_Shape(); + } + + + // update Axis from ReferenceAxis + try { + updateAxis(); + } catch (const Base::Exception& e) { + return new App::DocumentObjectExecReturn(e.what()); + } + + // get revolve axis + Base::Vector3d b = Base.getValue(); + gp_Pnt pnt(b.x,b.y,b.z); + Base::Vector3d v = Axis.getValue(); + gp_Dir dir(v.x,v.y,v.z); + + try { + this->positionByPrevious(); + TopLoc_Location invObjLoc = this->getLocation().Inverted(); + + base.Move(invObjLoc); + + // generate the helix path + TopoDS_Shape path = generateHelixPath(); + + // Below is basically a copy paste (with some simplification) from FeaturePipe.cpp Pipe::execute + // TODO: find a way to reduce code repetition. E.g can I rip out this functionality of Pipe:execute to a static helper + // function and call from here? + + std::vector wires; + try { + wires = getProfileWires(); + } catch (const Base::Exception& e) { + return new App::DocumentObjectExecReturn(e.what()); + } + + std::vector> wiresections; + for(TopoDS_Wire& wire : wires) + wiresections.emplace_back(1, wire); + + //maybe we need a scaling law + Handle(Law_Function) scalinglaw; + + //build all shells + std::vector shells; + std::vector frontwires, backwires; + for(std::vector& wires : wiresections) { + + BRepOffsetAPI_MakePipeShell mkPS(TopoDS::Wire(path)); + + mkPS.SetTolerance(Precision::Confusion()); + mkPS.SetTransitionMode(BRepBuilderAPI_Transformed); + + mkPS.SetMode(true); //This is for frenet + //mkPipeShell.SetMode(TopoDS::Wire(auxpath), true); // this is for two rails + + + if(!scalinglaw) { + for(TopoDS_Wire& wire : wires) { + wire.Move(invObjLoc); + mkPS.Add(wire); + } + } + else { + for(TopoDS_Wire& wire : wires) { + wire.Move(invObjLoc); + mkPS.SetLaw(wire, scalinglaw); + } + } + + if (!mkPS.IsReady()) + return new App::DocumentObjectExecReturn("Error: Could not build"); + + shells.push_back(mkPS.Shape()); + + + if (!mkPS.Shape().Closed()) { + // shell is not closed - use simulate to get the end wires + TopTools_ListOfShape sim; + mkPS.Simulate(2, sim); + + frontwires.push_back(TopoDS::Wire(sim.First())); + backwires.push_back(TopoDS::Wire(sim.Last())); + } + } + + BRepBuilderAPI_MakeSolid mkSolid; + + if (!frontwires.empty()) { + // build the end faces, sew the shell and build the final solid + TopoDS_Shape front = Part::FaceMakerCheese::makeFace(frontwires); + TopoDS_Shape back = Part::FaceMakerCheese::makeFace(backwires); + + BRepBuilderAPI_Sewing sewer; + sewer.SetTolerance(Precision::Confusion()); + sewer.Add(front); + sewer.Add(back); + + for(TopoDS_Shape& s : shells) + sewer.Add(s); + + sewer.Perform(); + mkSolid.Add(TopoDS::Shell(sewer.SewedShape())); + } else { + // shells are already closed - add them directly + for (TopoDS_Shape& s : shells) { + mkSolid.Add(TopoDS::Shell(s)); + } + } + + if(!mkSolid.IsDone()) + return new App::DocumentObjectExecReturn("Error: Result is not a solid"); + + TopoDS_Shape result = mkSolid.Shape(); + BRepClass3d_SolidClassifier SC(result); + SC.PerformInfinitePoint(Precision::Confusion()); + if (SC.State() == TopAbs_IN) + result.Reverse(); + + AddSubShape.setValue(result); + + + if(base.IsNull()) { + + if (getAddSubType() == FeatureAddSub::Subtractive) + return new App::DocumentObjectExecReturn("Error: There is nothing to subtract\n"); + + int solidCount = countSolids(result); + if (solidCount > 1) { + return new App::DocumentObjectExecReturn("Error: Result has multiple solids"); + } + Shape.setValue(getSolid(result)); + return App::DocumentObject::StdReturn; + } + + if(getAddSubType() == FeatureAddSub::Additive) { + + BRepAlgoAPI_Fuse mkFuse(base, result); + if (!mkFuse.IsDone()) + return new App::DocumentObjectExecReturn("Error: Adding the helix failed"); + // we have to get the solids (fuse sometimes creates compounds) + TopoDS_Shape boolOp = this->getSolid(mkFuse.Shape()); + // lets check if the result is a solid + if (boolOp.IsNull()) + return new App::DocumentObjectExecReturn("Error: Result is not a solid"); + + int solidCount = countSolids(boolOp); + if (solidCount > 1) { + return new App::DocumentObjectExecReturn("Error: Result has multiple solids"); + } + + boolOp = refineShapeIfActive(boolOp); + Shape.setValue(getSolid(boolOp)); + } + else if(getAddSubType() == FeatureAddSub::Subtractive) { + + TopoDS_Shape boolOp; + + if (Outside.getValue()) { // are we subtracting the inside or the outside of the profile. + BRepAlgoAPI_Common mkCom(result, base); + if (!mkCom.IsDone()) + return new App::DocumentObjectExecReturn("Error: Intersecting the helix failed"); + boolOp = this->getSolid(mkCom.Shape()); + + } else { + BRepAlgoAPI_Cut mkCut(base, result); + if (!mkCut.IsDone()) + return new App::DocumentObjectExecReturn("Error: Subtracting the helix failed"); + boolOp = this->getSolid(mkCut.Shape()); + } + + // lets check if the result is a solid + if (boolOp.IsNull()) + return new App::DocumentObjectExecReturn("Error: Result is not a solid"); + + int solidCount = countSolids(boolOp); + if (solidCount > 1) { + return new App::DocumentObjectExecReturn("Error: Result has multiple solids"); + } + + boolOp = refineShapeIfActive(boolOp); + Shape.setValue(getSolid(boolOp)); + } + + return App::DocumentObject::StdReturn; + } + catch (Standard_Failure& e) { + + if (std::string(e.GetMessageString()) == "TopoDS::Face") + return new App::DocumentObjectExecReturn("Error: Could not create face from sketch"); + else + return new App::DocumentObjectExecReturn(e.GetMessageString()); + } + catch (Base::Exception& e) { + return new App::DocumentObjectExecReturn(e.what()); + } +} + + + +void Helix::updateAxis(void) +{ + App::DocumentObject *pcReferenceAxis = ReferenceAxis.getValue(); + const std::vector &subReferenceAxis = ReferenceAxis.getSubValues(); + Base::Vector3d base; + Base::Vector3d dir; + getAxis(pcReferenceAxis, subReferenceAxis, base, dir, false); + + Base.setValue(base.x,base.y,base.z); + Axis.setValue(dir.x,dir.y,dir.z); +} + + +TopoDS_Shape Helix::generateHelixPath(void) +{ + double pitch = Pitch.getValue(); + double height = Height.getValue(); + bool leftHanded = LeftHanded.getValue(); + bool reversed = Reversed.getValue(); + double angle = Angle.getValue(); + if (angle < Precision::Confusion() && angle > -Precision::Confusion()) + angle = 0.0; + + // get revolve axis + Base::Vector3d b = Base.getValue(); + gp_Pnt pnt(b.x,b.y,b.z); + Base::Vector3d v = Axis.getValue(); + gp_Dir dir(v.x,v.y,v.z); + + Base::Vector3d normal = getProfileNormal(); + Base::Vector3d start = v.Cross(normal); // pointing towards the desired helix start point. + gp_Dir dir_start(start.x, start.y, start.z); + + // Find out in what quadrant relative to the axis the profile is located, and the exact position. + Base::Vector3d profileCenter = getProfileCenterPoint(); + double axisOffset = profileCenter*start - b*start; + double startOffset = profileCenter*v - b*v; + double radius = std::fabs(axisOffset); + bool turned = axisOffset < 0; + + if (radius < Precision::Confusion()) { + // in this case ensure that axis is not in the sketch plane + if (v*normal < Precision::Confusion()) + throw Base::ValueError("Error: Result is self intersecting"); + radius = 1.0; //fallback to radius 1 + startOffset = 0.0; + } + + //build the helix path + TopoShape helix = TopoShape().makeLongHelix(pitch, height, radius, angle, leftHanded); + TopoDS_Shape path = helix.getShape(); + + + /* + * The helix wire is created with the axis coinciding with z-axis and the start point at (radius, 0, 0) + * We want to move it so that the axis becomes aligned with "dir" and "pnt", we also want (radius,0,0) to + * map to the sketch plane. + */ + + gp_Pnt origo(0.0, 0.0, 0.0); + gp_Dir dir_axis1(0.0, 0.0, 1.0); // pointing along the helix axis, as created. + gp_Dir dir_axis2(1.0, 0.0, 0.0); // pointing towards the helix start point, as created. + + gp_Trsf mov; + + + if (reversed) { + mov.SetRotation(gp_Ax1(origo, dir_axis2), PI); + TopLoc_Location loc(mov); + path.Move(loc); + } + + if (abs(startOffset) > 0) { // translate the helix so that the starting point aligns with the profile + mov.SetTranslation(startOffset*gp_Vec(dir_axis1)); + TopLoc_Location loc(mov); + path.Move(loc); + } + + if (turned) { // turn the helix so that the starting point aligns with the profile + mov.SetRotation(gp_Ax1(origo, dir_axis1), PI); + TopLoc_Location loc(mov); + path.Move(loc); + } + + gp_Ax3 sourceCS(origo, dir_axis1, dir_axis2); + gp_Ax3 targetCS(pnt, dir, dir_start); + + mov.SetTransformation(sourceCS, targetCS); + TopLoc_Location loc(mov); + path.Move(loc.Inverted()); + + +# if OCC_VERSION_HEX < 0x70500 + /* I initially tried using path.Move(invObjLoc) like usual. But it does not give the right result + * The starting point of the helix is not correct and I don't know why! With below hack it works. + */ + Base::Vector3d placeAxis; + double placeAngle; + this->Placement.getValue().getRotation().getValue(placeAxis, placeAngle); + gp_Dir placeDir(placeAxis.x, placeAxis.y, placeAxis.z); + mov.SetRotation(gp_Ax1(origo, placeDir), placeAngle); + TopLoc_Location loc2(mov); + path.Move(loc2.Inverted()); +# else + TopLoc_Location invObjLoc = this->getLocation().Inverted(); + path.Move(invObjLoc); +# endif + + return path; +} + +// this function calculates self intersection safe pitch based on the profile bounding box. +double Helix::safePitch() +{ + // Below is an approximation. It is possible to do the general way by solving for the pitch + // where the helix is self intersecting. + + double angle = Angle.getValue()/180.0*PI; + + TopoDS_Shape sketchshape = getVerifiedFace(); + Bnd_Box bb; + BRepBndLib::Add(sketchshape, bb); + + double Xmin, Ymin, Zmin, Xmax, Ymax, Zmax; + bb.Get(Xmin, Ymin, Zmin, Xmax, Ymax, Zmax); + + double X = Xmax - Xmin, Y = Ymax - Ymin, Z = Zmax - Zmin; + + Base::Vector3d v = Axis.getValue(); + gp_Dir dir(v.x,v.y,v.z); + gp_Vec bbvec(X, Y, Z); + + double p0 = bbvec*dir; // safe pitch if angle=0 + + Base::Vector3d n = getProfileNormal(); + Base::Vector3d s = v.Cross(n); // pointing towards the desired helix start point. + gp_Dir dir_s(s.x, s.y, s.z); + + if (tan(abs(angle))*p0 > abs(bbvec*dir_s)) + return abs(bbvec*dir_s)/tan(abs(angle)); + else + return p0; +} + +// this function proposes pitch and height +void Helix::proposeParameters(bool force) +{ + if (force || !HasBeenEdited.getValue()) { + double pitch = 1.1*safePitch(); + Pitch.setValue(pitch); + Height.setValue(pitch*3.0); + HasBeenEdited.setValue(1); + } +} + +Base::Vector3d Helix::getProfileCenterPoint() +{ + TopoDS_Shape profileshape; + profileshape = getVerifiedFace(); + Bnd_Box box; + BRepBndLib::Add(profileshape, box); + box.SetGap(0.0); + double xmin, ymin, zmin, xmax, ymax, zmax; + box.Get(xmin, ymin, zmin, xmax, ymax, zmax); + return Base::Vector3d(0.5*(xmin+xmax), 0.5*(ymin+ymax), 0.5*(zmin+zmax)); +} + + +PROPERTY_SOURCE(PartDesign::AdditiveHelix, PartDesign::Helix) +AdditiveHelix::AdditiveHelix() { + addSubType = Additive; +} + +PROPERTY_SOURCE(PartDesign::SubtractiveHelix, PartDesign::Helix) +SubtractiveHelix::SubtractiveHelix() { + addSubType = Subtractive; +} diff --git a/src/Mod/PartDesign/App/FeatureHelix.h b/src/Mod/PartDesign/App/FeatureHelix.h new file mode 100644 index 0000000000..a623022f03 --- /dev/null +++ b/src/Mod/PartDesign/App/FeatureHelix.h @@ -0,0 +1,106 @@ +/*************************************************************************** + * Copyright (c) 2010 Juergen Riegel * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef PARTDESIGN_Helix_H +#define PARTDESIGN_Helix_H + +#include +#include "FeatureSketchBased.h" +#include + +namespace PartDesign +{ + +class PartDesignExport Helix : public ProfileBased +{ + PROPERTY_HEADER(PartDesign::Helix); + +public: + Helix(); + + App::PropertyVector Base; + App::PropertyVector Axis; + App::PropertyLength Pitch; + App::PropertyLength Height; + App::PropertyFloat Turns; + App::PropertyBool LeftHanded; + App::PropertyAngle Angle; + App::PropertyEnumeration Mode; + App::PropertyBool Outside; + App::PropertyBool HasBeenEdited; + + /** if this property is set to a valid link, both Axis and Base properties + * are calculated according to the linked line + */ + App::PropertyLinkSub ReferenceAxis; + + /** @name methods override feature */ + //@{ + App::DocumentObjectExecReturn *execute(void); + short mustExecute() const; + /// returns the type name of the view provider + const char* getViewProviderName(void) const { + return "PartDesignGui::ViewProviderHelix"; + } + //@} + + void proposeParameters(bool force = false); + double safePitch(void); + +protected: + /// updates Axis from ReferenceAxis + void updateAxis(void); + + /// generate helix and move it to the right location. + TopoDS_Shape generateHelixPath(void); + + // project shape on plane. Used for detecting self intersection. + TopoDS_Shape projectShape(const TopoDS_Shape& input, const gp_Ax2& plane); + + // center of profile bounding box + Base::Vector3d getProfileCenterPoint(); + +private: + static const char* ModeEnums[]; +}; + + +class PartDesignExport AdditiveHelix : public Helix { + + PROPERTY_HEADER(PartDesign::AdditiveHelix); +public: + AdditiveHelix(); +}; + + +class PartDesignExport SubtractiveHelix : public Helix { + + PROPERTY_HEADER(PartDesign::SubtractiveHelix); +public: + SubtractiveHelix(); +}; + +} //namespace PartDesign + + +#endif // PART_Helix_H diff --git a/src/Mod/PartDesign/App/FeatureHole.cpp b/src/Mod/PartDesign/App/FeatureHole.cpp index 1808d0b1e7..af43e512bf 100644 --- a/src/Mod/PartDesign/App/FeatureHole.cpp +++ b/src/Mod/PartDesign/App/FeatureHole.cpp @@ -71,7 +71,8 @@ namespace PartDesign { const char* Hole::DepthTypeEnums[] = { "Dimension", "ThroughAll", /*, "UpToFirst", */ NULL }; const char* Hole::ThreadTypeEnums[] = { "None", "ISOMetricProfile", "ISOMetricFineProfile", "UNC", "UNF", "UNEF", NULL}; -const char* Hole::ThreadFitEnums[] = { "Standard", "Close", "Wide", NULL}; +const char* Hole::ClearanceMetricEnums[] = { "Standard", "Close", "Wide", NULL}; +const char* Hole::ClearanceUTSEnums[] = { "Normal", "Close", "Loose", NULL }; const char* Hole::DrillPointEnums[] = { "Flat", "Angled", NULL}; /* "None" profile */ @@ -424,7 +425,9 @@ const double Hole::metricHoleDiameters[36][4] = { 6.0, 6.4, 6.6, 7.0}, { 7.0, 7.4, 7.6, 8.0}, { 8.0, 8.4, 9.0, 10.0}, + // 9.0 undefined { 10.0, 10.5, 11.0, 12.0}, + // 11.0 undefined { 12.0, 13.0, 13.5, 14.5}, { 14.0, 15.0, 15.5, 16.5}, { 16.0, 17.0, 17.5, 18.5}, @@ -447,6 +450,39 @@ const double Hole::metricHoleDiameters[36][4] = { 68.0, 70.0, 77.0, 78.0} }; +const Hole::UTSClearanceDefinition Hole::UTSHoleDiameters[22] = +{ + /* UTS clearance hole diameters according to ASME B18.2.8 */ + // for information: the norm defines a drill bit number (that is in turn standardized in another ASME norm). + // as result the norm defines a minimal clearance which is the diameter of that drill bit. + // we use here this minimal clearance as the theoretical exact hole diameter as this is also done in the ISO norm. + // {screw class, close, normal, loose} + { "#0", 1.7, 1.9, 2.4 }, + { "#1", 2.1, 2.3, 2.6 }, + { "#2", 2.4, 2.6, 2.9 }, + { "#3", 2.7, 2.9, 3.3 }, + { "#4", 3.0, 3.3, 3.7 }, + { "#5", 3.6, 4.0, 4.4 }, + { "#6", 3.9, 4.3, 4.7 }, + { "#8", 4.6, 5.0, 5.4 }, + { "#10", 5.2, 5.6, 6.0 }, + // "#12" not defined + { "1/4", 6.8, 7.1, 7.5 }, + { "5/16", 8.3, 8.7, 9.1 }, + { "3/8", 9.9, 10.3, 10.7 }, + { "7/16", 11.5, 11.9, 12.3 }, + { "1/2", 13.5, 14.3, 15.5 }, + // "9/16" not defined + { "5/8", 16.7, 17.5, 18.6 }, + { "3/4", 19.8, 20.6, 23.0 }, + { "7/8", 23.0, 23.8, 26.2 }, + { "1", 26.2, 27.8, 29.4 }, + { "1 1/8", 29.4, 31.0, 33.3 }, + { "1 1/4", 32.5, 34.1, 36.5 }, + { "1 3/8", 36.5, 38.1, 40.9 }, + { "1 1/2", 39.7, 41.3, 44.0 } +}; + /* ISO coarse metric enums */ std::vector Hole::HoleCutType_ISOmetric_Enums = { "None", "Counterbore", "Countersink", "Cheesehead (deprecated)", "Countersink socket screw (deprecated)", "Cap screw (deprecated)" }; const char* Hole::ThreadSize_ISOmetric_Enums[] = { "M1", "M1.1", "M1.2", "M1.4", "M1.6", @@ -564,8 +600,8 @@ Hole::Hole() ADD_PROPERTY_TYPE(ThreadClass, (0L), "Hole", App::Prop_None, "Thread class"); ThreadClass.setEnums(ThreadClass_None_Enums); - ADD_PROPERTY_TYPE(ThreadFit, (0L), "Hole", App::Prop_None, "Thread fit"); - ThreadFit.setEnums(ThreadFitEnums); + ADD_PROPERTY_TYPE(ThreadFit, (0L), "Hole", App::Prop_None, "Clearance hole fit"); + ThreadFit.setEnums(ClearanceMetricEnums); ADD_PROPERTY_TYPE(Diameter, (6.0), "Hole", App::Prop_None, "Diameter"); @@ -576,6 +612,8 @@ Hole::Hole() ADD_PROPERTY_TYPE(HoleCutType, (0L), "Hole", App::Prop_None, "Head cut type"); HoleCutType.setEnums(HoleCutType_None_Enums); + ADD_PROPERTY_TYPE(HoleCutCustomValues, (false), "Hole", App::Prop_None, "Custom cut values"); + ADD_PROPERTY_TYPE(HoleCutDiameter, (0.0), "Hole", App::Prop_None, "Head cut diameter"); ADD_PROPERTY_TYPE(HoleCutDepth, (0.0), "Hole", App::Prop_None, "Head cut deth"); @@ -591,6 +629,8 @@ Hole::Hole() DrillPoint.setEnums(DrillPointEnums); ADD_PROPERTY_TYPE(DrillPointAngle, (118.0), "Hole", App::Prop_None, "Drill point angle"); + ADD_PROPERTY_TYPE(DrillForDepth, ((long)0), "Hole", App::Prop_None, + "The size of the drill point will be taken into\n account for the depth of blind holes"); ADD_PROPERTY_TYPE(Tapered, (false),"Hole", App::Prop_None, "Tapered"); @@ -599,10 +639,10 @@ Hole::Hole() void Hole::updateHoleCutParams() { - std::string holeCutType = HoleCutType.getValueAsString(); + std::string holeCutTypeStr = HoleCutType.getValueAsString(); // there is no cut, thus return - if (holeCutType == "None") + if (holeCutTypeStr == "None") return; if (ThreadType.getValue() < 0) { @@ -610,73 +650,143 @@ void Hole::updateHoleCutParams() return; } - std::string threadType = ThreadType.getValueAsString(); - if (threadType == "ISOMetricProfile" || threadType == "ISOMetricFineProfile") { + // get diameter and size + double diameterVal = Diameter.getValue(); + + // handle thread types + std::string threadTypeStr = ThreadType.getValueAsString(); + if (threadTypeStr == "ISOMetricProfile" || threadTypeStr == "ISOMetricFineProfile") { if (ThreadSize.getValue() < 0) { throw Base::IndexError("Thread size out of range"); return; } - // get diameter and size - double diameter = threadDescription[ThreadType.getValue()][ThreadSize.getValue()].diameter; - std::string threadSize{ ThreadSize.getValueAsString() }; + std::string threadSizeStr = ThreadSize.getValueAsString(); // we don't update for these settings but we need to set a value for new holes + // furthermore we must assure the hole cut diameter is not <= the hole diameter // if we have a cut but the values are zero, we assume it is a new hole // we take in this case the values from the norm ISO 4762 or ISO 10642 - if (holeCutType == "Counterbore") { + if (holeCutTypeStr == "Counterbore") { // read ISO 4762 values - const CutDimensionSet& counter = find_cutDimensionSet(threadType, "ISO 4762"); - const CounterBoreDimension& dimen = counter.get_bore(threadSize); - if (HoleCutDiameter.getValue() == 0.0) { - HoleCutDiameter.setValue(dimen.diameter); - HoleCutDepth.setValue(dimen.depth); + const CutDimensionSet& counter = find_cutDimensionSet(threadTypeStr, "ISO 4762"); + const CounterBoreDimension& dimen = counter.get_bore(threadSizeStr); + if (HoleCutDiameter.getValue() == 0.0 || HoleCutDiameter.getValue() <= diameterVal) { + // there is no norm defining counterbores for all sizes, thus we need to use the + // same fallback as for the case HoleCutTypeMap.count(key) + if (dimen.diameter != 0.0) { + HoleCutDiameter.setValue(dimen.diameter); + HoleCutDepth.setValue(dimen.depth); + } + else { + // valid values for visual feedback + HoleCutDiameter.setValue(Diameter.getValue() + 0.1); + HoleCutDepth.setValue(0.1); + } } if (HoleCutDepth.getValue() == 0.0) HoleCutDepth.setValue(dimen.depth); + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + HoleCutCountersinkAngle.setReadOnly(true); } - else if (holeCutType == "Countersink") { + else if (holeCutTypeStr == "Countersink") { // read ISO 10642 values - const CutDimensionSet& counter = find_cutDimensionSet(threadType, "ISO 10642"); - if (HoleCutDiameter.getValue() == 0.0) { - const CounterSinkDimension& dimen = counter.get_sink(threadSize); - HoleCutDiameter.setValue(dimen.diameter); + const CutDimensionSet& counter = find_cutDimensionSet(threadTypeStr, "ISO 10642"); + if (HoleCutDiameter.getValue() == 0.0 || HoleCutDiameter.getValue() <= diameterVal) { + const CounterSinkDimension& dimen = counter.get_sink(threadSizeStr); + if (dimen.diameter != 0.0) { + HoleCutDiameter.setValue(dimen.diameter); + } + else { + HoleCutDiameter.setValue(Diameter.getValue() + 0.1); + } HoleCutCountersinkAngle.setValue(counter.angle); } if (HoleCutCountersinkAngle.getValue() == 0.0) { HoleCutCountersinkAngle.setValue(counter.angle); } + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + HoleCutCountersinkAngle.setReadOnly(false); } // cut definition - CutDimensionKey key { threadType, holeCutType }; + CutDimensionKey key { threadTypeStr, holeCutTypeStr }; if (HoleCutTypeMap.count(key)) { const CutDimensionSet &counter = find_cutDimensionSet(key); if (counter.cut_type == CutDimensionSet::Counterbore) { // disable HoleCutCountersinkAngle and reset it to ISO's default HoleCutCountersinkAngle.setValue(90.0); HoleCutCountersinkAngle.setReadOnly(true); - const CounterBoreDimension &dimen = counter.get_bore(threadSize); + const CounterBoreDimension &dimen = counter.get_bore(threadSizeStr); if (dimen.thread == "None") { // valid values for visual feedback HoleCutDiameter.setValue(Diameter.getValue() + 0.1); HoleCutDepth.setValue(0.1); + // we force custom values since there are no normed ones + HoleCutCustomValues.setReadOnly(true); + // important to set only if not already true, to avoid loop call of updateHoleCutParams() + if (!HoleCutCustomValues.getValue()) { + HoleCutCustomValues.setValue(true); + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + } } else { - HoleCutDiameter.setValue(dimen.diameter); - HoleCutDepth.setValue(dimen.depth); + // set normed values if not overwritten or if previously there + // were no normed values available and thus HoleCutCustomValues is checked and read-only + if (!HoleCutCustomValues.getValue() + || (HoleCutCustomValues.getValue() && HoleCutCustomValues.isReadOnly())) { + HoleCutDiameter.setValue(dimen.diameter); + HoleCutDepth.setValue(dimen.depth); + HoleCutDiameter.setReadOnly(true); + HoleCutDepth.setReadOnly(true); + if (HoleCutCustomValues.getValue() && HoleCutCustomValues.isReadOnly()) + HoleCutCustomValues.setValue(false); + } + else { + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + } + HoleCutCustomValues.setReadOnly(false); } } else if (counter.cut_type == CutDimensionSet::Countersink) { - const CounterSinkDimension &dimen = counter.get_sink(threadSize); + const CounterSinkDimension &dimen = counter.get_sink(threadSizeStr); if (dimen.thread == "None") { // valid values for visual feedback HoleCutDiameter.setValue(Diameter.getValue() + 0.1); // there might be an angle of zero (if no norm exists for the size) - if (HoleCutCountersinkAngle.getValue() == 0.0) { + if (HoleCutCountersinkAngle.getValue() == 0.0) { HoleCutCountersinkAngle.setValue(counter.angle); } + // we force custom values since there are no normed ones + HoleCutCustomValues.setReadOnly(true); + // important to set only if not already true, to avoid loop call of updateHoleCutParams() + if (!HoleCutCustomValues.getValue()) { + HoleCutCustomValues.setValue(true); + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + HoleCutCountersinkAngle.setReadOnly(false); + } } else { - HoleCutDiameter.setValue(dimen.diameter); - HoleCutCountersinkAngle.setValue(counter.angle); + // set normed values if not overwritten or if previously there + // were no normed values available and thus HoleCutCustomValues is checked and read-only + if (!HoleCutCustomValues.getValue() + || (HoleCutCustomValues.getValue() && HoleCutCustomValues.isReadOnly())) { + HoleCutDiameter.setValue(dimen.diameter); + HoleCutDiameter.setReadOnly(true); + HoleCutDepth.setReadOnly(true); + HoleCutCountersinkAngle.setValue(counter.angle); + HoleCutCountersinkAngle.setReadOnly(true); + if (HoleCutCustomValues.getValue() && HoleCutCustomValues.isReadOnly()) + HoleCutCustomValues.setValue(false); + } + else { + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + HoleCutCountersinkAngle.setReadOnly(false); + } + HoleCutCustomValues.setReadOnly(false); } } } @@ -685,52 +795,63 @@ void Hole::updateHoleCutParams() // user defined None, Counterbore and Countersink // handle legacy types but don’t change user settings for // user defined None, Counterbore and Countersink - else if (holeCutType == "Cheesehead (deprecated)") { - HoleCutDiameter.setValue(diameter * 1.6); - HoleCutDepth.setValue(diameter * 0.6); + else if (holeCutTypeStr == "Cheesehead (deprecated)") { + HoleCutDiameter.setValue(diameterVal * 1.6); + HoleCutDepth.setValue(diameterVal * 0.6); + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); } - else if (holeCutType == "Countersink socket screw (deprecated)") { - HoleCutDiameter.setValue(diameter * 2.0); - HoleCutDepth.setValue(diameter * 0.0); + else if (holeCutTypeStr == "Countersink socket screw (deprecated)") { + HoleCutDiameter.setValue(diameterVal * 2.0); + HoleCutDepth.setValue(diameterVal * 0.0); if (HoleCutCountersinkAngle.getValue() == 0.0) { HoleCutCountersinkAngle.setValue(90.0); } + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + HoleCutCountersinkAngle.setReadOnly(false); } - else if (holeCutType == "Cap screw (deprecated)") { - HoleCutDiameter.setValue(diameter * 1.5); - HoleCutDepth.setValue(diameter * 1.25); + else if (holeCutTypeStr == "Cap screw (deprecated)") { + HoleCutDiameter.setValue(diameterVal * 1.5); + HoleCutDepth.setValue(diameterVal * 1.25); + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); } } else { // we have an UTS profile or none - // get diameter - double diameter = threadDescription[ThreadType.getValue()][ThreadSize.getValue()].diameter; // we don't update for these settings but we need to set a value for new holes + // furthermore we must assure the hole cut diameter is not <= the hole diameter // if we have a cut but the values are zero, we assume it is a new hole // we use rules of thumbs as proposal - if (holeCutType == "Counterbore") { - if (HoleCutDiameter.getValue() == 0.0) { - HoleCutDiameter.setValue(diameter * 1.6); - HoleCutDepth.setValue(diameter * 0.9); + if (holeCutTypeStr == "Counterbore") { + if (HoleCutDiameter.getValue() == 0.0 || HoleCutDiameter.getValue() <= diameterVal) { + HoleCutDiameter.setValue(diameterVal * 1.6); + HoleCutDepth.setValue(diameterVal * 0.9); } if (HoleCutDepth.getValue() == 0.0) - HoleCutDepth.setValue(diameter * 0.9); + HoleCutDepth.setValue(diameterVal * 0.9); + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); } - else if (holeCutType == "Countersink") { - if (HoleCutDiameter.getValue() == 0.0) { - HoleCutDiameter.setValue(diameter * 1.7); + else if (holeCutTypeStr == "Countersink") { + if (HoleCutDiameter.getValue() == 0.0 || HoleCutDiameter.getValue() <= diameterVal) { + HoleCutDiameter.setValue(diameterVal * 1.7); // 82 degrees for UTS, 90 otherwise - if (threadType != "None") + if (threadTypeStr != "None") HoleCutCountersinkAngle.setValue(82.0); else HoleCutCountersinkAngle.setValue(90.0); } if (HoleCutCountersinkAngle.getValue() == 0.0) { - if (threadType != "None") + if (threadTypeStr != "None") HoleCutCountersinkAngle.setValue(82.0); else HoleCutCountersinkAngle.setValue(90.0); } + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + HoleCutCountersinkAngle.setReadOnly(false); } } } @@ -790,52 +911,130 @@ void Hole::updateDiameterParam() } else { // we have a clearance hole bool found = false; - int MatrixRowSize = sizeof(metricHoleDiameters) / sizeof(metricHoleDiameters[0]); - switch ( ThreadFit.getValue() ) { - case 0: /* standard fit */ - // read diameter out of matrix - for (int i = 0; i < MatrixRowSize; i++) { - if (metricHoleDiameters[i][0] == diameter) { - diameter = metricHoleDiameters[i][2]; - found = true; - break; + std::string threadType = ThreadType.getValueAsString(); + // UTS and metric have a different clearance hole set + if (threadType == "ISOMetricProfile" || threadType == "ISOMetricFineProfile") { + int MatrixRowSizeMetric = sizeof(metricHoleDiameters) / sizeof(metricHoleDiameters[0]); + switch (ThreadFit.getValue()) { + case 0: /* standard fit */ + // read diameter out of matrix + for (int i = 0; i < MatrixRowSizeMetric; i++) { + if (metricHoleDiameters[i][0] == diameter) { + diameter = metricHoleDiameters[i][2]; + found = true; + break; + } } - } - // if nothing was found (e.g. if not metric), we must calculate - if (!found) { - diameter = (5 * ((int)((diameter * 110) / 5))) / 100.0; - } - break; - case 1: /* close fit */ - // read diameter out of matrix - for (int i = 0; i < MatrixRowSize; i++) { - if (metricHoleDiameters[i][0] == diameter) { - diameter = metricHoleDiameters[i][1]; - found = true; - break; + // if nothing is defined (e.g. for M2.2, M9 and M11), we must calculate + // we use the factors defined for M5 in the metricHoleDiameters list + if (!found) { + diameter = diameter * 1.10; } - } - // if nothing was found, we must calculate - if (!found) { - diameter = (5 * ((int)((diameter * 105) / 5))) / 100.0; - } - break; - case 2: /* wide fit */ - // read diameter out of matrix - for (int i = 0; i < MatrixRowSize; i++) { - if (metricHoleDiameters[i][0] == diameter) { - diameter = metricHoleDiameters[i][3]; - found = true; - break; + break; + case 1: /* close fit */ + // read diameter out of matrix + for (int i = 0; i < MatrixRowSizeMetric; i++) { + if (metricHoleDiameters[i][0] == diameter) { + diameter = metricHoleDiameters[i][1]; + found = true; + break; + } } + // if nothing was found, we must calculate + if (!found) { + diameter = diameter * 1.06; + } + break; + case 2: /* wide fit */ + // read diameter out of matrix + for (int i = 0; i < MatrixRowSizeMetric; i++) { + if (metricHoleDiameters[i][0] == diameter) { + diameter = metricHoleDiameters[i][3]; + found = true; + break; + } + } + // if nothing was found, we must calculate + if (!found) { + diameter = diameter * 1.16; + } + break; + default: + throw Base::IndexError("Thread fit out of range"); } - // if nothing was found, we must calculate - if (!found) { - diameter = (5 * ((int)((diameter * 115) / 5))) / 100.0; + } + else if (threadType == "UNC" || threadType == "UNF" || threadType == "UNEF") { + std::string ThreadSizeString = ThreadSize.getValueAsString(); + int MatrixRowSizeUTS = sizeof(UTSHoleDiameters) / sizeof(UTSHoleDiameters[0]); + switch (ThreadFit.getValue()) { + case 0: /* normal fit */ + // read diameter out of matrix + for (int i = 0; i < MatrixRowSizeUTS; i++) { + if (UTSHoleDiameters[i].designation == ThreadSizeString) { + diameter = UTSHoleDiameters[i].normal; + found = true; + break; + } + } + // if nothing was found (if "#12" or "9/16"), we must calculate + // // we use the factors defined for "3/8" in the UTSHoleDiameters list + if (!found) { + diameter = diameter * 1.08; + } + break; + case 1: /* close fit */ + // read diameter out of matrix + for (int i = 0; i < MatrixRowSizeUTS; i++) { + if (UTSHoleDiameters[i].designation == ThreadSizeString) { + diameter = UTSHoleDiameters[i].close; + found = true; + break; + } + } + // if nothing was found, we must calculate + if (!found) { + diameter = diameter * 1.04; + } + break; + case 2: /* loose fit */ + // read diameter out of matrix + for (int i = 0; i < MatrixRowSizeUTS; i++) { + if (UTSHoleDiameters[i].designation == ThreadSizeString) { + diameter = UTSHoleDiameters[i].loose; + found = true; + break; + } + } + // if nothing was found, we must calculate + if (!found) { + diameter = diameter * 1.12; + } + break; + default: + throw Base::IndexError("Thread fit out of range"); + } + } + else { + switch (ThreadFit.getValue()) { + case 0: /* normal fit */ + // we must calculate + if (!found) { + diameter = diameter * 1.1; + } + break; + case 1: /* close fit */ + if (!found) { + diameter = diameter * 1.05; + } + break; + case 2: /* loose fit */ + if (!found) { + diameter = diameter * 1.15; + } + break; + default: + throw Base::IndexError("Thread fit out of range"); } - break; - default: - assert( 0 ); } } Diameter.setValue(diameter); @@ -844,11 +1043,11 @@ void Hole::updateDiameterParam() void Hole::onChanged(const App::Property *prop) { if (prop == &ThreadType) { - std::string type, holeCutType; + std::string type, holeCutTypeStr; if (ThreadType.isValid()) type = ThreadType.getValueAsString(); if (HoleCutType.isValid()) - holeCutType = HoleCutType.getValueAsString(); + holeCutTypeStr = HoleCutType.getValueAsString(); if (type == "None" ) { ThreadSize.setEnums(ThreadSize_None_Enums); @@ -865,6 +1064,7 @@ void Hole::onChanged(const App::Property *prop) ThreadSize.setEnums(ThreadSize_ISOmetric_Enums); ThreadClass.setEnums(ThreadClass_ISOmetric_Enums); HoleCutType.setEnums(HoleCutType_ISOmetric_Enums); + ThreadFit.setEnums(ClearanceMetricEnums); Threaded.setReadOnly(false); ThreadSize.setReadOnly(false); // thread class and direction are only sensible if threaded @@ -877,6 +1077,7 @@ void Hole::onChanged(const App::Property *prop) ThreadSize.setEnums(ThreadSize_ISOmetricfine_Enums); ThreadClass.setEnums(ThreadClass_ISOmetricfine_Enums); HoleCutType.setEnums(HoleCutType_ISOmetricfine_Enums); + ThreadFit.setEnums(ClearanceMetricEnums); Threaded.setReadOnly(false); ThreadSize.setReadOnly(false); // thread class and direction are only sensible if threaded @@ -889,6 +1090,7 @@ void Hole::onChanged(const App::Property *prop) ThreadSize.setEnums(ThreadSize_UNC_Enums); ThreadClass.setEnums(ThreadClass_UNC_Enums); HoleCutType.setEnums(HoleCutType_UNC_Enums); + ThreadFit.setEnums(ClearanceUTSEnums); Threaded.setReadOnly(false); ThreadSize.setReadOnly(false); // thread class and direction are only sensible if threaded @@ -901,6 +1103,7 @@ void Hole::onChanged(const App::Property *prop) ThreadSize.setEnums(ThreadSize_UNF_Enums); ThreadClass.setEnums(ThreadClass_UNF_Enums); HoleCutType.setEnums(HoleCutType_UNF_Enums); + ThreadFit.setEnums(ClearanceUTSEnums); Threaded.setReadOnly(false); ThreadSize.setReadOnly(false); // thread class and direction are only sensible if threaded @@ -913,6 +1116,7 @@ void Hole::onChanged(const App::Property *prop) ThreadSize.setEnums(ThreadSize_UNEF_Enums); ThreadClass.setEnums(ThreadClass_UNEF_Enums); HoleCutType.setEnums(HoleCutType_UNEF_Enums); + ThreadFit.setEnums(ClearanceUTSEnums); Threaded.setReadOnly(false); ThreadSize.setReadOnly(false); // thread class and direction are only sensible if threaded @@ -922,25 +1126,38 @@ void Hole::onChanged(const App::Property *prop) Diameter.setReadOnly(true); } - if (holeCutType == "None") { + if (holeCutTypeStr == "None") { + HoleCutCustomValues.setReadOnly(true); HoleCutDiameter.setReadOnly(true); HoleCutDepth.setReadOnly(true); HoleCutCountersinkAngle.setReadOnly(true); } - else if (holeCutType == "Counterbore") { + else if (holeCutTypeStr == "Counterbore") { + HoleCutCustomValues.setReadOnly(true); HoleCutDiameter.setReadOnly(false); HoleCutDepth.setReadOnly(false); HoleCutCountersinkAngle.setReadOnly(true); } - else if (holeCutType == "Countersink") { + else if (holeCutTypeStr == "Countersink") { + HoleCutCustomValues.setReadOnly(true); HoleCutDiameter.setReadOnly(false); HoleCutDepth.setReadOnly(false); HoleCutCountersinkAngle.setReadOnly(false); } else { // screw definition - HoleCutDiameter.setReadOnly(false); - HoleCutDepth.setReadOnly(false); - HoleCutCountersinkAngle.setReadOnly(false); + HoleCutCustomValues.setReadOnly(false); + if (HoleCutCustomValues.getValue()) { + HoleCutDiameter.setReadOnly(false); + HoleCutDepth.setReadOnly(false); + // we must not set HoleCutCountersinkAngle here because the info if this can + // be enabled is first available in updateHoleCutParams and thus handled there + updateHoleCutParams(); + } + else { + HoleCutDiameter.setReadOnly(true); + HoleCutDepth.setReadOnly(true); + HoleCutCountersinkAngle.setReadOnly(true); + } } // Signal changes to these @@ -994,10 +1211,14 @@ void Hole::onChanged(const App::Property *prop) ThreadCutOffOuter.setReadOnly(v); } else if (prop == &DrillPoint) { - if (DrillPoint.getValue() == 1) + if (DrillPoint.getValue() == 1) { DrillPointAngle.setReadOnly(false); - else + DrillForDepth.setReadOnly(false); + } + else { DrillPointAngle.setReadOnly(true); + DrillForDepth.setReadOnly(true); + } } else if (prop == &Tapered) { if (Tapered.getValue()) @@ -1007,55 +1228,47 @@ void Hole::onChanged(const App::Property *prop) } else if (prop == &ThreadSize) { updateDiameterParam(); - updateHoleCutParams(); + // updateHoleCutParams() will later automatically be called because updateDiameterParam() changes &Diameter } else if (prop == &ThreadFit) { updateDiameterParam(); } + else if (prop == &Diameter) { + // a changed diameter means we also need to check the hole cut + // because the hole cut diameter must not be <= than the diameter + updateHoleCutParams(); + } else if (prop == &HoleCutType) { - std::string holeCutType; - if (HoleCutType.isValid()) - holeCutType = HoleCutType.getValueAsString(); - if (holeCutType == "None") { - HoleCutDiameter.setReadOnly(true); - HoleCutDepth.setReadOnly(true); - HoleCutCountersinkAngle.setReadOnly(true); - } - else if (holeCutType == "Counterbore") { - HoleCutDiameter.setReadOnly(false); - HoleCutDepth.setReadOnly(false); - HoleCutCountersinkAngle.setReadOnly(true); - } - else if (holeCutType == "Countersink") { - HoleCutDiameter.setReadOnly(false); - HoleCutDepth.setReadOnly(false); - HoleCutCountersinkAngle.setReadOnly(false); - } - else { // screw definition - HoleCutDiameter.setReadOnly(false); - HoleCutDepth.setReadOnly(false); - HoleCutCountersinkAngle.setReadOnly(false); - } ProfileBased::onChanged(&HoleCutDiameter); ProfileBased::onChanged(&HoleCutDepth); ProfileBased::onChanged(&HoleCutCountersinkAngle); + + // the read-only states are set in updateHoleCutParams() + updateHoleCutParams(); + } + else if (prop == &HoleCutCustomValues) { + // when going back to standardized values, we must recalculate + // also to find out if HoleCutCountersinkAngle can be ReadOnly + // both an also the read-only states is done in updateHoleCutParams() updateHoleCutParams(); } else if (prop == &DepthType) { Depth.setReadOnly((std::string(DepthType.getValueAsString()) != "Dimension")); + DrillPoint.setReadOnly((std::string(DepthType.getValueAsString()) != "Dimension")); + DrillPointAngle.setReadOnly((std::string(DepthType.getValueAsString()) != "Dimension")); + DrillForDepth.setReadOnly((std::string(DepthType.getValueAsString()) != "Dimension")); } ProfileBased::onChanged(prop); } /** - * Compute 2D intersection between the lines (pa1, pa2) and (pb1, pb2). + * Computes 2D intersection between the lines (pa1, pa2) and (pb1, pb2). * The lines are assumed to be crossing, and it is an error * to specify parallel lines. + * Only the x and y coordinates of the points are used to compute the 2D intersection. * - * Only the x and y coordinates are used to compute the 2D intersection. - * + * The result are the x and y coordinate of the intersection point. */ - static void computeIntersection(gp_Pnt pa1, gp_Pnt pa2, gp_Pnt pb1, gp_Pnt pb2, double & x, double & y) { double vx1 = pa1.X() - pa2.X(); @@ -1200,7 +1413,7 @@ App::DocumentObjectExecReturn *Hole::execute(void) base.Move(invObjLoc); if (profileshape.IsNull()) - return new App::DocumentObjectExecReturn("Pocket: Creating a face from sketch failed"); + return new App::DocumentObjectExecReturn("Hole error: Creating a face from sketch failed"); profileshape.Move(invObjLoc); /* Build the prototype hole */ @@ -1238,10 +1451,10 @@ App::DocumentObjectExecReturn *Hole::execute(void) length = 1e4; } else - return new App::DocumentObjectExecReturn("Hole: Unsupported length specification."); + return new App::DocumentObjectExecReturn("Hole error: Unsupported length specification"); - if (length <= 0) - return new App::DocumentObjectExecReturn("Hole: Invalid hole depth"); + if (length <= 0.0) + return new App::DocumentObjectExecReturn("Hole error: Invalid hole depth"); BRepBuilderAPI_MakeWire mkWire; const std::string holeCutType = HoleCutType.getValueAsString(); @@ -1253,26 +1466,27 @@ App::DocumentObjectExecReturn *Hole::execute(void) holeCutType == "Cheesehead (deprecated)" || holeCutType == "Cap screw (deprecated)" || isDynamicCounterbore(threadType, holeCutType)); - double hasTaperedAngle = Tapered.getValue() ? Base::toRadians( TaperedAngle.getValue() ) : Base::toRadians(90.0); - double radiusBottom = Diameter.getValue() / 2.0 - length * 1.0 / tan( hasTaperedAngle ); + double TaperedAngleVal = Tapered.getValue() ? Base::toRadians( TaperedAngle.getValue() ) : Base::toRadians(90.0); + double radiusBottom = Diameter.getValue() / 2.0 - length / tan(TaperedAngleVal); double radius = Diameter.getValue() / 2.0; double holeCutRadius = HoleCutDiameter.getValue() / 2.0; gp_Pnt firstPoint(0, 0, 0); gp_Pnt lastPoint(0, 0, 0); - double length1 = 0; + double lengthCounter = 0.0; + double xPosCounter = 0.0; + double zPosCounter = 0.0; - if ( hasTaperedAngle <= 0 || hasTaperedAngle > Base::toRadians( 180.0 ) ) - return new App::DocumentObjectExecReturn("Hole: Invalid taper angle."); + if (TaperedAngleVal <= 0.0 || TaperedAngleVal > Base::toRadians( 180.0 ) ) + return new App::DocumentObjectExecReturn("Hole error: Invalid taper angle"); if ( isCountersink ) { - double x, z; double countersinkAngle = Base::toRadians( HoleCutCountersinkAngle.getValue() / 2.0 ); if ( countersinkAngle <= 0 || countersinkAngle > Base::toRadians( 180.0 ) ) - return new App::DocumentObjectExecReturn("Hole: Invalid countersink angle."); + return new App::DocumentObjectExecReturn("Hole error: Invalid countersink angle"); if (holeCutRadius < radius) - return new App::DocumentObjectExecReturn("Hole: Hole cut diameter too small."); + return new App::DocumentObjectExecReturn("Hole error: Hole cut diameter too small"); // Top point gp_Pnt newPoint = toPnt(holeCutRadius * xDir); @@ -1282,28 +1496,27 @@ App::DocumentObjectExecReturn *Hole::execute(void) computeIntersection(gp_Pnt( holeCutRadius, 0, 0 ), gp_Pnt( holeCutRadius - sin( countersinkAngle ), -cos( countersinkAngle ), 0 ), gp_Pnt( radius, 0, 0 ), - gp_Pnt( radiusBottom, -length, 0), x, z ); - if (-length > z) - return new App::DocumentObjectExecReturn("Hole: Invalid countersink."); + gp_Pnt( radiusBottom, -length, 0), xPosCounter, zPosCounter); + if (-length > zPosCounter) + return new App::DocumentObjectExecReturn("Hole error: Invalid countersink"); - length1 = z; + lengthCounter = zPosCounter; - newPoint = toPnt(x * xDir + z * zDir); + newPoint = toPnt(xPosCounter * xDir + zPosCounter * zDir); mkWire.Add( BRepBuilderAPI_MakeEdge( lastPoint, newPoint ) ); lastPoint = newPoint; } else if ( isCounterbore ) { double holeCutDepth = HoleCutDepth.getValue(); - double x, z; - if (holeCutDepth <= 0) - return new App::DocumentObjectExecReturn("Hole: Hole cut depth must be greater than zero."); + if (holeCutDepth <= 0.0) + return new App::DocumentObjectExecReturn("Hole error: Hole cut depth must be greater than zero"); if (holeCutDepth > length) - return new App::DocumentObjectExecReturn("Hole: Hole cut depth must be less than hole depth."); + return new App::DocumentObjectExecReturn("Hole error: Hole cut depth must be less than hole depth"); if (holeCutRadius < radius) - return new App::DocumentObjectExecReturn("Hole: Hole cut diameter too small."); + return new App::DocumentObjectExecReturn("Hole error: Hole cut diameter too small"); // Top point gp_Pnt newPoint = toPnt(holeCutRadius * xDir); @@ -1311,7 +1524,7 @@ App::DocumentObjectExecReturn *Hole::execute(void) lastPoint = newPoint; // Bottom of counterbore - newPoint = toPnt(holeCutRadius * xDir -holeCutDepth * zDir); + newPoint = toPnt(holeCutRadius * xDir - holeCutDepth * zDir); mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); lastPoint = newPoint; @@ -1319,10 +1532,10 @@ App::DocumentObjectExecReturn *Hole::execute(void) computeIntersection(gp_Pnt( 0, -holeCutDepth, 0 ), gp_Pnt( holeCutRadius, -holeCutDepth, 0 ), gp_Pnt( radius, 0, 0 ), - gp_Pnt( radiusBottom, length, 0 ), x, z ); + gp_Pnt( radiusBottom, length, 0 ), xPosCounter, zPosCounter); - length1 = z; - newPoint = toPnt(x * xDir + z * zDir); + lengthCounter = zPosCounter; + newPoint = toPnt(xPosCounter * xDir + zPosCounter * zDir); mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); lastPoint = newPoint; } @@ -1330,10 +1543,12 @@ App::DocumentObjectExecReturn *Hole::execute(void) gp_Pnt newPoint = toPnt(radius * xDir); mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); lastPoint = newPoint; - length1 = 0; + lengthCounter = 0.0; } std::string drillPoint = DrillPoint.getValueAsString(); + double xPosDrill = 0.0; + double zPosDrill = 0.0; if (drillPoint == "Flat") { gp_Pnt newPoint = toPnt(radiusBottom * xDir + -length * zDir); mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); @@ -1345,30 +1560,47 @@ App::DocumentObjectExecReturn *Hole::execute(void) } else if (drillPoint == "Angled") { double drillPointAngle = Base::toRadians( ( 180.0 - DrillPointAngle.getValue() ) / 2.0 ); - double x, z; gp_Pnt newPoint; + bool isDrillForDepth = DrillForDepth.getValue(); - if ( drillPointAngle <= 0 || drillPointAngle > Base::toRadians( 180.0 ) ) - return new App::DocumentObjectExecReturn("Hole: Invalid drill point angle."); + // the angle is in any case > 0 and < 90 but nevertheless this safeguard: + if ( drillPointAngle <= 0.0 || drillPointAngle >= Base::toRadians( 180.0 ) ) + return new App::DocumentObjectExecReturn("Hole error: Invalid drill point angle"); - computeIntersection(gp_Pnt( 0, -length, 0 ), - gp_Pnt( cos( drillPointAngle ), -length + sin( drillPointAngle ), 0), - gp_Pnt(radius, 0, 0), - gp_Pnt(radiusBottom, -length, 0), x, z); + // if option to take drill point size into account + // the next wire point is the intersection of the drill edge and the hole edge + if (isDrillForDepth) { + computeIntersection(gp_Pnt(0, -length, 0), + gp_Pnt(radius, radius * tan(drillPointAngle) - length, 0), + gp_Pnt(radius, 0, 0), + gp_Pnt(radiusBottom, -length, 0), xPosDrill, zPosDrill); + if (zPosDrill > 0 || zPosDrill >= lengthCounter) + return new App::DocumentObjectExecReturn("Hole error: Invalid drill point"); - if (z > 0 || z >= length1) - return new App::DocumentObjectExecReturn("Hole: Invalid drill point."); + newPoint = toPnt(xPosDrill * xDir + zPosDrill * zDir); + mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); + lastPoint = newPoint; - newPoint = toPnt(x * xDir + z * zDir); - mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); - lastPoint = newPoint; + newPoint = toPnt(-length * zDir); + mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); + lastPoint = newPoint; + } + else { + xPosDrill = radiusBottom; + zPosDrill = -length; - newPoint = toPnt(-length * zDir); - mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); - lastPoint = newPoint; + newPoint = toPnt(xPosDrill * xDir + zPosDrill * zDir); + mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); + lastPoint = newPoint; + + // the end point is the size of the drill tip + newPoint = toPnt((-length - radius * tan(drillPointAngle)) * zDir); + mkWire.Add(BRepBuilderAPI_MakeEdge(lastPoint, newPoint)); + lastPoint = newPoint; + } } - mkWire.Add( BRepBuilderAPI_MakeEdge(lastPoint, firstPoint) ); + mkWire.Add( BRepBuilderAPI_MakeEdge(lastPoint, firstPoint) ); TopoDS_Wire wire = mkWire.Wire(); @@ -1382,10 +1614,10 @@ App::DocumentObjectExecReturn *Hole::execute(void) protoHole = RevolMaker.Shape(); if (protoHole.IsNull()) - return new App::DocumentObjectExecReturn("Hole: Resulting shape is empty"); + return new App::DocumentObjectExecReturn("Hole error: Resulting shape is empty"); } else - return new App::DocumentObjectExecReturn("Hole: Could not revolve sketch!"); + return new App::DocumentObjectExecReturn("Hole error: Could not revolve sketch"); #if 0 // Make thread diff --git a/src/Mod/PartDesign/App/FeatureHole.h b/src/Mod/PartDesign/App/FeatureHole.h index 66dde28a3a..c28ef80465 100644 --- a/src/Mod/PartDesign/App/FeatureHole.h +++ b/src/Mod/PartDesign/App/FeatureHole.h @@ -57,6 +57,7 @@ public: App::PropertyLength Diameter; App::PropertyEnumeration ThreadDirection; App::PropertyEnumeration HoleCutType; + App::PropertyBool HoleCutCustomValues; App::PropertyLength HoleCutDiameter; App::PropertyLength HoleCutDepth; App::PropertyAngle HoleCutCountersinkAngle; @@ -64,6 +65,7 @@ public: App::PropertyLength Depth; App::PropertyEnumeration DrillPoint; App::PropertyAngle DrillPointAngle; + App::PropertyBool DrillForDepth; App::PropertyBool Tapered; App::PropertyAngle TaperedAngle; @@ -89,6 +91,14 @@ public: static const double metricHoleDiameters[36][4]; + typedef struct { + std::string designation; + double close; + double normal; + double loose; + } UTSClearanceDefinition; + static const UTSClearanceDefinition UTSHoleDiameters[22]; + virtual void Restore(Base::XMLReader & reader); virtual void updateProps(); @@ -98,7 +108,8 @@ protected: private: static const char* DepthTypeEnums[]; static const char* ThreadTypeEnums[]; - static const char* ThreadFitEnums[]; + static const char* ClearanceMetricEnums[]; + static const char* ClearanceUTSEnums[]; static const char* DrillPointEnums[]; static const char* ThreadDirectionEnums[]; diff --git a/src/Mod/PartDesign/App/FeatureSketchBased.cpp b/src/Mod/PartDesign/App/FeatureSketchBased.cpp index 8fe9f1dc58..f92b3299fc 100644 --- a/src/Mod/PartDesign/App/FeatureSketchBased.cpp +++ b/src/Mod/PartDesign/App/FeatureSketchBased.cpp @@ -208,7 +208,7 @@ TopoDS_Shape ProfileBased::getVerifiedFace(bool silent) const { if(faces.empty()) { if(!shape.hasSubShape(TopAbs_WIRE)) shape = shape.makEWires(); - if(shape.hasSubShape(TopAbs_WIRE)) + if(shape.hasSubShape(TopAbs_WIRE)) shape = shape.makEFace(0,"Part::FaceMakerCheese"); else err = "Cannot make face from profile"; @@ -1012,7 +1012,7 @@ double ProfileBased::getReversedAngle(const Base::Vector3d &b, const Base::Vecto } void ProfileBased::getAxis(const App::DocumentObject *pcReferenceAxis, const std::vector &subReferenceAxis, - Base::Vector3d& base, Base::Vector3d& dir) + Base::Vector3d& base, Base::Vector3d& dir, bool checkPerpendicular) { dir = Base::Vector3d(0,0,0); // If unchanged signals that no valid axis was found if (pcReferenceAxis == NULL) @@ -1071,7 +1071,7 @@ void ProfileBased::getAxis(const App::DocumentObject *pcReferenceAxis, const std dir = line->getDirection(); // Check that axis is perpendicular with sketch plane! - if (sketchplane.Axis().Direction().IsParallel(gp_Dir(dir.x, dir.y, dir.z), Precision::Angular())) + if (checkPerpendicular && sketchplane.Axis().Direction().IsParallel(gp_Dir(dir.x, dir.y, dir.z), Precision::Angular())) throw Base::ValueError("Rotation axis must not be perpendicular with the sketch plane"); return; } @@ -1082,7 +1082,7 @@ void ProfileBased::getAxis(const App::DocumentObject *pcReferenceAxis, const std line->Placement.getValue().multVec(Base::Vector3d (1,0,0), dir); // Check that axis is perpendicular with sketch plane! - if (sketchplane.Axis().Direction().IsParallel(gp_Dir(dir.x, dir.y, dir.z), Precision::Angular())) + if (checkPerpendicular && sketchplane.Axis().Direction().IsParallel(gp_Dir(dir.x, dir.y, dir.z), Precision::Angular())) throw Base::ValueError("Rotation axis must not be perpendicular with the sketch plane"); return; } diff --git a/src/Mod/PartDesign/App/FeatureSketchBased.h b/src/Mod/PartDesign/App/FeatureSketchBased.h index 207bde6d85..7e13019ba4 100644 --- a/src/Mod/PartDesign/App/FeatureSketchBased.h +++ b/src/Mod/PartDesign/App/FeatureSketchBased.h @@ -78,7 +78,7 @@ public: * Default is false. */ Part::Part2DObject* getVerifiedSketch(bool silent=false) const; - + /** * Verifies the linked Profile and returns it if it is a valid object * @param silent if profile property is malformed and the parameter is true @@ -86,7 +86,7 @@ public: * Default is false. */ Part::Feature* getVerifiedObject(bool silent=false) const; - + /** * Verifies the linked Object and returns the shape used as profile * @param silent if profirle property is malformed and the parameter is true @@ -94,25 +94,25 @@ public: * Default is false. */ TopoDS_Shape getVerifiedFace(bool silent = false) const; - + /// Returns the wires the sketch is composed of std::vector getProfileWires() const; - + /// Returns the face of the sketch support (if any) const TopoDS_Face getSupportFace() const; - + Base::Vector3d getProfileNormal() const; Part::TopoShape getProfileShape() const; /// retrieves the number of axes in the linked sketch (defined as construction lines) - int getSketchAxisCount(void) const; + int getSketchAxisCount(void) const; virtual Part::Feature* getBaseObject(bool silent=false) const; - + //backwards compatibility: profile property was renamed and has different type now virtual void Restore(Base::XMLReader& reader); - + protected: void remapSupportShape(const TopoDS_Shape&); @@ -167,8 +167,8 @@ protected: double getReversedAngle(const Base::Vector3d& b, const Base::Vector3d& v); /// get Axis from ReferenceAxis void getAxis(const App::DocumentObject* pcReferenceAxis, const std::vector& subReferenceAxis, - Base::Vector3d& base, Base::Vector3d& dir); - + Base::Vector3d& base, Base::Vector3d& dir, bool checkPerpendicular=true); + void onChanged(const App::Property* prop); private: bool isParallelPlane(const TopoDS_Shape&, const TopoDS_Shape&) const; diff --git a/src/Mod/PartDesign/Gui/AppPartDesignGui.cpp b/src/Mod/PartDesign/Gui/AppPartDesignGui.cpp index 880ffddefb..32fcab7467 100644 --- a/src/Mod/PartDesign/Gui/AppPartDesignGui.cpp +++ b/src/Mod/PartDesign/Gui/AppPartDesignGui.cpp @@ -61,6 +61,7 @@ #include "ViewProviderThickness.h" #include "ViewProviderPipe.h" #include "ViewProviderLoft.h" +#include "ViewProviderHelix.h" #include "ViewProviderShapeBinder.h" #include "ViewProviderBase.h" @@ -156,6 +157,7 @@ PyMOD_INIT_FUNC(PartDesignGui) PartDesignGui::ViewProviderPrimitive ::init(); PartDesignGui::ViewProviderPipe ::init(); PartDesignGui::ViewProviderLoft ::init(); + PartDesignGui::ViewProviderHelix ::init(); PartDesignGui::ViewProviderBase ::init(); // add resources and reloads the translators diff --git a/src/Mod/PartDesign/Gui/CMakeLists.txt b/src/Mod/PartDesign/Gui/CMakeLists.txt index 292221c8c6..2a2ce18401 100644 --- a/src/Mod/PartDesign/Gui/CMakeLists.txt +++ b/src/Mod/PartDesign/Gui/CMakeLists.txt @@ -55,6 +55,7 @@ set(PartDesignGui_MOC_HDRS TaskPrimitiveParameters.h TaskPipeParameters.h TaskLoftParameters.h + TaskHelixParameters.h ) fc_wrap_cpp(PartDesignGui_MOC_SRCS ${PartDesignGui_MOC_HDRS}) SOURCE_GROUP("Moc" FILES ${PartDesignGui_MOC_SRCS}) @@ -89,6 +90,7 @@ set(PartDesignGui_UIC_SRCS TaskPipeScaling.ui TaskLoftParameters.ui DlgReference.ui + TaskHelixParameters.ui ) if(BUILD_QT5) @@ -158,7 +160,9 @@ SET(PartDesignGuiViewProvider_SRCS ViewProviderPipe.cpp ViewProviderLoft.h ViewProviderLoft.cpp - ViewProviderBase.h + ViewProviderHelix.h + ViewProviderHelix.cpp + ViewProviderBase.h ViewProviderBase.cpp ) SOURCE_GROUP("ViewProvider" FILES ${PartDesignGuiViewProvider_SRCS}) @@ -237,6 +241,9 @@ SET(PartDesignGuiTaskDlgs_SRCS TaskLoftParameters.ui TaskLoftParameters.h TaskLoftParameters.cpp + TaskHelixParameters.ui + TaskHelixParameters.h + TaskHelixParameters.cpp ) SOURCE_GROUP("TaskDialogs" FILES ${PartDesignGuiTaskDlgs_SRCS}) diff --git a/src/Mod/PartDesign/Gui/Command.cpp b/src/Mod/PartDesign/Gui/Command.cpp index 295da09853..547821c59b 100644 --- a/src/Mod/PartDesign/Gui/Command.cpp +++ b/src/Mod/PartDesign/Gui/Command.cpp @@ -55,6 +55,7 @@ #include #include #include + #include #include #include @@ -384,7 +385,7 @@ void CmdPartDesignSubShapeBinder::activated(int iMsg) } values = std::move(links); } - + PartDesign::SubShapeBinder *binder = 0; try { openCommand(QT_TRANSLATE_NOOP("Command", "Create SubShapeBinder")); @@ -403,7 +404,7 @@ void CmdPartDesignSubShapeBinder::activated(int iMsg) commitCommand(); } catch (Base::Exception &e) { e.ReportException(); - QMessageBox::critical(Gui::getMainWindow(), + QMessageBox::critical(Gui::getMainWindow(), QObject::tr("Sub-Shape Binder"), QString::fromUtf8(e.what())); abortCommand(); } @@ -821,7 +822,7 @@ void CmdPartDesignNewSketch::activated(int iMsg) Gui::Control().closeDialog(); Gui::Selection().clearSelection(); - Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick(planes, status, accepter, worker, quitter)); + Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick(planes, status, accepter, worker, true, quitter)); } } } @@ -994,7 +995,7 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons FCMD_OBJ_CMD(pcActiveBody,"newObject('PartDesign::" << which << "','" << FeatName << "')"); auto Feat = pcActiveBody->getDocument()->getObject(FeatName.c_str()); - + auto objCmd = Gui::Command::getObjectCmd(feature); if (feature->isDerivedFrom(Part::Part2DObject::getClassTypeId()) || subs.empty()) { FCMD_OBJ_CMD(Feat,"Profile = " << objCmd); @@ -1003,7 +1004,7 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons std::ostringstream ss; for (auto &s : subs) ss << "'" << s << "',"; - FCMD_OBJ_CMD(Feat,"Profile = (" << objCmd << ", [" << ss.str() << "])"); + FCMD_OBJ_CMD(Feat,"Profile = (" << objCmd << ", [" << ss.str() << "])"); } //for additive and subtractive lofts allow the user to preselect the sections @@ -1042,10 +1043,44 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons func(static_cast(feature), Feat); }; + + // in case of subtractive types, check that there is something to subtract from + if ((which.find("Subtractive") != std::string::npos) || + (which.compare("Groove") == 0) || + (which.compare("Pocket") == 0)) { + + if (!pcActiveBody->isSolid()) { + QMessageBox msgBox; + msgBox.setText(QObject::tr("Cannot use this command as there is no solid to subtract from.")); + msgBox.setInformativeText(QObject::tr("Ensure that the body contains a feature before attempting a subtractive command.")); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + return; + } + } + + //if a profile is selected we can make our life easy and fast std::vector selection = cmd->getSelection().getSelectionEx(); if (!selection.empty()) { - base_worker(selection.front().getObject(), selection.front().getSubNames()); + bool onlyAllowed = true; + for (auto it = selection.begin(); it!=selection.end(); ++it){ + if (PartDesign::Body::findBodyOf((*it).getObject()) != pcActiveBody) { // the selected objects must belong to the body + onlyAllowed = false; + break; + } + } + if (!onlyAllowed) { + QMessageBox msgBox; + msgBox.setText(QObject::tr("Cannot use selected object. Selected object must belong to the active body")); + msgBox.setInformativeText(QObject::tr("Consider using a ShapeBinder or a BaseFeature to reference external geometry in a body.")); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } else { + base_worker(selection.front().getObject(), selection.front().getSubNames()); + } return; } @@ -1151,7 +1186,7 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons Gui::Control().closeDialog(); Gui::Selection().clearSelection(); - pickDlg = new PartDesignGui::TaskDlgFeaturePick(sketches, status, accepter, sketch_worker); + pickDlg = new PartDesignGui::TaskDlgFeaturePick(sketches, status, accepter, sketch_worker, true); // Logically dead code because 'bNoSketchWasSelected' must be true //if (!bNoSketchWasSelected && extReference) // pickDlg->showExternal(true); @@ -1418,7 +1453,7 @@ void CmdPartDesignGroove::activated(int iMsg) else { FCMD_OBJ_CMD(Feat,"ReferenceAxis = ("<getOrigin()->getY())<<",[''])"); } - + FCMD_OBJ_CMD(Feat,"Angle = 360.0"); try { @@ -1643,6 +1678,119 @@ bool CmdPartDesignSubtractiveLoft::isActive(void) return hasActiveDocument(); } +//=========================================================================== +// PartDesign_Additive_Helix +//=========================================================================== +DEF_STD_CMD_A(CmdPartDesignAdditiveHelix) + +CmdPartDesignAdditiveHelix::CmdPartDesignAdditiveHelix() + : Command("PartDesign_AdditiveHelix") +{ + sAppModule = "PartDesign"; + sGroup = QT_TR_NOOP("PartDesign"); + sMenuText = QT_TR_NOOP("Additive helix"); + sToolTipText = QT_TR_NOOP("Sweep a selected sketch along a helix"); + sWhatsThis = "PartDesign_AdditiveHelix"; + sStatusTip = sToolTipText; + sPixmap = "PartDesign_Additive_Helix"; +} + +void CmdPartDesignAdditiveHelix::activated(int iMsg) +{ + Q_UNUSED(iMsg); + App::Document *doc = getDocument(); + if (!PartDesignGui::assureModernWorkflow(doc)) + return; + + PartDesign::Body *pcActiveBody = PartDesignGui::getBody(true); + + if (!pcActiveBody) + return; + + Gui::Command* cmd = this; + auto worker = [cmd, &pcActiveBody](Part::Feature* sketch, App::DocumentObject *Feat) { + + if (!Feat) return; + + // specific parameters for helix + Gui::Command::updateActive(); + + if (sketch->isDerivedFrom(Part::Part2DObject::getClassTypeId())) { + FCMD_OBJ_CMD(Feat,"ReferenceAxis = (" << getObjectCmd(sketch) << ",['V_Axis'])"); + } + else { + FCMD_OBJ_CMD(Feat,"ReferenceAxis = (" << getObjectCmd(pcActiveBody->getOrigin()->getY()) << ",[''])"); + } + + finishProfileBased(cmd, sketch, Feat); + cmd->adjustCameraPosition(); + }; + + prepareProfileBased(pcActiveBody, this, "AdditiveHelix", worker); +} + +bool CmdPartDesignAdditiveHelix::isActive(void) +{ + return hasActiveDocument(); +} + + +//=========================================================================== +// PartDesign_Subtractive_Helix +//=========================================================================== +DEF_STD_CMD_A(CmdPartDesignSubtractiveHelix) + +CmdPartDesignSubtractiveHelix::CmdPartDesignSubtractiveHelix() + : Command("PartDesign_SubtractiveHelix") +{ + sAppModule = "PartDesign"; + sGroup = QT_TR_NOOP("PartDesign"); + sMenuText = QT_TR_NOOP("Subtractive helix"); + sToolTipText = QT_TR_NOOP("Sweep a selected sketch along a helix and remove it from the body"); + sWhatsThis = "PartDesign_SubtractiveHelix"; + sStatusTip = sToolTipText; + sPixmap = "PartDesign_Subtractive_Helix"; +} + +void CmdPartDesignSubtractiveHelix::activated(int iMsg) +{ + Q_UNUSED(iMsg); + App::Document *doc = getDocument(); + if (!PartDesignGui::assureModernWorkflow(doc)) + return; + + PartDesign::Body *pcActiveBody = PartDesignGui::getBody(true); + + if (!pcActiveBody) + return; + + Gui::Command* cmd = this; + auto worker = [cmd, &pcActiveBody](Part::Feature* sketch, App::DocumentObject *Feat) { + + if (!Feat) return; + + // specific parameters for helix + Gui::Command::updateActive(); + + if (sketch->isDerivedFrom(Part::Part2DObject::getClassTypeId())) { + FCMD_OBJ_CMD(Feat,"ReferenceAxis = (" << getObjectCmd(sketch) << ",['V_Axis'])"); + } + else { + FCMD_OBJ_CMD(Feat,"ReferenceAxis = (" << getObjectCmd(pcActiveBody->getOrigin()->getY()) << ",[''])"); + } + + finishProfileBased(cmd, sketch, Feat); + cmd->adjustCameraPosition(); + }; + + prepareProfileBased(pcActiveBody, this, "SubtractiveHelix", worker); +} + +bool CmdPartDesignSubtractiveHelix::isActive(void) +{ + return hasActiveDocument(); +} + //=========================================================================== // Common utility functions for Dressup features //=========================================================================== @@ -1979,7 +2127,7 @@ void prepareTransformed(PartDesign::Body *pcActiveBody, Gui::Command* cmd, const Gui::Control().closeDialog(); Gui::Selection().clearSelection(); - Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick(features, status, accepter, worker)); + Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick(features, status, accepter, worker, false)); return; } else if (features.empty()) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("No valid features in this document"), @@ -2176,7 +2324,7 @@ void CmdPartDesignPolarPattern::activated(int iMsg) } if (!direction) { auto body = static_cast(Part::BodyBase::findBodyOf(features.front())); - if (body) { + if (body) { FCMD_OBJ_CMD(Feat,"Axis = ("<getOrigin()->getZ())<<",[''])"); } } @@ -2397,7 +2545,7 @@ void CmdPartDesignBoolean::activated(int iMsg) std::string FeatName = getUniqueObjectName("Boolean",pcActiveBody); FCMD_OBJ_CMD(pcActiveBody,"newObject('PartDesign::Boolean','"<getDocument()->getObject(FeatName.c_str()); - + // If we don't add an object to the boolean group then don't update the body // as otherwise this will fail and it will be marked as invalid bool updateDocument = false; @@ -2456,6 +2604,8 @@ void CreatePartDesignCommands(void) rcCmdMgr.addCommand(new CmdPartDesignSubtractivePipe); rcCmdMgr.addCommand(new CmdPartDesignAdditiveLoft); rcCmdMgr.addCommand(new CmdPartDesignSubtractiveLoft); + rcCmdMgr.addCommand(new CmdPartDesignAdditiveHelix); + rcCmdMgr.addCommand(new CmdPartDesignSubtractiveHelix); rcCmdMgr.addCommand(new CmdPartDesignFillet()); rcCmdMgr.addCommand(new CmdPartDesignDraft()); diff --git a/src/Mod/PartDesign/Gui/CommandBody.cpp b/src/Mod/PartDesign/Gui/CommandBody.cpp index f3efffb205..61824c58c5 100644 --- a/src/Mod/PartDesign/Gui/CommandBody.cpp +++ b/src/Mod/PartDesign/Gui/CommandBody.cpp @@ -286,7 +286,7 @@ void CmdPartDesignBody::activated(int iMsg) Gui::TaskView::TaskDialog *dlg = Gui::Control().activeDialog(); if (!dlg) { Gui::Selection().clearSelection(); - Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick(planes, status, accepter, worker, quitter)); + Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick(planes, status, accepter, worker, true, quitter)); } } } diff --git a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc index 729a9c3b9a..bb55317411 100644 --- a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc +++ b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc @@ -6,6 +6,7 @@ icons/PartDesign_Additive_Ellipsoid.svg icons/PartDesign_Additive_Loft.svg icons/PartDesign_Additive_Pipe.svg + icons/PartDesign_Additive_Helix.svg icons/PartDesign_Additive_Prism.svg icons/PartDesign_Additive_Sphere.svg icons/PartDesign_Additive_Torus.svg @@ -50,6 +51,7 @@ icons/PartDesign_Subtractive_Ellipsoid.svg icons/PartDesign_Subtractive_Loft.svg icons/PartDesign_Subtractive_Pipe.svg + icons/PartDesign_Subtractive_Helix.svg icons/PartDesign_Subtractive_Prism.svg icons/PartDesign_Subtractive_Sphere.svg icons/PartDesign_Subtractive_Torus.svg diff --git a/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Additive_Helix.svg b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Additive_Helix.svg new file mode 100644 index 0000000000..ed20f4cb1c --- /dev/null +++ b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Additive_Helix.svg @@ -0,0 +1,1456 @@ + + + PartDesign_Additive_Helix + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + PartDesign_Additive_Helix + + + bitacovir, davidosterberg + + + PartDesign_Revolution + 2020/12/30 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Subtractive_Helix.svg b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Subtractive_Helix.svg new file mode 100644 index 0000000000..91e1d56a6d --- /dev/null +++ b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Subtractive_Helix.svg @@ -0,0 +1,1456 @@ + + + PartDesign_Subtractive_Helix + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + PartDesign_Subtractive_Helix + + + bitacovir, davidosterberg + + + PartDesign_Revolution + 2020/12/30 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp b/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp index 056aa99434..c72e241686 100644 --- a/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp @@ -53,13 +53,14 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskBooleanParameters */ TaskBooleanParameters::TaskBooleanParameters(ViewProviderBoolean *BooleanView,QWidget *parent) - : TaskBox(Gui::BitmapFactory().pixmap("PartDesign_Boolean"),tr("Boolean parameters"),true, parent),BooleanView(BooleanView) + : TaskBox(Gui::BitmapFactory().pixmap("PartDesign_Boolean"), tr("Boolean parameters"), true, parent) + , ui(new Ui_TaskBooleanParameters) + , BooleanView(BooleanView) { selectionMode = none; // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskBooleanParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -289,7 +290,6 @@ void TaskBooleanParameters::onBodyDeleted(void) TaskBooleanParameters::~TaskBooleanParameters() { - delete ui; } void TaskBooleanParameters::changeEvent(QEvent *e) diff --git a/src/Mod/PartDesign/Gui/TaskBooleanParameters.h b/src/Mod/PartDesign/Gui/TaskBooleanParameters.h index 6020dd850e..6f9739ce55 100644 --- a/src/Mod/PartDesign/Gui/TaskBooleanParameters.h +++ b/src/Mod/PartDesign/Gui/TaskBooleanParameters.h @@ -70,7 +70,7 @@ protected: private: QWidget* proxy; - Ui_TaskBooleanParameters* ui; + std::unique_ptr ui; ViewProviderBoolean *BooleanView; enum selectionModes { none, bodyAdd, bodyRemove }; diff --git a/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp b/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp index 31e84795c8..75745dc5fb 100644 --- a/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp @@ -57,10 +57,10 @@ using namespace Gui; TaskChamferParameters::TaskChamferParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, true, true, parent) + , ui(new Ui_TaskChamferParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskChamferParameters(); ui->setupUi(proxy); this->groupLayout()->addWidget(proxy); @@ -311,8 +311,6 @@ TaskChamferParameters::~TaskChamferParameters() { Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); - - delete ui; } bool TaskChamferParameters::event(QEvent *e) diff --git a/src/Mod/PartDesign/Gui/TaskChamferParameters.h b/src/Mod/PartDesign/Gui/TaskChamferParameters.h index c33c47c14f..436c879c0c 100644 --- a/src/Mod/PartDesign/Gui/TaskChamferParameters.h +++ b/src/Mod/PartDesign/Gui/TaskChamferParameters.h @@ -67,7 +67,7 @@ protected: private: void setUpUI(PartDesign::Chamfer* pcChamfer); - Ui_TaskChamferParameters* ui; + std::unique_ptr ui; }; /// simulation dialog for the TaskView diff --git a/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp b/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp index 3429c99be6..3a9298fc13 100644 --- a/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp @@ -56,10 +56,10 @@ using namespace Gui; TaskDraftParameters::TaskDraftParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, false, true, parent) + , ui(new Ui_TaskDraftParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskDraftParameters(); ui->setupUi(proxy); this->groupLayout()->addWidget(proxy); @@ -319,8 +319,6 @@ TaskDraftParameters::~TaskDraftParameters() { Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); - - delete ui; } bool TaskDraftParameters::event(QEvent *e) diff --git a/src/Mod/PartDesign/Gui/TaskDraftParameters.h b/src/Mod/PartDesign/Gui/TaskDraftParameters.h index dcff0bbdaf..390e9c278c 100644 --- a/src/Mod/PartDesign/Gui/TaskDraftParameters.h +++ b/src/Mod/PartDesign/Gui/TaskDraftParameters.h @@ -60,7 +60,7 @@ protected: virtual void onSelectionChanged(const Gui::SelectionChanges& msg); private: - Ui_TaskDraftParameters* ui; + std::unique_ptr ui; }; /// simulation dialog for the TaskView diff --git a/src/Mod/PartDesign/Gui/TaskFeaturePick.cpp b/src/Mod/PartDesign/Gui/TaskFeaturePick.cpp index fd49cc49d3..905323e59b 100644 --- a/src/Mod/PartDesign/Gui/TaskFeaturePick.cpp +++ b/src/Mod/PartDesign/Gui/TaskFeaturePick.cpp @@ -26,6 +26,7 @@ #ifndef _PreComp_ # include # include +# include #endif #include @@ -79,6 +80,7 @@ const QString TaskFeaturePick::getFeatureStatusString(const featureStatus st) TaskFeaturePick::TaskFeaturePick(std::vector& objects, const std::vector& status, + bool singleFeatureSelect, QWidget* parent) : TaskBox(Gui::BitmapFactory().pixmap("edit-select-box"), tr("Select feature"), true, parent) @@ -96,6 +98,12 @@ TaskFeaturePick::TaskFeaturePick(std::vector& objects, connect(ui->radioDependent, SIGNAL(toggled(bool)), this, SLOT(onUpdate(bool))); connect(ui->radioXRef, SIGNAL(toggled(bool)), this, SLOT(onUpdate(bool))); connect(ui->listWidget, SIGNAL(itemSelectionChanged()), this, SLOT(onItemSelectionChanged())); + connect(ui->listWidget, SIGNAL(itemDoubleClicked(QListWidgetItem *)), this, SLOT(onDoubleClick(QListWidgetItem *))); + + + if (!singleFeatureSelect) { + ui->listWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); + } enum { axisBit=0, planeBit = 1}; @@ -273,7 +281,6 @@ std::vector TaskFeaturePick::buildFeatures() result.push_back(obj); } - break; } index++; @@ -471,6 +478,18 @@ void TaskFeaturePick::onItemSelectionChanged() doSelection = false; } +void TaskFeaturePick::onDoubleClick(QListWidgetItem *item) +{ + if (doSelection) + return; + doSelection = true; + QString t = item->data(Qt::UserRole).toString(); + Gui::Selection().addSelection(documentName.c_str(), t.toLatin1()); + doSelection = false; + + QMetaObject::invokeMethod(qobject_cast(&Gui::Control()), "accept", Qt::QueuedConnection); +} + void TaskFeaturePick::slotDeletedObject(const Gui::ViewProviderDocumentObject& Obj) { std::vector::iterator it; @@ -510,10 +529,11 @@ TaskDlgFeaturePick::TaskDlgFeaturePick( std::vector &objec const std::vector &status, boost::function)> afunc, boost::function)> wfunc, + bool singleFeatureSelect, boost::function abortfunc /* = NULL */ ) : TaskDialog(), accepted(false) { - pick = new TaskFeaturePick(objects, status); + pick = new TaskFeaturePick(objects, status, singleFeatureSelect); Content.push_back(pick); acceptFunction = afunc; @@ -572,4 +592,5 @@ void TaskDlgFeaturePick::showExternal(bool val) } + #include "moc_TaskFeaturePick.cpp" diff --git a/src/Mod/PartDesign/Gui/TaskFeaturePick.h b/src/Mod/PartDesign/Gui/TaskFeaturePick.h index eef80dc5c2..3f1ed083f6 100644 --- a/src/Mod/PartDesign/Gui/TaskFeaturePick.h +++ b/src/Mod/PartDesign/Gui/TaskFeaturePick.h @@ -31,6 +31,7 @@ #include #include +#include namespace PartDesignGui { @@ -55,22 +56,24 @@ public: afterTip }; - TaskFeaturePick(std::vector &objects, + TaskFeaturePick(std::vector &objects, const std::vector &status, + bool singleFeatureSelect, QWidget *parent = 0); - + ~TaskFeaturePick(); std::vector getFeatures(); std::vector buildFeatures(); void showExternal(bool val); - + static App::DocumentObject* makeCopy(App::DocumentObject* obj, std::string sub, bool independent); - + protected Q_SLOTS: void onUpdate(bool); void onSelectionChanged(const Gui::SelectionChanges& msg); void onItemSelectionChanged(); + void onDoubleClick(QListWidgetItem *item); protected: /** Notifies when the object is about to be removed. */ @@ -81,7 +84,7 @@ protected: virtual void slotDeleteDocument(const Gui::Document& Doc); private: - Ui_TaskFeaturePick* ui; + std::unique_ptr ui; QWidget* proxy; std::vector origins; bool doSelection; @@ -105,7 +108,8 @@ public: const std::vector &status, boost::function)> acceptfunc, boost::function)> workfunc, - boost::function abortfunc = 0 ); + bool singleFeatureSelect, + boost::function abortfunc = 0); ~TaskDlgFeaturePick(); public: @@ -117,16 +121,17 @@ public: virtual bool accept(); /// is called by the framework if the dialog is rejected (Cancel) virtual bool reject(); - /// is called by the framework if the user presses the help button + /// is called by the framework if the user presses the help button virtual bool isAllowedAlterDocument(void) const { return false; } - + void showExternal(bool val); - /// returns for Close and Help button + /// returns for Close and Help button virtual QDialogButtonBox::StandardButtons getStandardButtons(void) const { return QDialogButtonBox::Ok|QDialogButtonBox::Cancel; } + protected: TaskFeaturePick *pick; bool accepted; diff --git a/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp b/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp index 1749edbb8d..0e8b8e424b 100644 --- a/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp @@ -54,10 +54,10 @@ using namespace Gui; TaskFilletParameters::TaskFilletParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, true, true, parent) + , ui(new Ui_TaskFilletParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskFilletParameters(); ui->setupUi(proxy); this->groupLayout()->addWidget(proxy); @@ -214,8 +214,6 @@ TaskFilletParameters::~TaskFilletParameters() { Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); - - delete ui; } bool TaskFilletParameters::event(QEvent *e) diff --git a/src/Mod/PartDesign/Gui/TaskFilletParameters.h b/src/Mod/PartDesign/Gui/TaskFilletParameters.h index ea4fb684e6..e4eeb57ab0 100644 --- a/src/Mod/PartDesign/Gui/TaskFilletParameters.h +++ b/src/Mod/PartDesign/Gui/TaskFilletParameters.h @@ -53,7 +53,7 @@ protected: virtual void onSelectionChanged(const Gui::SelectionChanges& msg); private: - Ui_TaskFilletParameters* ui; + std::unique_ptr ui; }; /// simulation dialog for the TaskView diff --git a/src/Mod/PartDesign/Gui/TaskHelixParameters.cpp b/src/Mod/PartDesign/Gui/TaskHelixParameters.cpp new file mode 100644 index 0000000000..ca17897522 --- /dev/null +++ b/src/Mod/PartDesign/Gui/TaskHelixParameters.cpp @@ -0,0 +1,511 @@ +/*************************************************************************** + * Copyright (c) 2011 Juergen Riegel * + * 2020 David Österberg * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#ifndef _PreComp_ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ReferenceSelection.h" +#include "Utils.h" + +#include "ui_TaskHelixParameters.h" +#include "TaskHelixParameters.h" + +using namespace PartDesignGui; +using namespace Gui; + + +/* TRANSLATOR PartDesignGui::TaskHelixParameters */ + +TaskHelixParameters::TaskHelixParameters(PartDesignGui::ViewProviderHelix *HelixView, QWidget *parent) + : TaskSketchBasedParameters(HelixView, parent, "PartDesign_Additive_Helix", tr("Helix parameters")), + ui (new Ui_TaskHelixParameters) +{ + // we need a separate container widget to add all controls to + proxy = new QWidget(this); + ui->setupUi(proxy); + QMetaObject::connectSlotsByName(this); + + connect(ui->pitch, SIGNAL(valueChanged(double)), + this, SLOT(onPitchChanged(double))); + connect(ui->height, SIGNAL(valueChanged(double)), + this, SLOT(onHeightChanged(double))); + connect(ui->turns, SIGNAL(valueChanged(double)), + this, SLOT(onTurnsChanged(double))); + connect(ui->coneAngle, SIGNAL(valueChanged(double)), + this, SLOT(onAngleChanged(double))); + connect(ui->axis, SIGNAL(activated(int)), + this, SLOT(onAxisChanged(int))); + connect(ui->checkBoxLeftHanded, SIGNAL(toggled(bool)), + this, SLOT(onLeftHandedChanged(bool))); + connect(ui->checkBoxReversed, SIGNAL(toggled(bool)), + this, SLOT(onReversedChanged(bool))); + connect(ui->checkBoxUpdateView, SIGNAL(toggled(bool)), + this, SLOT(onUpdateView(bool))); + connect(ui->inputMode, SIGNAL(activated(int)), + this, SLOT(onModeChanged(int))); + connect(ui->checkBoxOutside, SIGNAL(toggled(bool)), + this, SLOT(onOutsideChanged(bool))); + + this->groupLayout()->addWidget(proxy); + + // Temporarily prevent unnecessary feature recomputes + ui->axis->blockSignals(true); + ui->pitch->blockSignals(true); + ui->height->blockSignals(true); + ui->turns->blockSignals(true); + ui->coneAngle->blockSignals(true); + ui->checkBoxLeftHanded->blockSignals(true); + ui->checkBoxReversed->blockSignals(true); + ui->checkBoxOutside->blockSignals(true); + + //bind property mirrors + PartDesign::ProfileBased* pcFeat = static_cast(vp->getObject()); + + PartDesign::Helix* rev = static_cast(vp->getObject()); + + if (!(rev->HasBeenEdited).getValue()) { + rev->proposeParameters(); + recomputeFeature(); + } + + this->propAngle = &(rev->Angle); + this->propPitch = &(rev->Pitch); + this->propHeight = &(rev->Height); + this->propTurns = &(rev->Turns); + this->propReferenceAxis = &(rev->ReferenceAxis); + this->propLeftHanded = &(rev->LeftHanded); + this->propReversed = &(rev->Reversed); + this->propMode = &(rev->Mode); + this->propOutside = &(rev->Outside); + + double pitch = propPitch->getValue(); + double height = propHeight->getValue(); + double turns = propTurns->getValue(); + double angle = propAngle->getValue(); + bool leftHanded = propLeftHanded->getValue(); + bool reversed = propReversed->getValue(); + int index = propMode->getValue(); + bool outside = propOutside->getValue(); + + ui->pitch->setValue(pitch); + ui->height->setValue(height); + ui->turns->setValue(turns); + ui->coneAngle->setValue(angle); + ui->checkBoxLeftHanded->setChecked(leftHanded); + ui->checkBoxReversed->setChecked(reversed); + ui->inputMode->setCurrentIndex(index); + ui->checkBoxOutside->setChecked(outside); + + blockUpdate = false; + updateUI(); + + // enable use of parametric expressions for the numerical fields + ui->pitch->bind(static_cast(pcFeat)->Pitch); + ui->height->bind(static_cast(pcFeat)->Height); + ui->turns->bind(static_cast(pcFeat)->Turns); + ui->coneAngle->bind(static_cast(pcFeat)->Angle); + + ui->axis->blockSignals(false); + ui->pitch->blockSignals(false); + ui->height->blockSignals(false); + ui->turns->blockSignals(false); + ui->coneAngle->blockSignals(false); + ui->checkBoxLeftHanded->blockSignals(false); + ui->checkBoxReversed->blockSignals(false); + ui->checkBoxOutside->blockSignals(false); + + setFocus (); + + //show the parts coordinate system axis for selection + PartDesign::Body * body = PartDesign::Body::findBodyOf ( vp->getObject () ); + if(body) { + try { + App::Origin *origin = body->getOrigin(); + ViewProviderOrigin* vpOrigin; + vpOrigin = static_cast(Gui::Application::Instance->getViewProvider(origin)); + vpOrigin->setTemporaryVisibility(true, false); + } catch (const Base::Exception &ex) { + ex.ReportException(); + } + } +} + +void TaskHelixParameters::fillAxisCombo(bool forceRefill) +{ + bool oldVal_blockUpdate = blockUpdate; + blockUpdate = true; + + if (axesInList.empty()) + forceRefill = true;//not filled yet, full refill + + if (forceRefill){ + ui->axis->clear(); + + this->axesInList.clear(); + + //add sketch axes + PartDesign::ProfileBased* pcFeat = static_cast(vp->getObject()); + Part::Part2DObject* pcSketch = dynamic_cast(pcFeat->Profile.getValue()); + if (pcSketch){ + addAxisToCombo(pcSketch,"V_Axis",QObject::tr("Vertical sketch axis")); + addAxisToCombo(pcSketch,"H_Axis",QObject::tr("Horizontal sketch axis")); + for (int i=0; i < pcSketch->getAxisCount(); i++) { + QString itemText = QObject::tr("Construction line %1").arg(i+1); + std::stringstream sub; + sub << "Axis" << i; + addAxisToCombo(pcSketch,sub.str(),itemText); + } + } + + //add part axes + PartDesign::Body * body = PartDesign::Body::findBodyOf ( pcFeat ); + if (body) { + try { + App::Origin* orig = body->getOrigin(); + addAxisToCombo(orig->getX(),"",tr("Base X axis")); + addAxisToCombo(orig->getY(),"",tr("Base Y axis")); + addAxisToCombo(orig->getZ(),"",tr("Base Z axis")); + } catch (const Base::Exception &ex) { + ex.ReportException(); + } + } + + //add "Select reference" + addAxisToCombo(0,std::string(),tr("Select reference...")); + }//endif forceRefill + + //add current link, if not in list + //first, figure out the item number for current axis + int indexOfCurrent = -1; + App::DocumentObject* ax = propReferenceAxis->getValue(); + const std::vector &subList = propReferenceAxis->getSubValues(); + for (size_t i = 0; i < axesInList.size(); i++) { + if (ax == axesInList[i]->getValue() && subList == axesInList[i]->getSubValues()) + indexOfCurrent = i; + } + if (indexOfCurrent == -1 && ax) { + assert(subList.size() <= 1); + std::string sub; + if (!subList.empty()) + sub = subList[0]; + addAxisToCombo(ax, sub, getRefStr(ax, subList)); + indexOfCurrent = axesInList.size()-1; + } + + //highlight current. + if (indexOfCurrent != -1) + ui->axis->setCurrentIndex(indexOfCurrent); + + blockUpdate = oldVal_blockUpdate; +} + +void TaskHelixParameters::addAxisToCombo(App::DocumentObject* linkObj, + std::string linkSubname, + QString itemText) +{ + this->ui->axis->addItem(itemText); + this->axesInList.emplace_back(new App::PropertyLinkSub); + App::PropertyLinkSub &lnk = *(axesInList[axesInList.size()-1]); + lnk.setValue(linkObj,std::vector(1,linkSubname)); +} + +void TaskHelixParameters::updateUI() +{ + fillAxisCombo(); + + auto pcHelix = static_cast(vp->getObject()); + auto status = std::string(pcHelix->getStatusString()); + if (status.compare("Valid")==0 || status.compare("Touched")==0) { + if (pcHelix->safePitch() > propPitch->getValue()) + status = "Warning: helix might be self intersecting"; + else + status = ""; + } + ui->labelMessage->setText(QString::fromUtf8(status.c_str())); + + bool isPitchVisible = false; + bool isHeightVisible = false; + bool isTurnsVisible = false; + bool isOutsideVisible = false; + + if(pcHelix->getAddSubType() == PartDesign::FeatureAddSub::Subtractive) + isOutsideVisible = true; + + switch (propMode->getValue()) { + case 0: + isPitchVisible = true; + isHeightVisible = true; + break; + case 1: + isPitchVisible = true; + isTurnsVisible = true; + break; + default: + isHeightVisible = true; + isTurnsVisible = true; + } + + ui->pitch->setVisible(isPitchVisible); + ui->labelPitch->setVisible(isPitchVisible); + + ui->height->setVisible(isHeightVisible); + ui->labelHeight->setVisible(isHeightVisible); + + ui->turns->setVisible(isTurnsVisible); + ui->labelTurns->setVisible(isTurnsVisible); + + ui->checkBoxOutside->setVisible(isOutsideVisible); + +} + +void TaskHelixParameters::onSelectionChanged(const Gui::SelectionChanges& msg) +{ + if (msg.Type == Gui::SelectionChanges::AddSelection) { + + exitSelectionMode(); + std::vector axis; + App::DocumentObject* selObj; + if (getReferencedSelection(vp->getObject(), msg, selObj, axis) && selObj) { + propReferenceAxis->setValue(selObj, axis); + recomputeFeature(); + updateUI(); + } + } +} + + +void TaskHelixParameters::onPitchChanged(double len) +{ + propPitch->setValue(len); + recomputeFeature(); + updateUI(); +} + +void TaskHelixParameters::onHeightChanged(double len) +{ + propHeight->setValue(len); + recomputeFeature(); + updateUI(); +} + +void TaskHelixParameters::onTurnsChanged(double len) +{ + propTurns->setValue(len); + recomputeFeature(); + updateUI(); +} + +void TaskHelixParameters::onAngleChanged(double len) +{ + propAngle->setValue(len); + recomputeFeature(); + updateUI(); +} + +void TaskHelixParameters::onAxisChanged(int num) +{ + PartDesign::ProfileBased* pcHelix = static_cast(vp->getObject()); + + if (axesInList.empty()) + return; + + App::DocumentObject *oldRefAxis = propReferenceAxis->getValue(); + std::vector oldSubRefAxis = propReferenceAxis->getSubValues(); + std::string oldRefName; + if (!oldSubRefAxis.empty()) + oldRefName = oldSubRefAxis.front(); + + App::PropertyLinkSub &lnk = *(axesInList[num]); + if (lnk.getValue() == 0) { + // enter reference selection mode + TaskSketchBasedParameters::onSelectReference(true, true, false, true); + } else { + if (!pcHelix->getDocument()->isIn(lnk.getValue())){ + Base::Console().Error("Object was deleted\n"); + return; + } + propReferenceAxis->Paste(lnk); + exitSelectionMode(); + } + + try { + App::DocumentObject *newRefAxis = propReferenceAxis->getValue(); + const std::vector &newSubRefAxis = propReferenceAxis->getSubValues(); + std::string newRefName; + if (!newSubRefAxis.empty()) + newRefName = newSubRefAxis.front(); + + if (oldRefAxis != newRefAxis || + oldSubRefAxis.size() != newSubRefAxis.size() || + oldRefName != newRefName) { + bool reversed = propReversed->getValue(); + if (reversed != propReversed->getValue()) { + propReversed->setValue(reversed); + ui->checkBoxReversed->blockSignals(true); + ui->checkBoxReversed->setChecked(reversed); + ui->checkBoxReversed->blockSignals(false); + } + } + + recomputeFeature(); + } + catch (const Base::Exception& e) { + e.ReportException(); + } +} + +void TaskHelixParameters::onModeChanged(int index) +{ + + propMode->setValue(index); + + ui->pitch->setValue(propPitch->getValue()); + ui->height->setValue(propHeight->getValue()); + ui->turns->setValue((propHeight->getValue())/(propPitch->getValue())); + + recomputeFeature(); + updateUI(); +} + +void TaskHelixParameters::onLeftHandedChanged(bool on) +{ + propLeftHanded->setValue(on); + recomputeFeature(); +} + +void TaskHelixParameters::onReversedChanged(bool on) +{ + propReversed->setValue(on); + recomputeFeature(); + updateUI(); +} + +void TaskHelixParameters::onOutsideChanged(bool on) +{ + propOutside->setValue(on); + recomputeFeature(); + updateUI(); +} + + +TaskHelixParameters::~TaskHelixParameters() +{ + try { + //hide the parts coordinate system axis for selection + PartDesign::Body * body = vp ? PartDesign::Body::findBodyOf(vp->getObject()) : 0; + if (body) { + App::Origin *origin = body->getOrigin(); + ViewProviderOrigin* vpOrigin; + vpOrigin = static_cast(Gui::Application::Instance->getViewProvider(origin)); + vpOrigin->resetTemporaryVisibility(); + } + } catch (const Base::Exception &ex) { + ex.ReportException(); + } + +} + +void TaskHelixParameters::changeEvent(QEvent *e) +{ + TaskBox::changeEvent(e); + if (e->type() == QEvent::LanguageChange) { + ui->retranslateUi(proxy); + } +} + +void TaskHelixParameters::getReferenceAxis(App::DocumentObject*& obj, std::vector& sub) const +{ + if (axesInList.empty()) + throw Base::RuntimeError("Not initialized!"); + + int num = ui->axis->currentIndex(); + const App::PropertyLinkSub &lnk = *(axesInList[num]); + if (lnk.getValue() == 0) { + throw Base::RuntimeError("Still in reference selection mode; reference wasn't selected yet"); + } else { + PartDesign::ProfileBased* pcRevolution = static_cast(vp->getObject()); + if (!pcRevolution->getDocument()->isIn(lnk.getValue())){ + throw Base::RuntimeError("Object was deleted"); + } + + obj = lnk.getValue(); + sub = lnk.getSubValues(); + } +} + +// this is used for logging the command fully when recording macros +void TaskHelixParameters::apply() +{ + std::vector sub; + App::DocumentObject* obj; + getReferenceAxis(obj, sub); + std::string axis = buildLinkSingleSubPythonStr(obj, sub); + auto tobj = vp->getObject(); + FCMD_OBJ_CMD(tobj,"ReferenceAxis = " << axis); + FCMD_OBJ_CMD(tobj,"Mode = " << propMode->getValue()); + FCMD_OBJ_CMD(tobj,"Pitch = " << propPitch->getValue()); + FCMD_OBJ_CMD(tobj,"Height = " << propHeight->getValue()); + FCMD_OBJ_CMD(tobj,"Turns = " << propTurns->getValue()); + FCMD_OBJ_CMD(tobj,"Angle = " << propAngle->getValue()); + FCMD_OBJ_CMD(tobj,"LeftHanded = " << (propLeftHanded->getValue() ? 1 : 0)); + FCMD_OBJ_CMD(tobj,"Reversed = " << (propReversed->getValue() ? 1 : 0)); +} + + +//************************************************************************** +//************************************************************************** +// TaskDialog +//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +TaskDlgHelixParameters::TaskDlgHelixParameters(ViewProviderHelix *HelixView) + : TaskDlgSketchBasedParameters(HelixView) +{ + assert(HelixView); + Content.push_back(new TaskHelixParameters(HelixView)); +} + + +#include "moc_TaskHelixParameters.cpp" diff --git a/src/Mod/PartDesign/Gui/TaskHelixParameters.h b/src/Mod/PartDesign/Gui/TaskHelixParameters.h new file mode 100644 index 0000000000..aaa1c7ed7b --- /dev/null +++ b/src/Mod/PartDesign/Gui/TaskHelixParameters.h @@ -0,0 +1,131 @@ +/*************************************************************************** + * Copyright (c) 2011 Juergen Riegel * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef GUI_TASKVIEW_TaskHelixParameters_H +#define GUI_TASKVIEW_TaskHelixParameters_H + +#include +#include +#include + +#include "TaskSketchBasedParameters.h" +#include "ViewProviderHelix.h" + +class Ui_TaskHelixParameters; + +namespace App { +class Property; +} + +namespace Gui { +class ViewProvider; +} + +namespace PartDesignGui { + + + +class TaskHelixParameters : public TaskSketchBasedParameters +{ + Q_OBJECT + +public: + TaskHelixParameters(ViewProviderHelix *HelixView,QWidget *parent = 0); + ~TaskHelixParameters(); + + virtual void apply() override; + + /** + * @brief fillAxisCombo fills the combo and selects the item according to + * current value of revolution object's axis reference. + * @param forceRefill if true, the combo box will be completely refilled. If + * false, the current value of revolution object's axis will be added to the + * list (if necessary), and selected. If the list is empty, it will be refilled anyway. + */ + void fillAxisCombo(bool forceRefill = false); + void addAxisToCombo(App::DocumentObject *linkObj, std::string linkSubname, QString itemText); + +private Q_SLOTS: + void onPitchChanged(double); + void onHeightChanged(double); + void onTurnsChanged(double); + void onAngleChanged(double); + void onAxisChanged(int); + void onLeftHandedChanged(bool); + void onReversedChanged(bool); + void onModeChanged(int); + void onOutsideChanged(bool); + + +protected: + void onSelectionChanged(const Gui::SelectionChanges& msg) override; + void changeEvent(QEvent *e) override; + bool updateView() const; + void getReferenceAxis(App::DocumentObject *&obj, std::vector &sub) const; + + + //mirrors of helixes's properties + App::PropertyLength* propPitch; + App::PropertyLength* propHeight; + App::PropertyFloat* propTurns; + App::PropertyBool* propLeftHanded; + App::PropertyBool* propReversed; + App::PropertyLinkSub* propReferenceAxis; + App::PropertyAngle* propAngle; + App::PropertyEnumeration* propMode; + App::PropertyBool* propOutside; + + +private: + void updateUI(); + +private: + QWidget* proxy; + std::unique_ptr ui; + + /** + * @brief axesInList is the list of links corresponding to axis combo; must + * be kept in sync with the combo. A special value of zero-pointer link is + * for "Select axis" item. + * + * It is a list of pointers, because properties prohibit assignment. Use new + * when adding stuff, and delete when removing stuff. + */ + std::vector> axesInList; +}; + +/// simulation dialog for the TaskView +class TaskDlgHelixParameters : public TaskDlgSketchBasedParameters +{ + Q_OBJECT + +public: + TaskDlgHelixParameters(ViewProviderHelix *HelixView); + + ViewProviderHelix* getHelixView() const + { return static_cast(vp); } +}; + +} //namespace PartDesignGui + +#endif // GUI_TASKVIEW_TaskHelixParameters_H diff --git a/src/Mod/PartDesign/Gui/TaskHelixParameters.ui b/src/Mod/PartDesign/Gui/TaskHelixParameters.ui new file mode 100644 index 0000000000..ac4215ac14 --- /dev/null +++ b/src/Mod/PartDesign/Gui/TaskHelixParameters.ui @@ -0,0 +1,303 @@ + + + PartDesignGui::TaskHelixParameters + + + + 0 + 0 + 278 + 193 + + + + Form + + + + + + + + + Status: + + + + + + + Valid + + + + + + + + + + + + Axis: + + + + + + + + Base X axis + + + + + Base Y axis + + + + + Base Z axis + + + + + Horizontal sketch axis + + + + + Vertical sketch axis + + + + + Select reference... + + + + + + + + + + + + + Mode: + + + + + + + + Pitch-Height + + + + + Pitch-Turns + + + + + Height-Turns + + + + + + + + + + + + + Pitch: + + + + + + + false + + + mm + + + 0.000000000000000 + + + 1.000000000000000 + + + 10.000000000000000 + + + + + + + + + + + + Height: + + + + + + + false + + + mm + + + 0.000000000000000 + + + 1.000000000000000 + + + 30.000000000000000 + + + + + + + + + + + + Turns: + + + + + + + false + + + 0.000000000000000 + + + 1.000000000000000 + + + 3.0000000000000 + + + + + + + + + + + + Cone angle: + + + + + + + false + + + deg + + + -89.000000000000000 + + + 89.000000000000000 + + + 5.000000000000000 + + + 0.000000000000000 + + + + + + + + + + true + + + Left handed + + + + + + + + true + + + Reversed + + + + + + + + Remove outside of profile + + + false + + + + + + + + Qt::Horizontal + + + + + + + + Update view + + + true + + + + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
+ + +
diff --git a/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp b/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp index 407f5465b5..dbd5a866bd 100644 --- a/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp @@ -57,27 +57,16 @@ namespace bp = boost::placeholders; #endif TaskHoleParameters::TaskHoleParameters(ViewProviderHole *HoleView, QWidget *parent) - : TaskSketchBasedParameters(HoleView, parent, "PartDesign_Hole",tr("Hole parameters")) + : TaskSketchBasedParameters(HoleView, parent, "PartDesign_Hole", tr("Hole parameters")) , observer(new Observer(this, static_cast(vp->getObject()))) , isApplying(false) + , ui(new Ui_TaskHoleParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskHoleParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); - /* Remove actual threading parameters for now */ - ui->ModelActualThread->setVisible(false); - ui->ThreadPitch->setVisible(false); - ui->ThreadCutOffInner->setVisible(false); - ui->ThreadCutOffOuter->setVisible(false); - ui->ThreadAngle->setVisible(false); - ui->label_Pitch->setVisible(false); - ui->label_CutoffInner->setVisible(false); - ui->label_CutoffOuter->setVisible(false); - ui->label_Angle->setVisible(false); - ui->ThreadType->addItem(tr("None"), QByteArray("None")); ui->ThreadType->addItem(tr("ISO metric regular profile"), QByteArray("ISO")); ui->ThreadType->addItem(tr("ISO metric fine profile"), QByteArray("ISO")); @@ -88,11 +77,6 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole *HoleView, QWidget *pare // read values from the hole properties PartDesign::Hole* pcHole = static_cast(vp->getObject()); ui->Threaded->setChecked(pcHole->Threaded.getValue()); - ui->ModelActualThread->setChecked(pcHole->ModelActualThread.getValue()); - ui->ThreadPitch->setValue(pcHole->ThreadPitch.getValue()); - ui->ThreadAngle->setValue(pcHole->ThreadAngle.getValue()); - ui->ThreadCutOffInner->setValue(pcHole->ThreadCutOffInner.getValue()); - ui->ThreadCutOffOuter->setValue(pcHole->ThreadCutOffOuter.getValue()); ui->ThreadType->setCurrentIndex(pcHole->ThreadType.getValue()); ui->ThreadSize->clear(); const char** cursor = pcHole->ThreadSize.getEnums(); @@ -131,34 +115,16 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole *HoleView, QWidget *pare ++cursor; } ui->HoleCutType->setCurrentIndex(pcHole->HoleCutType.getValue()); + ui->HoleCutCustomValues->setChecked(pcHole->HoleCutCustomValues.getValue()); + ui->HoleCutCustomValues->setDisabled(pcHole->HoleCutCustomValues.isReadOnly()); + // HoleCutDiameter must not be smaller or equal than the Diameter + ui->HoleCutDiameter->setMinimum(pcHole->Diameter.getValue() + 0.1); ui->HoleCutDiameter->setValue(pcHole->HoleCutDiameter.getValue()); + ui->HoleCutDiameter->setDisabled(pcHole->HoleCutDiameter.isReadOnly()); ui->HoleCutDepth->setValue(pcHole->HoleCutDepth.getValue()); + ui->HoleCutDepth->setDisabled(pcHole->HoleCutDepth.isReadOnly()); ui->HoleCutCountersinkAngle->setValue(pcHole->HoleCutCountersinkAngle.getValue()); - - std::string holeCutType; - if (pcHole->HoleCutType.isValid()) - holeCutType = pcHole->HoleCutType.getValueAsString(); - - if (holeCutType == "None") { - ui->HoleCutDiameter->setEnabled(false); - ui->HoleCutDepth->setEnabled(false); - ui->HoleCutCountersinkAngle->setEnabled(false); - } - else if (holeCutType == "Counterbore") { - ui->HoleCutDiameter->setEnabled(true); - ui->HoleCutDepth->setEnabled(true); - ui->HoleCutCountersinkAngle->setEnabled(false); - } - else if (holeCutType == "Countersink") { - ui->HoleCutDiameter->setEnabled(true); - ui->HoleCutDepth->setEnabled(true); - ui->HoleCutCountersinkAngle->setEnabled(true); - } - else { // screw definition - ui->HoleCutDiameter->setEnabled(true); - ui->HoleCutDepth->setEnabled(true); - ui->HoleCutCountersinkAngle->setEnabled(true); - } + ui->HoleCutCountersinkAngle->setDisabled(pcHole->HoleCutCountersinkAngle.isReadOnly()); ui->DepthType->setCurrentIndex(pcHole->DepthType.getValue()); ui->Depth->setValue(pcHole->Depth.getValue()); @@ -167,6 +133,29 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole *HoleView, QWidget *pare else ui->drillPointAngled->setChecked(true); ui->DrillPointAngle->setValue(pcHole->DrillPointAngle.getValue()); + ui->DrillForDepth->setChecked(pcHole->DrillForDepth.getValue()); + // drill point settings are only enabled (sensible) if type is 'Dimension' + if (std::string(pcHole->DepthType.getValueAsString()) == "Dimension") { + ui->drillPointFlat->setEnabled(true); + ui->drillPointAngled->setEnabled(true); + ui->DrillPointAngle->setEnabled(true); + ui->DrillForDepth->setEnabled(true); + } + else { + ui->drillPointFlat->setEnabled(false); + ui->drillPointAngled->setEnabled(false); + ui->DrillPointAngle->setEnabled(false); + ui->DrillForDepth->setEnabled(false); + } + // drill point is sensible but flat, disable angle and option + if (!ui->drillPointFlat->isChecked()) { + ui->DrillPointAngle->setEnabled(true); + ui->DrillForDepth->setEnabled(true); + } + else { + ui->DrillPointAngle->setEnabled(false); + ui->DrillForDepth->setEnabled(false); + } ui->Tapered->setChecked(pcHole->Tapered.getValue()); // Angle is only enabled (sensible) if tapered ui->TaperedAngle->setEnabled(pcHole->Tapered.getValue()); @@ -175,18 +164,14 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole *HoleView, QWidget *pare connect(ui->Threaded, SIGNAL(clicked(bool)), this, SLOT(threadedChanged())); connect(ui->ThreadType, SIGNAL(currentIndexChanged(int)), this, SLOT(threadTypeChanged(int))); - connect(ui->ModelActualThread, SIGNAL(clicked(bool)), this, SLOT(modelActualThreadChanged())); - connect(ui->ThreadPitch, SIGNAL(valueChanged(double)), this, SLOT(threadPitchChanged(double))); - connect(ui->ThreadAngle, SIGNAL(valueChanged(double)), this, SLOT(threadAngleChanged(double))); - connect(ui->ThreadCutOffInner, SIGNAL(valueChanged(double)), this, SLOT(threadCutOffInnerChanged(double))); - connect(ui->ThreadCutOffOuter, SIGNAL(valueChanged(double)), this, SLOT(threadCutOffOuterChanged(double))); connect(ui->ThreadSize, SIGNAL(currentIndexChanged(int)), this, SLOT(threadSizeChanged(int))); connect(ui->ThreadClass, SIGNAL(currentIndexChanged(int)), this, SLOT(threadClassChanged(int))); connect(ui->ThreadFit, SIGNAL(currentIndexChanged(int)), this, SLOT(threadFitChanged(int))); connect(ui->Diameter, SIGNAL(valueChanged(double)), this, SLOT(threadDiameterChanged(double))); connect(ui->directionRightHand, SIGNAL(clicked(bool)), this, SLOT(threadDirectionChanged())); connect(ui->directionLeftHand, SIGNAL(clicked(bool)), this, SLOT(threadDirectionChanged())); - connect(ui->HoleCutType, SIGNAL(currentIndexChanged(int)), this, SLOT(holeCutChanged(int))); + connect(ui->HoleCutType, SIGNAL(currentIndexChanged(int)), this, SLOT(holeCutTypeChanged(int))); + connect(ui->HoleCutCustomValues, SIGNAL(clicked(bool)), this, SLOT(holeCutCustomValuesChanged())); connect(ui->HoleCutDiameter, SIGNAL(valueChanged(double)), this, SLOT(holeCutDiameterChanged(double))); connect(ui->HoleCutDepth, SIGNAL(valueChanged(double)), this, SLOT(holeCutDepthChanged(double))); connect(ui->HoleCutCountersinkAngle, SIGNAL(valueChanged(double)), this, SLOT(holeCutCountersinkAngleChanged(double))); @@ -195,16 +180,13 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole *HoleView, QWidget *pare connect(ui->drillPointFlat, SIGNAL(clicked(bool)), this, SLOT(drillPointChanged())); connect(ui->drillPointAngled, SIGNAL(clicked(bool)), this, SLOT(drillPointChanged())); connect(ui->DrillPointAngle, SIGNAL(valueChanged(double)), this, SLOT(drillPointAngledValueChanged(double))); + connect(ui->DrillForDepth, SIGNAL(clicked(bool)), this, SLOT(drillForDepthChanged())); connect(ui->Tapered, SIGNAL(clicked(bool)), this, SLOT(taperedChanged())); connect(ui->Reversed, SIGNAL(clicked(bool)), this, SLOT(reversedChanged())); connect(ui->TaperedAngle, SIGNAL(valueChanged(double)), this, SLOT(taperedAngleChanged(double))); vp->show(); - ui->ThreadPitch->bind(pcHole->ThreadPitch); - ui->ThreadAngle->bind(pcHole->ThreadAngle); - ui->ThreadCutOffInner->bind(pcHole->ThreadCutOffInner); - ui->ThreadCutOffOuter->bind(pcHole->ThreadCutOffOuter); ui->Diameter->bind(pcHole->Diameter); ui->HoleCutDiameter->bind(pcHole->HoleCutDiameter); ui->HoleCutDepth->bind(pcHole->HoleCutDepth); @@ -221,7 +203,6 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole *HoleView, QWidget *pare TaskHoleParameters::~TaskHoleParameters() { - delete ui; } void TaskHoleParameters::threadedChanged() @@ -236,7 +217,7 @@ void TaskHoleParameters::modelActualThreadChanged() { PartDesign::Hole* pcHole = static_cast(vp->getObject()); - pcHole->ModelActualThread.setValue(ui->ModelActualThread->isChecked()); + pcHole->ModelActualThread.setValue(false); recomputeFeature(); } @@ -272,7 +253,7 @@ void TaskHoleParameters::threadCutOffOuterChanged(double value) recomputeFeature(); } -void TaskHoleParameters::holeCutChanged(int index) +void TaskHoleParameters::holeCutTypeChanged(int index) { if (index < 0) return; @@ -283,8 +264,65 @@ void TaskHoleParameters::holeCutChanged(int index) // therefore reset it, it will be reset to sensible values by setting the new HoleCutType pcHole->HoleCutDepth.setValue(0.0); + // when holeCutType was changed, reset HoleCutCustomValues to false because it should + // be a purpose decision to overwrite the normed values + // we will handle the case that there is no normed value later in this routine + ui->HoleCutCustomValues->setChecked(false); + pcHole->HoleCutCustomValues.setValue(false); + pcHole->HoleCutType.setValue(index); + + // recompute to get the info about the HoleCutType properties recomputeFeature(); + + // apply the result to the widgets + ui->HoleCutCustomValues->setDisabled(pcHole->HoleCutCustomValues.isReadOnly()); + ui->HoleCutCustomValues->setChecked(pcHole->HoleCutCustomValues.getValue()); + + // HoleCutCustomValues is only enabled for screw definitions + // we must do this after recomputeFeature() because this gives us the info if + // the type is a countersink and thus if HoleCutCountersinkAngle can be enabled + std::string HoleCutTypeString = pcHole->HoleCutType.getValueAsString(); + if (HoleCutTypeString == "None" || HoleCutTypeString == "Counterbore" + || HoleCutTypeString == "Countersink") { + ui->HoleCutCustomValues->setEnabled(false); + } + else { // screw definition + // we can have the case that we have no normed values + // in this case HoleCutCustomValues is read-only AND true + if (ui->HoleCutCustomValues->isChecked()) { + ui->HoleCutDiameter->setEnabled(true); + ui->HoleCutDepth->setEnabled(true); + if (!pcHole->HoleCutCountersinkAngle.isReadOnly()) + ui->HoleCutCountersinkAngle->setEnabled(true); + } + else { + ui->HoleCutDiameter->setEnabled(false); + ui->HoleCutDepth->setEnabled(false); + ui->HoleCutCountersinkAngle->setEnabled(false); + } + } +} + +void TaskHoleParameters::holeCutCustomValuesChanged() +{ + PartDesign::Hole* pcHole = static_cast(vp->getObject()); + + pcHole->HoleCutCustomValues.setValue(ui->HoleCutCustomValues->isChecked()); + + if (ui->HoleCutCustomValues->isChecked()) { + ui->HoleCutDiameter->setEnabled(true); + ui->HoleCutDepth->setEnabled(true); + if (!pcHole->HoleCutCountersinkAngle.isReadOnly()) + ui->HoleCutCountersinkAngle->setEnabled(true); + } + else { + ui->HoleCutDiameter->setEnabled(false); + ui->HoleCutDepth->setEnabled(false); + ui->HoleCutCountersinkAngle->setEnabled(false); + } + + recomputeFeature(); } void TaskHoleParameters::holeCutDiameterChanged(double value) @@ -324,7 +362,7 @@ void TaskHoleParameters::holeCutCountersinkAngleChanged(double value) { PartDesign::Hole* pcHole = static_cast(vp->getObject()); - pcHole->HoleCutCountersinkAngle.setValue((double)value); + pcHole->HoleCutCountersinkAngle.setValue(value); recomputeFeature(); } @@ -333,6 +371,20 @@ void TaskHoleParameters::depthChanged(int index) PartDesign::Hole* pcHole = static_cast(vp->getObject()); pcHole->DepthType.setValue(index); + + // disable drill point widgets if not 'Dimension' + if (std::string(pcHole->DepthType.getValueAsString()) == "Dimension") { + ui->drillPointFlat->setEnabled(true); + ui->drillPointAngled->setEnabled(true); + ui->DrillPointAngle->setEnabled(true); + ui->DrillForDepth->setEnabled(true); + } + else { + ui->drillPointFlat->setEnabled(false); + ui->drillPointAngled->setEnabled(false); + ui->DrillPointAngle->setEnabled(false); + ui->DrillForDepth->setEnabled(false); + } recomputeFeature(); } @@ -348,12 +400,17 @@ void TaskHoleParameters::drillPointChanged() { PartDesign::Hole* pcHole = static_cast(vp->getObject()); - if (sender() == ui->drillPointFlat) + if (sender() == ui->drillPointFlat) { pcHole->DrillPoint.setValue((long)0); - else if (sender() == ui->drillPointAngled) + ui->DrillForDepth->setEnabled(false); + } + else if (sender() == ui->drillPointAngled) { pcHole->DrillPoint.setValue((long)1); - else - assert( 0 ); + ui->DrillForDepth->setEnabled(true); + } + else { + assert(0); + } recomputeFeature(); } @@ -365,6 +422,14 @@ void TaskHoleParameters::drillPointAngledValueChanged(double value) recomputeFeature(); } +void TaskHoleParameters::drillForDepthChanged() +{ + PartDesign::Hole* pcHole = static_cast(vp->getObject()); + + pcHole->DrillForDepth.setValue(ui->DrillForDepth->isChecked()); + recomputeFeature(); +} + void TaskHoleParameters::taperedChanged() { PartDesign::Hole* pcHole = static_cast(vp->getObject()); @@ -414,31 +479,37 @@ void TaskHoleParameters::threadTypeChanged(int index) // now set the new type, this will reset the comboboxes to item 0 pcHole->ThreadType.setValue(index); - // Size - // the size for ISO type has either the form "M3x0.35" or just "M3" - // so we need to check if the size contains a 'x'. If yes, check if the string - // up to the 'x' is exists in the new list + // size and clearance if (TypeClass == QByteArray("ISO")) { + // the size for ISO type has either the form "M3x0.35" or just "M3" + // so we need to check if the size contains a 'x'. If yes, check if the string + // up to the 'x' is exists in the new list if (ThreadSizeString.indexOf(QString::fromLatin1("x")) > -1) { // we have an ISO fine size // cut of the part behind the 'x' ThreadSizeString = ThreadSizeString.left(ThreadSizeString.indexOf(QString::fromLatin1("x"))); } - // search if the string exists in the combobox int threadSizeIndex = ui->ThreadSize->findText(ThreadSizeString, Qt::MatchContains); if (threadSizeIndex > -1) { // we can set it ui->ThreadSize->setCurrentIndex(threadSizeIndex); } - } - - // for the UTS types the entries are the same - if (TypeClass == QByteArray("UTS")) { + // the names of the clearance types are different in ISO and UTS + ui->ThreadFit->setItemText(0, QCoreApplication::translate("TaskHoleParameters", "Standard", nullptr)); + ui->ThreadFit->setItemText(1, QCoreApplication::translate("TaskHoleParameters", "Close", nullptr)); + ui->ThreadFit->setItemText(2, QCoreApplication::translate("TaskHoleParameters", "Wide", nullptr)); + } + else if (TypeClass == QByteArray("UTS")) { + // for all UTS types the size entries are the same int threadSizeIndex = ui->ThreadSize->findText(ThreadSizeString, Qt::MatchContains); if (threadSizeIndex > -1) { ui->ThreadSize->setCurrentIndex(threadSizeIndex); } + // the names of the clearance types are different in ISO and UTS + ui->ThreadFit->setItemText(0, QCoreApplication::translate("TaskHoleParameters", "Normal", nullptr)); + ui->ThreadFit->setItemText(1, QCoreApplication::translate("TaskHoleParameters", "Close", nullptr)); + ui->ThreadFit->setItemText(2, QCoreApplication::translate("TaskHoleParameters", "Loose", nullptr)); } // Class and cut type @@ -449,6 +520,11 @@ void TaskHoleParameters::threadTypeChanged(int index) int holeCutIndex = ui->HoleCutType->findText(CutTypeString, Qt::MatchContains); if (holeCutIndex > -1) ui->HoleCutType->setCurrentIndex(holeCutIndex); + + // we must set the read-only state according to the new HoleCutType + holeCutTypeChanged(ui->HoleCutType->currentIndex()); + + recomputeFeature(); } void TaskHoleParameters::threadSizeChanged(int index) @@ -460,6 +536,10 @@ void TaskHoleParameters::threadSizeChanged(int index) pcHole->ThreadSize.setValue(index); recomputeFeature(); + + // apply the recompute result to the widgets + ui->HoleCutCustomValues->setDisabled(pcHole->HoleCutCustomValues.isReadOnly()); + ui->HoleCutCustomValues->setChecked(pcHole->HoleCutCustomValues.getValue()); } void TaskHoleParameters::threadClassChanged(int index) @@ -478,6 +558,10 @@ void TaskHoleParameters::threadDiameterChanged(double value) PartDesign::Hole* pcHole = static_cast(vp->getObject()); pcHole->Diameter.setValue(value); + + // HoleCutDiameter must not be smaller or equal than the Diameter + ui->HoleCutDiameter->setMinimum(value + 0.1); + recomputeFeature(); } @@ -528,46 +612,6 @@ void TaskHoleParameters::changedObject(const App::Document&, const App::Property } ui->Threaded->setDisabled(ro); } - else if (&Prop == &pcHole->ModelActualThread) { - if (ui->ModelActualThread->isChecked() ^ pcHole->ModelActualThread.getValue()) { - ui->ModelActualThread->blockSignals(true); - ui->ModelActualThread->setChecked(pcHole->ModelActualThread.getValue()); - ui->ModelActualThread->blockSignals(false); - } - ui->ModelActualThread->setDisabled(ro); - } - else if (&Prop == &pcHole->ThreadPitch) { - if (ui->ThreadPitch->value().getValue() != pcHole->ThreadPitch.getValue()) { - ui->ThreadPitch->blockSignals(true); - ui->ThreadPitch->setValue(pcHole->ThreadPitch.getValue()); - ui->ThreadPitch->blockSignals(false); - } - ui->ThreadPitch->setDisabled(ro); - } - else if (&Prop == &pcHole->ThreadAngle) { - if (ui->ThreadAngle->value().getValue() != pcHole->ThreadAngle.getValue()) { - ui->ThreadAngle->blockSignals(true); - ui->ThreadAngle->setValue(pcHole->ThreadAngle.getValue()); - ui->ThreadAngle->blockSignals(false); - } - ui->ThreadAngle->setDisabled(ro); - } - else if (&Prop == &pcHole->ThreadCutOffInner) { - if (ui->ThreadCutOffInner->value().getValue() != pcHole->ThreadCutOffInner.getValue()) { - ui->ThreadCutOffInner->blockSignals(true); - ui->ThreadCutOffInner->setValue(pcHole->ThreadCutOffInner.getValue()); - ui->ThreadCutOffInner->blockSignals(false); - } - ui->ThreadCutOffInner->setDisabled(ro); - } - else if (&Prop == &pcHole->ThreadCutOffOuter) { - if (ui->ThreadCutOffOuter->value().getValue() != pcHole->ThreadCutOffOuter.getValue()) { - ui->ThreadCutOffOuter->blockSignals(true); - ui->ThreadCutOffOuter->setValue(pcHole->ThreadCutOffOuter.getValue()); - ui->ThreadCutOffOuter->blockSignals(false); - } - ui->ThreadCutOffOuter->setDisabled(ro); - } else if (&Prop == &pcHole->ThreadType) { ui->ThreadType->setEnabled(true); @@ -742,6 +786,15 @@ void TaskHoleParameters::changedObject(const App::Document&, const App::Property } ui->DrillPointAngle->setDisabled(ro); } + else if (&Prop == &pcHole->DrillForDepth) { + ui->DrillForDepth->setEnabled(true); + if (ui->DrillForDepth->isChecked() ^ pcHole->DrillForDepth.getValue()) { + ui->DrillForDepth->blockSignals(true); + ui->DrillForDepth->setChecked(pcHole->DrillForDepth.getValue()); + ui->DrillForDepth->blockSignals(false); + } + ui->DrillForDepth->setDisabled(ro); + } else if (&Prop == &pcHole->Tapered) { ui->Tapered->setEnabled(true); if (ui->Tapered->isChecked() ^ pcHole->Tapered.getValue()) { @@ -821,6 +874,11 @@ long TaskHoleParameters::getHoleCutType() const return ui->HoleCutType->currentIndex(); } +bool TaskHoleParameters::getHoleCutCustomValues() const +{ + return ui->HoleCutCustomValues->isChecked(); +} + Base::Quantity TaskHoleParameters::getHoleCutDiameter() const { return ui->HoleCutDiameter->value(); @@ -866,6 +924,11 @@ bool TaskHoleParameters::getTapered() const return ui->Tapered->isChecked(); } +bool TaskHoleParameters::getDrillForDepth() const +{ + return ui->DrillForDepth->isChecked(); +} + Base::Quantity TaskHoleParameters::getTaperedAngle() const { return ui->TaperedAngle->value(); @@ -878,10 +941,6 @@ void TaskHoleParameters::apply() isApplying = true; - ui->ThreadPitch->apply(); - ui->ThreadAngle->apply(); - ui->ThreadCutOffInner->apply(); - ui->ThreadCutOffOuter->apply(); ui->Diameter->apply(); ui->HoleCutDiameter->apply(); ui->HoleCutDepth->apply(); @@ -906,10 +965,14 @@ void TaskHoleParameters::apply() FCMD_OBJ_CMD(obj,"ThreadDirection = " << getThreadDirection()); if (!pcHole->HoleCutType.isReadOnly()) FCMD_OBJ_CMD(obj,"HoleCutType = " << getHoleCutType()); + if (!pcHole->HoleCutCustomValues.isReadOnly()) + FCMD_OBJ_CMD(obj, "HoleCutCustomValues = " << (getHoleCutCustomValues() ? 1 : 0)); if (!pcHole->DepthType.isReadOnly()) FCMD_OBJ_CMD(obj,"DepthType = " << getDepthType()); if (!pcHole->DrillPoint.isReadOnly()) FCMD_OBJ_CMD(obj,"DrillPoint = " << getDrillPoint()); + if (!pcHole->DrillForDepth.isReadOnly()) + FCMD_OBJ_CMD(obj, "DrillForDepth = " << (getDrillForDepth() ? 1 : 0)); if (!pcHole->Tapered.isReadOnly()) FCMD_OBJ_CMD(obj,"Tapered = " << getTapered()); diff --git a/src/Mod/PartDesign/Gui/TaskHoleParameters.h b/src/Mod/PartDesign/Gui/TaskHoleParameters.h index 9c74bb30af..dc366dc55e 100644 --- a/src/Mod/PartDesign/Gui/TaskHoleParameters.h +++ b/src/Mod/PartDesign/Gui/TaskHoleParameters.h @@ -69,6 +69,7 @@ public: Base::Quantity getDiameter() const; long getThreadDirection() const; long getHoleCutType() const; + bool getHoleCutCustomValues() const; Base::Quantity getHoleCutDiameter() const; Base::Quantity getHoleCutDepth() const; Base::Quantity getHoleCutCountersinkAngle() const; @@ -76,6 +77,7 @@ public: Base::Quantity getDepth() const; long getDrillPoint() const; Base::Quantity getDrillPointAngle() const; + bool getDrillForDepth() const; bool getTapered() const; Base::Quantity getTaperedAngle() const; @@ -92,7 +94,8 @@ private Q_SLOTS: void threadAngleChanged(double value); void threadDiameterChanged(double value); void threadDirectionChanged(); - void holeCutChanged(int index); + void holeCutTypeChanged(int index); + void holeCutCustomValuesChanged(); void holeCutDiameterChanged(double value); void holeCutDepthChanged(double value); void holeCutCountersinkAngleChanged(double value); @@ -100,6 +103,7 @@ private Q_SLOTS: void depthValueChanged(double value); void drillPointChanged(); void drillPointAngledValueChanged(double value); + void drillForDepthChanged(); void taperedChanged(); void reversedChanged(); void taperedAngleChanged(double value); @@ -126,9 +130,9 @@ private: Connection connectPropChanged; std::unique_ptr observer; - QWidget* proxy; - Ui_TaskHoleParameters* ui; bool isApplying; + QWidget* proxy; + std::unique_ptr ui; }; /// simulation dialog for the TaskView diff --git a/src/Mod/PartDesign/Gui/TaskHoleParameters.ui b/src/Mod/PartDesign/Gui/TaskHoleParameters.ui index c723acb05c..acb1d821e9 100644 --- a/src/Mod/PartDesign/Gui/TaskHoleParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskHoleParameters.ui @@ -6,8 +6,8 @@ 0 0 - 441 - 710 + 342 + 486 @@ -40,10 +40,10 @@ - + - + 0 0 @@ -58,6 +58,12 @@ + + + 0 + 0 + + Whether the hole gets a thread @@ -66,116 +72,7 @@ - - - - false - - - Model actual thread - - - - - - - false - - - Pitch - - - - - - - false - - - mm - - - 0.000000000000000 - - - - - - - <b>Hole cut</b> - - - - - - - false - - - Angle - - - - - - - false - - - deg - - - 0.000000000000000 - - - - - - - false - - - Cutoff inner - - - - - - - false - - - mm - - - 0.000000000000000 - - - - - - - false - - - Cutoff outer - - - - - - - false - - - mm - - - 0.000000000000000 - - - - + @@ -191,8 +88,14 @@ - + + + + 0 + 0 + + @@ -204,8 +107,14 @@ - + + + + 0 + 0 + + Left hand @@ -214,7 +123,7 @@ - + @@ -227,40 +136,46 @@ - + - + 0 0 - 140 + 16777215 16777215 - + + + + 0 + 0 + + Clearance - + - + 0 0 - 110 + 16777215 16777215 @@ -285,7 +200,7 @@ Only available for holes without thread - + @@ -298,17 +213,17 @@ Only available for holes without thread - + - + 0 0 - 140 + 16777215 16777215 @@ -317,33 +232,30 @@ Only available for holes without thread - - - - Qt::Horizontal + + + + + 0 + 0 + - - QSizePolicy::Fixed + + Diameter - - - 13 - 20 - - - + - + - + 0 0 - 110 + 16777215 16777215 @@ -358,20 +270,7 @@ Only available for holes without thread - - - - - 0 - 0 - - - - Diameter - - - - + @@ -384,17 +283,17 @@ Only available for holes without thread - + - + 0 0 - 140 + 16777215 16777215 @@ -410,10 +309,10 @@ Only available for holes without thread - + - + 0 0 @@ -423,7 +322,14 @@ Only available for holes without thread - + + + + <b>Hole cut</b> + + + + @@ -436,10 +342,10 @@ Only available for holes without thread - + - + 0 0 @@ -455,17 +361,39 @@ Only available for holes without thread - + + + + + 0 + 0 + + + + Check to override the values predefined by the 'Type' + + + Custom values + + + + + + + 0 + 0 + + Diameter - + - + 0 0 @@ -490,17 +418,23 @@ Only available for holes without thread - + + + + 0 + 0 + + Depth - + - + 0 0 @@ -522,17 +456,23 @@ Only available for holes without thread - + + + + 0 + 0 + + Countersink angle - + - + 0 0 @@ -551,7 +491,7 @@ Only available for holes without thread - + @@ -567,8 +507,14 @@ Only available for holes without thread - + + + + 0 + 0 + + Type @@ -577,10 +523,10 @@ Only available for holes without thread - + - + 0 0 @@ -593,10 +539,10 @@ Only available for holes without thread - + - + 0 0 @@ -609,10 +555,10 @@ Only available for holes without thread - + - + 0 0 @@ -625,22 +571,51 @@ Only available for holes without thread - + + + + + 0 + 0 + + + + The size of the drill point will be taken into +account for the depth of blind holes + + + Take into account for depth + + + + <b>Misc</b> - + + + + 0 + 0 + + Tapered - + + + + 0 + 0 + + Taper angle for the hole 90 degree: straight hole @@ -655,8 +630,14 @@ over 90: larger hole radius at the bottom - + + + + 0 + 0 + + Reverses the hole direction @@ -679,6 +660,30 @@ over 90: larger hole radius at the bottom
Gui/PrefWidgets.h
+ + ThreadType + Threaded + directionRightHand + directionLeftHand + ThreadSize + ThreadFit + ThreadClass + Diameter + DepthType + Depth + HoleCutType + HoleCutCustomValues + HoleCutDiameter + HoleCutDepth + HoleCutCountersinkAngle + drillPointFlat + drillPointAngled + DrillPointAngle + DrillForDepth + Tapered + TaperedAngle + Reversed + @@ -697,22 +702,6 @@ over 90: larger hole radius at the bottom - - Threaded - clicked(bool) - ThreadFit - setEnabled(bool) - - - 136 - 63 - - - 322 - 254 - - - Threaded clicked(bool) @@ -729,9 +718,25 @@ over 90: larger hole radius at the bottom + + Threaded + clicked(bool) + ThreadFit + setEnabled(bool) + + + 136 + 63 + + + 322 + 254 + + + - + diff --git a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp index a1fa891a12..2be4b8cdc6 100644 --- a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp @@ -63,11 +63,11 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskLinearPatternParameters */ TaskLinearPatternParameters::TaskLinearPatternParameters(ViewProviderTransformed *TransformedView,QWidget *parent) - : TaskTransformedParameters(TransformedView, parent) + : TaskTransformedParameters(TransformedView, parent) + , ui(new Ui_TaskLinearPatternParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskLinearPatternParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -83,10 +83,9 @@ TaskLinearPatternParameters::TaskLinearPatternParameters(ViewProviderTransformed } TaskLinearPatternParameters::TaskLinearPatternParameters(TaskMultiTransformParameters *parentTask, QLayout *layout) - : TaskTransformedParameters(parentTask) + : TaskTransformedParameters(parentTask), ui(new Ui_TaskLinearPatternParameters) { proxy = new QWidget(parentTask); - ui = new Ui_TaskLinearPatternParameters(); ui->setupUi(proxy); connect(ui->buttonOK, SIGNAL(pressed()), parentTask, SLOT(onSubTaskButtonOK())); @@ -415,7 +414,6 @@ TaskLinearPatternParameters::~TaskLinearPatternParameters() Base::Console().Error ("%s\n", ex.what () ); } - delete ui; if (proxy) delete proxy; } diff --git a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h index b492c965cd..d48dbeb67a 100644 --- a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h +++ b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h @@ -85,7 +85,7 @@ private: void kickUpdateViewTimer() const; private: - Ui_TaskLinearPatternParameters* ui; + std::unique_ptr ui; QTimer* updateViewTimer; ComboLinks dirLinks; diff --git a/src/Mod/PartDesign/Gui/TaskLoftParameters.cpp b/src/Mod/PartDesign/Gui/TaskLoftParameters.cpp index 2106604e87..33040229c1 100644 --- a/src/Mod/PartDesign/Gui/TaskLoftParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskLoftParameters.cpp @@ -56,11 +56,11 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskLoftParameters */ TaskLoftParameters::TaskLoftParameters(ViewProviderLoft *LoftView,bool /*newObj*/, QWidget *parent) - : TaskSketchBasedParameters(LoftView, parent, "PartDesign_Additive_Loft",tr("Loft parameters")) + : TaskSketchBasedParameters(LoftView, parent, "PartDesign_Additive_Loft", tr("Loft parameters")) + , ui(new Ui_TaskLoftParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskLoftParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -134,7 +134,6 @@ TaskLoftParameters::TaskLoftParameters(ViewProviderLoft *LoftView,bool /*newObj* TaskLoftParameters::~TaskLoftParameters() { - delete ui; } void TaskLoftParameters::updateUI(int index) diff --git a/src/Mod/PartDesign/Gui/TaskLoftParameters.h b/src/Mod/PartDesign/Gui/TaskLoftParameters.h index 4e5d276c49..b99a771edf 100644 --- a/src/Mod/PartDesign/Gui/TaskLoftParameters.h +++ b/src/Mod/PartDesign/Gui/TaskLoftParameters.h @@ -75,7 +75,7 @@ private: private: QWidget* proxy; - Ui_TaskLoftParameters* ui; + std::unique_ptr ui; enum selectionModes { none, refAdd, refRemove, refProfile }; selectionModes selectionMode = none; diff --git a/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp b/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp index 74533f9efa..4f1bfce4ef 100644 --- a/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp @@ -60,11 +60,11 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskMirroredParameters */ TaskMirroredParameters::TaskMirroredParameters(ViewProviderTransformed *TransformedView, QWidget *parent) - : TaskTransformedParameters(TransformedView, parent) + : TaskTransformedParameters(TransformedView, parent) + , ui(new Ui_TaskMirroredParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskMirroredParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -80,10 +80,9 @@ TaskMirroredParameters::TaskMirroredParameters(ViewProviderTransformed *Transfor } TaskMirroredParameters::TaskMirroredParameters(TaskMultiTransformParameters *parentTask, QLayout *layout) - : TaskTransformedParameters(parentTask) + : TaskTransformedParameters(parentTask), ui(new Ui_TaskMirroredParameters) { proxy = new QWidget(parentTask); - ui = new Ui_TaskMirroredParameters(); ui->setupUi(proxy); connect(ui->buttonOK, SIGNAL(pressed()), parentTask, SLOT(onSubTaskButtonOK())); @@ -318,7 +317,6 @@ TaskMirroredParameters::~TaskMirroredParameters() Base::Console().Error ("%s\n", ex.what () ); } - delete ui; if (proxy) delete proxy; } diff --git a/src/Mod/PartDesign/Gui/TaskMirroredParameters.h b/src/Mod/PartDesign/Gui/TaskMirroredParameters.h index 906c0d4516..297b572391 100644 --- a/src/Mod/PartDesign/Gui/TaskMirroredParameters.h +++ b/src/Mod/PartDesign/Gui/TaskMirroredParameters.h @@ -79,7 +79,7 @@ private: ComboLinks planeLinks; private: - Ui_TaskMirroredParameters* ui; + std::unique_ptr ui; }; diff --git a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp index 48ff9329f9..a5f4616615 100644 --- a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp @@ -61,11 +61,13 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskMultiTransformParameters */ TaskMultiTransformParameters::TaskMultiTransformParameters(ViewProviderTransformed *TransformedView,QWidget *parent) - : TaskTransformedParameters(TransformedView, parent), subTask(nullptr), subFeature(nullptr) + : TaskTransformedParameters(TransformedView, parent) + , ui(new Ui_TaskMultiTransformParameters) + , subTask(nullptr) + , subFeature(nullptr) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskMultiTransformParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); this->groupLayout()->addWidget(proxy); @@ -484,7 +486,6 @@ void TaskMultiTransformParameters::apply() TaskMultiTransformParameters::~TaskMultiTransformParameters() { closeSubTask(); - delete ui; if (proxy) delete proxy; } diff --git a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h index f309247fb1..57188b73d6 100644 --- a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h +++ b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h @@ -100,7 +100,7 @@ private: void finishAdd(std::string &newFeatName); private: - Ui_TaskMultiTransformParameters* ui; + std::unique_ptr ui; /// The subTask and subFeature currently active in the UI TaskTransformedParameters* subTask; PartDesign::Transformed* subFeature; diff --git a/src/Mod/PartDesign/Gui/TaskPadParameters.cpp b/src/Mod/PartDesign/Gui/TaskPadParameters.cpp index 539c33fc85..2604b60aad 100644 --- a/src/Mod/PartDesign/Gui/TaskPadParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskPadParameters.cpp @@ -54,11 +54,11 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskPadParameters */ TaskPadParameters::TaskPadParameters(ViewProviderPad *PadView, QWidget *parent, bool newObj) - : TaskSketchBasedParameters(PadView, parent, "PartDesign_Pad",tr("Pad parameters")) + : TaskSketchBasedParameters(PadView, parent, "PartDesign_Pad", tr("Pad parameters")) + , ui(new Ui_TaskPadParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskPadParameters(); ui->setupUi(proxy); #if QT_VERSION >= 0x040700 ui->lineFaceName->setPlaceholderText(tr("No face selected")); @@ -487,7 +487,6 @@ QString TaskPadParameters::getFaceName(void) const TaskPadParameters::~TaskPadParameters() { - delete ui; } void TaskPadParameters::changeEvent(QEvent *e) diff --git a/src/Mod/PartDesign/Gui/TaskPadParameters.h b/src/Mod/PartDesign/Gui/TaskPadParameters.h index 6a0f64f796..17172344ed 100644 --- a/src/Mod/PartDesign/Gui/TaskPadParameters.h +++ b/src/Mod/PartDesign/Gui/TaskPadParameters.h @@ -90,7 +90,7 @@ private: private: QWidget* proxy; - Ui_TaskPadParameters* ui; + std::unique_ptr ui; }; /// simulation dialog for the TaskView diff --git a/src/Mod/PartDesign/Gui/TaskPadParameters.ui b/src/Mod/PartDesign/Gui/TaskPadParameters.ui index 375986c44e..5a040a832d 100644 --- a/src/Mod/PartDesign/Gui/TaskPadParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskPadParameters.ui @@ -15,15 +15,15 @@ - - + + Type - + @@ -32,18 +32,14 @@ - - - - - + Length - + false @@ -70,17 +66,15 @@ the sketch plane's normal vector will be used true - - - - + + x - + x-component of direction vector @@ -102,18 +96,14 @@ the sketch plane's normal vector will be used - - - - - + y - + y-component of direction vector @@ -135,18 +125,14 @@ the sketch plane's normal vector will be used - - - - - + z - + z-component of direction vector @@ -171,8 +157,6 @@ the sketch plane's normal vector will be used - - diff --git a/src/Mod/PartDesign/Gui/TaskPipeParameters.cpp b/src/Mod/PartDesign/Gui/TaskPipeParameters.cpp index 62312c1d95..bfea0fd500 100644 --- a/src/Mod/PartDesign/Gui/TaskPipeParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskPipeParameters.cpp @@ -72,11 +72,11 @@ using namespace Gui; //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TaskPipeParameters::TaskPipeParameters(ViewProviderPipe *PipeView, bool /*newObj*/, QWidget *parent) - : TaskSketchBasedParameters(PipeView, parent, "PartDesign_Additive_Pipe",tr("Pipe parameters")) + : TaskSketchBasedParameters(PipeView, parent, "PartDesign_Additive_Pipe", tr("Pipe parameters")) + , ui(new Ui_TaskPipeParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskPipeParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -163,8 +163,6 @@ TaskPipeParameters::~TaskPipeParameters() // getDocument() may raise an exception e.ReportException(); } - - delete ui; } void TaskPipeParameters::updateUI() @@ -441,11 +439,11 @@ void TaskPipeParameters::exitSelectionMode() { //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TaskPipeOrientation::TaskPipeOrientation(ViewProviderPipe* PipeView, bool /*newObj*/, QWidget* parent) - : TaskSketchBasedParameters(PipeView, parent, "PartDesign_Additive_Pipe", tr("Section orientation")) { - + : TaskSketchBasedParameters(PipeView, parent, "PartDesign_Additive_Pipe", tr("Section orientation")), + ui(new Ui_TaskPipeOrientation) +{ // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskPipeOrientation(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -768,11 +766,11 @@ void TaskPipeOrientation::updateUI(int idx) { // Task Scaling //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TaskPipeScaling::TaskPipeScaling(ViewProviderPipe* PipeView, bool /*newObj*/, QWidget* parent) - : TaskSketchBasedParameters(PipeView, parent, "PartDesign_Additive_Pipe", tr("Section transformation")) + : TaskSketchBasedParameters(PipeView, parent, "PartDesign_Additive_Pipe", tr("Section transformation")), + ui(new Ui_TaskPipeScaling) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskPipeScaling(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); diff --git a/src/Mod/PartDesign/Gui/TaskPipeParameters.h b/src/Mod/PartDesign/Gui/TaskPipeParameters.h index cd4ad127b2..5d1f95b0ed 100644 --- a/src/Mod/PartDesign/Gui/TaskPipeParameters.h +++ b/src/Mod/PartDesign/Gui/TaskPipeParameters.h @@ -84,7 +84,7 @@ private: private: QWidget* proxy; - Ui_TaskPipeParameters* ui; + std::unique_ptr ui; }; class TaskPipeOrientation : public TaskSketchBasedParameters @@ -123,7 +123,7 @@ private: private: QWidget* proxy; - Ui_TaskPipeOrientation* ui; + std::unique_ptr ui; }; @@ -157,7 +157,7 @@ private: private: QWidget* proxy; - Ui_TaskPipeScaling* ui; + std::unique_ptr ui; }; diff --git a/src/Mod/PartDesign/Gui/TaskPocketParameters.cpp b/src/Mod/PartDesign/Gui/TaskPocketParameters.cpp index a163f4c048..6f491c5d15 100644 --- a/src/Mod/PartDesign/Gui/TaskPocketParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskPocketParameters.cpp @@ -53,12 +53,12 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskPocketParameters */ TaskPocketParameters::TaskPocketParameters(ViewProviderPocket *PocketView,QWidget *parent, bool newObj) - : TaskSketchBasedParameters(PocketView, parent, "PartDesign_Pocket",tr("Pocket parameters")) + : TaskSketchBasedParameters(PocketView, parent, "PartDesign_Pocket", tr("Pocket parameters")) + , ui(new Ui_TaskPocketParameters) , oldLength(0) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskPocketParameters(); ui->setupUi(proxy); #if QT_VERSION >= 0x040700 ui->lineFaceName->setPlaceholderText(tr("No face selected")); @@ -422,7 +422,6 @@ QString TaskPocketParameters::getFaceName(void) const TaskPocketParameters::~TaskPocketParameters() { - delete ui; } void TaskPocketParameters::changeEvent(QEvent *e) diff --git a/src/Mod/PartDesign/Gui/TaskPocketParameters.h b/src/Mod/PartDesign/Gui/TaskPocketParameters.h index 1925b5b69f..397b545d16 100644 --- a/src/Mod/PartDesign/Gui/TaskPocketParameters.h +++ b/src/Mod/PartDesign/Gui/TaskPocketParameters.h @@ -82,7 +82,7 @@ private: private: QWidget* proxy; - Ui_TaskPocketParameters* ui; + std::unique_ptr ui; double oldLength; }; diff --git a/src/Mod/PartDesign/Gui/TaskPocketParameters.ui b/src/Mod/PartDesign/Gui/TaskPocketParameters.ui index 29ea32605f..cf487e40eb 100644 --- a/src/Mod/PartDesign/Gui/TaskPocketParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskPocketParameters.ui @@ -6,8 +6,8 @@ 0 0 - 241 - 233 + 193 + 272 @@ -15,15 +15,15 @@ - - + + Type - + @@ -32,18 +32,14 @@ - - - - - + Length - + false @@ -53,18 +49,14 @@ - - - - - + Offset - + false @@ -153,6 +145,17 @@
Gui/PrefWidgets.h
+ + changeMode + lengthEdit + offsetEdit + checkBoxMidplane + checkBoxReversed + lengthEdit2 + buttonFace + lineFaceName + checkBoxUpdateView + diff --git a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp index 1812549652..2e4272d389 100644 --- a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp @@ -61,11 +61,11 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskPolarPatternParameters */ TaskPolarPatternParameters::TaskPolarPatternParameters(ViewProviderTransformed *TransformedView,QWidget *parent) - : TaskTransformedParameters(TransformedView, parent) + : TaskTransformedParameters(TransformedView, parent) + , ui(new Ui_TaskPolarPatternParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskPolarPatternParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -81,10 +81,9 @@ TaskPolarPatternParameters::TaskPolarPatternParameters(ViewProviderTransformed * } TaskPolarPatternParameters::TaskPolarPatternParameters(TaskMultiTransformParameters *parentTask, QLayout *layout) - : TaskTransformedParameters(parentTask) + : TaskTransformedParameters(parentTask), ui(new Ui_TaskPolarPatternParameters) { proxy = new QWidget(parentTask); - ui = new Ui_TaskPolarPatternParameters(); ui->setupUi(proxy); connect(ui->buttonOK, SIGNAL(pressed()), parentTask, SLOT(onSubTaskButtonOK())); @@ -406,7 +405,6 @@ TaskPolarPatternParameters::~TaskPolarPatternParameters() Base::Console().Error ("%s\n", ex.what () ); } - delete ui; if (proxy) delete proxy; } diff --git a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h index 3cbb777a7b..d9864cee4a 100644 --- a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h +++ b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h @@ -87,7 +87,7 @@ private: void kickUpdateViewTimer() const; private: - Ui_TaskPolarPatternParameters* ui; + std::unique_ptr ui; QTimer* updateViewTimer; ComboLinks axesLinks; diff --git a/src/Mod/PartDesign/Gui/TaskRevolutionParameters.cpp b/src/Mod/PartDesign/Gui/TaskRevolutionParameters.cpp index bc1326b71b..7da3ea44a4 100644 --- a/src/Mod/PartDesign/Gui/TaskRevolutionParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskRevolutionParameters.cpp @@ -58,11 +58,11 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskRevolutionParameters */ TaskRevolutionParameters::TaskRevolutionParameters(PartDesignGui::ViewProvider* RevolutionView, QWidget *parent) - : TaskSketchBasedParameters(RevolutionView, parent, "PartDesign_Revolution",tr("Revolution parameters")) + : TaskSketchBasedParameters(RevolutionView, parent, "PartDesign_Revolution", tr("Revolution parameters")) + , ui(new Ui_TaskRevolutionParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskRevolutionParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -379,8 +379,6 @@ TaskRevolutionParameters::~TaskRevolutionParameters() ex.ReportException(); } - delete ui; - for (size_t i = 0; i < axesInList.size(); i++) { delete axesInList[i]; } diff --git a/src/Mod/PartDesign/Gui/TaskRevolutionParameters.h b/src/Mod/PartDesign/Gui/TaskRevolutionParameters.h index bc810e4d34..1f28cf8657 100644 --- a/src/Mod/PartDesign/Gui/TaskRevolutionParameters.h +++ b/src/Mod/PartDesign/Gui/TaskRevolutionParameters.h @@ -92,7 +92,7 @@ private: private: QWidget* proxy; - Ui_TaskRevolutionParameters* ui; + std::unique_ptr ui; /** * @brief axesInList is the list of links corresponding to axis combo; must diff --git a/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp b/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp index 3bd6bb84c4..dcf82058b8 100644 --- a/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp @@ -51,11 +51,11 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskScaledParameters */ TaskScaledParameters::TaskScaledParameters(ViewProviderTransformed *TransformedView,QWidget *parent) - : TaskTransformedParameters(TransformedView, parent) + : TaskTransformedParameters(TransformedView, parent) + , ui(new Ui_TaskScaledParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskScaledParameters(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -69,10 +69,9 @@ TaskScaledParameters::TaskScaledParameters(ViewProviderTransformed *TransformedV } TaskScaledParameters::TaskScaledParameters(TaskMultiTransformParameters *parentTask, QLayout *layout) - : TaskTransformedParameters(parentTask) + : TaskTransformedParameters(parentTask), ui(new Ui_TaskScaledParameters) { proxy = new QWidget(parentTask); - ui = new Ui_TaskScaledParameters(); ui->setupUi(proxy); connect(ui->buttonOK, SIGNAL(pressed()), parentTask, SLOT(onSubTaskButtonOK())); @@ -242,7 +241,6 @@ unsigned TaskScaledParameters::getOccurrences(void) const TaskScaledParameters::~TaskScaledParameters() { - delete ui; if (proxy) delete proxy; } diff --git a/src/Mod/PartDesign/Gui/TaskScaledParameters.h b/src/Mod/PartDesign/Gui/TaskScaledParameters.h index 2e21501503..7fc844ff18 100644 --- a/src/Mod/PartDesign/Gui/TaskScaledParameters.h +++ b/src/Mod/PartDesign/Gui/TaskScaledParameters.h @@ -76,7 +76,7 @@ private: void updateUI(); private: - Ui_TaskScaledParameters* ui; + std::unique_ptr ui; }; diff --git a/src/Mod/PartDesign/Gui/TaskShapeBinder.cpp b/src/Mod/PartDesign/Gui/TaskShapeBinder.cpp index 7a556afd29..ff869fdba5 100644 --- a/src/Mod/PartDesign/Gui/TaskShapeBinder.cpp +++ b/src/Mod/PartDesign/Gui/TaskShapeBinder.cpp @@ -60,10 +60,10 @@ TaskShapeBinder::TaskShapeBinder(ViewProviderShapeBinder *view, bool /*newObj*/, : Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("PartDesign_ShapeBinder"), tr("Datum shape parameters"), true, parent) , SelectionObserver(view) + , ui(new Ui_TaskShapeBinder) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskShapeBinder(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -159,7 +159,6 @@ TaskShapeBinder::~TaskShapeBinder() } static_cast(vp)->highlightReferences(false, false); */ - delete ui; } void TaskShapeBinder::changeEvent(QEvent *) diff --git a/src/Mod/PartDesign/Gui/TaskShapeBinder.h b/src/Mod/PartDesign/Gui/TaskShapeBinder.h index a886961759..fdf1364a0d 100644 --- a/src/Mod/PartDesign/Gui/TaskShapeBinder.h +++ b/src/Mod/PartDesign/Gui/TaskShapeBinder.h @@ -76,7 +76,7 @@ private: private: QWidget* proxy; - Ui_TaskShapeBinder* ui; + std::unique_ptr ui; ViewProviderShapeBinder* vp; }; diff --git a/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp b/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp index f752dc91e5..44b3424da4 100644 --- a/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp @@ -54,10 +54,10 @@ using namespace Gui; TaskThicknessParameters::TaskThicknessParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, false, true, parent) + , ui(new Ui_TaskThicknessParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskThicknessParameters(); ui->setupUi(proxy); this->groupLayout()->addWidget(proxy); @@ -290,8 +290,6 @@ TaskThicknessParameters::~TaskThicknessParameters() { Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); - - delete ui; } bool TaskThicknessParameters::event(QEvent *e) diff --git a/src/Mod/PartDesign/Gui/TaskThicknessParameters.h b/src/Mod/PartDesign/Gui/TaskThicknessParameters.h index bf5aed26e7..d2ad5490ba 100644 --- a/src/Mod/PartDesign/Gui/TaskThicknessParameters.h +++ b/src/Mod/PartDesign/Gui/TaskThicknessParameters.h @@ -60,7 +60,7 @@ protected: virtual void onSelectionChanged(const Gui::SelectionChanges& msg); private: - Ui_TaskThicknessParameters* ui; + std::unique_ptr ui; }; /// simulation dialog for the TaskView diff --git a/src/Mod/PartDesign/Gui/TaskTransformedMessages.cpp b/src/Mod/PartDesign/Gui/TaskTransformedMessages.cpp index 5d3290d78d..0674829203 100644 --- a/src/Mod/PartDesign/Gui/TaskTransformedMessages.cpp +++ b/src/Mod/PartDesign/Gui/TaskTransformedMessages.cpp @@ -40,12 +40,12 @@ using namespace Gui::TaskView; namespace bp = boost::placeholders; TaskTransformedMessages::TaskTransformedMessages(ViewProviderTransformed *transformedView_) - : TaskBox(Gui::BitmapFactory().pixmap("document-new"),tr("Transformed feature messages"),true, 0), - transformedView(transformedView_) + : TaskBox(Gui::BitmapFactory().pixmap("document-new"), tr("Transformed feature messages"), true, 0) + , transformedView(transformedView_) + , ui(new Ui_TaskTransformedMessages) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskTransformedMessages(); ui->setupUi(proxy); // set a minimum height to avoid a sudden resize and to // lose focus of the currently used spin boxes @@ -61,7 +61,6 @@ TaskTransformedMessages::TaskTransformedMessages(ViewProviderTransformed *transf TaskTransformedMessages::~TaskTransformedMessages() { connectionDiagnosis.disconnect(); - delete ui; } void TaskTransformedMessages::slotDiagnosis(QString msg) diff --git a/src/Mod/PartDesign/Gui/TaskTransformedMessages.h b/src/Mod/PartDesign/Gui/TaskTransformedMessages.h index ad1b349488..3445dfdd06 100644 --- a/src/Mod/PartDesign/Gui/TaskTransformedMessages.h +++ b/src/Mod/PartDesign/Gui/TaskTransformedMessages.h @@ -56,7 +56,7 @@ protected: private: QWidget* proxy; - Ui_TaskTransformedMessages* ui; + std::unique_ptr ui; }; } //namespace PartDesignGui diff --git a/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp b/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp index 7c43fe671d..fa5b10a5cf 100644 --- a/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp @@ -65,14 +65,12 @@ using namespace Gui; TaskTransformedParameters::TaskTransformedParameters(ViewProviderTransformed *TransformedView, QWidget *parent) : TaskBox(Gui::BitmapFactory().pixmap((std::string("PartDesign_") + TransformedView->featureName).c_str()), - QString::fromLatin1((TransformedView->featureName + " parameters").c_str()), - true, - parent), - proxy(nullptr), - TransformedView(TransformedView), - parentTask(nullptr), - insideMultiTransform(false), - blockUpdate(false) + QString::fromLatin1((TransformedView->featureName + " parameters").c_str()), true, parent) + , proxy(nullptr) + , TransformedView(TransformedView) + , parentTask(nullptr) + , insideMultiTransform(false) + , blockUpdate(false) { selectionMode = none; diff --git a/src/Mod/PartDesign/Gui/ViewProviderHelix.cpp b/src/Mod/PartDesign/Gui/ViewProviderHelix.cpp new file mode 100644 index 0000000000..d1ec60c4c7 --- /dev/null +++ b/src/Mod/PartDesign/Gui/ViewProviderHelix.cpp @@ -0,0 +1,120 @@ +/*************************************************************************** + * Copyright (c) 2011 Juergen Riegel * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +#endif + +#include +#include + +#include +#include +#include + +#include "TaskHelixParameters.h" +#include "ViewProviderHelix.h" + +using namespace PartDesignGui; + +PROPERTY_SOURCE(PartDesignGui::ViewProviderHelix,PartDesignGui::ViewProvider) + + +ViewProviderHelix::ViewProviderHelix() +{ +} + +ViewProviderHelix::~ViewProviderHelix() +{ +} + +void ViewProviderHelix::setupContextMenu(QMenu* menu, QObject* receiver, const char* member) +{ + QAction* act; + act = menu->addAction(QObject::tr("Edit helix"), receiver, member); + act->setData(QVariant((int)ViewProvider::Default)); + PartDesignGui::ViewProviderAddSub::setupContextMenu(menu, receiver, member); +} + +TaskDlgFeatureParameters *ViewProviderHelix::getEditDialog() +{ + return new TaskDlgHelixParameters( this ); +} + +QIcon ViewProviderHelix::getIcon(void) const { + QString str = QString::fromLatin1("PartDesign_"); + auto* prim = static_cast(getObject()); + if(prim->getAddSubType() == PartDesign::FeatureAddSub::Additive) + str += QString::fromLatin1("Additive_"); + else + str += QString::fromLatin1("Subtractive_"); + + str += QString::fromLatin1("Helix.svg"); + return PartDesignGui::ViewProvider::mergeGreyableOverlayIcons(Gui::BitmapFactory().pixmap(str.toStdString().c_str())); +} + +bool ViewProviderHelix::setEdit(int ModNum) +{ + + if (ModNum == ViewProvider::Default ) { + auto* prim = static_cast(getObject()); + setPreviewDisplayMode(prim->getAddSubType() == PartDesign::FeatureAddSub::Subtractive); + } + return ViewProviderAddSub::setEdit(ModNum); +} + +void ViewProviderHelix::unsetEdit(int ModNum) +{ + setPreviewDisplayMode(false); + // Rely on parent class to: + // restitute old workbench (set setEdit above) and close the dialog if exiting editing + PartDesignGui::ViewProvider::unsetEdit(ModNum); +} + +std::vector ViewProviderHelix::claimChildren(void) const { + std::vector temp; + App::DocumentObject* sketch = static_cast(getObject())->Profile.getValue(); + if (sketch != NULL && sketch->isDerivedFrom(Part::Part2DObject::getClassTypeId())) + temp.push_back(sketch); + + return temp; +} + +bool ViewProviderHelix::onDelete(const std::vector &s) { + PartDesign::ProfileBased* feature = static_cast(getObject()); + + // get the Sketch + Sketcher::SketchObject *pcSketch = 0; + if (feature->Profile.getValue()) + pcSketch = static_cast(feature->Profile.getValue()); + + // if abort command deleted the object the sketch is visible again + if (pcSketch && Gui::Application::Instance->getViewProvider(pcSketch)) + Gui::Application::Instance->getViewProvider(pcSketch)->show(); + + return ViewProvider::onDelete(s); +} + diff --git a/src/Mod/PartDesign/Gui/ViewProviderHelix.h b/src/Mod/PartDesign/Gui/ViewProviderHelix.h new file mode 100644 index 0000000000..83dd7d6d55 --- /dev/null +++ b/src/Mod/PartDesign/Gui/ViewProviderHelix.h @@ -0,0 +1,62 @@ +/*************************************************************************** + * Copyright (c) 2011 Juergen Riegel * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef PARTGUI_ViewProviderHelix_H +#define PARTGUI_ViewProviderHelix_H + +#include "ViewProviderAddSub.h" + + +namespace PartDesignGui { + +class PartDesignGuiExport ViewProviderHelix : public ViewProviderAddSub +{ + PROPERTY_HEADER(PartDesignGui::ViewProviderHelix); + +public: + /// constructor + ViewProviderHelix(); + /// destructor + virtual ~ViewProviderHelix(); + + void setupContextMenu(QMenu*, QObject*, const char*); + + /// grouping handling + std::vector claimChildren(void)const; + + virtual bool onDelete(const std::vector &); + +protected: + virtual QIcon getIcon(void) const; + + /// Returns a newly created TaskDlgHelixParameters + virtual TaskDlgFeatureParameters *getEditDialog(); + virtual bool setEdit(int ModNum); + virtual void unsetEdit(int ModNum); +}; + + +} // namespace PartDesignGui + + +#endif // PARTGUI_ViewProviderHelix_H diff --git a/src/Mod/PartDesign/Gui/ViewProviderSketchBased.cpp b/src/Mod/PartDesign/Gui/ViewProviderSketchBased.cpp index fb47925a07..399ad44242 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderSketchBased.cpp +++ b/src/Mod/PartDesign/Gui/ViewProviderSketchBased.cpp @@ -72,3 +72,4 @@ bool ViewProviderSketchBased::onDelete(const std::vector &s) { return ViewProvider::onDelete(s); } + diff --git a/src/Mod/PartDesign/Gui/ViewProviderSketchBased.h b/src/Mod/PartDesign/Gui/ViewProviderSketchBased.h index 3831c3c923..cb5d9e18d3 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderSketchBased.h +++ b/src/Mod/PartDesign/Gui/ViewProviderSketchBased.h @@ -23,7 +23,7 @@ #ifndef VIEWPROVIDERSKETCHBASED_H_QKP3UG9A #define VIEWPROVIDERSKETCHBASED_H_QKP3UG9A -#include "ViewProvider.h" +#include "ViewProviderAddSub.h" namespace PartDesignGui { @@ -44,6 +44,7 @@ public: std::vector claimChildren(void)const; virtual bool onDelete(const std::vector &); + }; } /* PartDesignGui */ diff --git a/src/Mod/PartDesign/Gui/Workbench.cpp b/src/Mod/PartDesign/Gui/Workbench.cpp index 073473aa8e..73b7576c16 100644 --- a/src/Mod/PartDesign/Gui/Workbench.cpp +++ b/src/Mod/PartDesign/Gui/Workbench.cpp @@ -409,6 +409,8 @@ void Workbench::activated() "PartDesign_SubtractivePipe", "PartDesign_AdditiveLoft", "PartDesign_SubtractiveLoft", + "PartDesign_AdditiveHelix", + "PartDesign_SubtractiveHelix", 0}; Watcher.push_back(new Gui::TaskView::TaskWatcherCommands( "SELECT Sketcher::SketchObject COUNT 1", @@ -496,14 +498,14 @@ Gui::MenuItem* Workbench::setupMenuBar() const Gui::MenuItem* additives = new Gui::MenuItem; additives->setCommand("Create an additive feature"); *additives << "PartDesign_Pad" << "PartDesign_Revolution" - << "PartDesign_AdditiveLoft" << "PartDesign_AdditivePipe"; + << "PartDesign_AdditiveLoft" << "PartDesign_AdditivePipe" << "PartDesign_AdditiveHelix"; // subtractives Gui::MenuItem* subtractives = new Gui::MenuItem; subtractives->setCommand("Create a subtractive feature"); *subtractives << "PartDesign_Pocket" << "PartDesign_Hole" << "PartDesign_Groove" << "PartDesign_SubtractiveLoft" - << "PartDesign_SubtractivePipe"; + << "PartDesign_SubtractivePipe" << "PartDesign_SubtractiveHelix"; // transformations Gui::MenuItem* transformations = new Gui::MenuItem; @@ -598,6 +600,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "PartDesign_Revolution" << "PartDesign_AdditiveLoft" << "PartDesign_AdditivePipe" + << "PartDesign_AdditiveHelix" << "PartDesign_CompPrimitiveAdditive" << "Separator" << "PartDesign_Pocket" @@ -605,6 +608,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "PartDesign_Groove" << "PartDesign_SubtractiveLoft" << "PartDesign_SubtractivePipe" + << "PartDesign_SubtractiveHelix" << "PartDesign_CompPrimitiveSubtractive" << "Separator" << "PartDesign_Mirrored" diff --git a/src/Mod/PartDesign/PartDesignTests/TestHole.py b/src/Mod/PartDesign/PartDesignTests/TestHole.py index cc838f4a3b..a32dd11bd4 100644 --- a/src/Mod/PartDesign/PartDesignTests/TestHole.py +++ b/src/Mod/PartDesign/PartDesignTests/TestHole.py @@ -86,6 +86,7 @@ class TestHole(unittest.TestCase): self.Hole.DepthType = 0 self.Hole.DrillPoint = 1 self.Hole.Tapered = 0 + self.Hole.DrillForDepth = 1 self.Doc.recompute() self.assertEqual(len(self.Hole.Shape.Faces), 8) diff --git a/src/Mod/PartDesign/Resources/Hole/iso10642.json b/src/Mod/PartDesign/Resources/Hole/iso10642.json index 4ab3affb6d..35aafa8a11 100644 --- a/src/Mod/PartDesign/Resources/Hole/iso10642.json +++ b/src/Mod/PartDesign/Resources/Hole/iso10642.json @@ -4,17 +4,14 @@ "thread_type": "metric", "angle": 90, "data": [ - { "thread": "M1.6", "diameter": 3.6 }, - { "thread": "M2", "diameter": 4.5 }, - { "thread": "M2.5", "diameter": 5.6 }, { "thread": "M3", "diameter": 6.7 }, { "thread": "M4", "diameter": 9.0 }, { "thread": "M5", "diameter": 12.2 }, - { "thread": "M6", "diameter": 13.5 }, - { "thread": "M8", "diameter": 18.0 }, + { "thread": "M6", "diameter": 13.4 }, + { "thread": "M8", "diameter": 17.9 }, { "thread": "M10", "diameter": 22.4 }, - { "thread": "M12", "diameter": 26.8 }, - { "thread": "M14", "diameter": 30.9 }, + { "thread": "M12", "diameter": 26.9 }, + { "thread": "M14", "diameter": 30.8 }, { "thread": "M16", "diameter": 33.6 }, { "thread": "M20", "diameter": 40.3 } ] diff --git a/src/Mod/PartDesign/Resources/Hole/iso7046.json b/src/Mod/PartDesign/Resources/Hole/iso7046.json index 6a4bbec5be..ec227e1893 100644 --- a/src/Mod/PartDesign/Resources/Hole/iso7046.json +++ b/src/Mod/PartDesign/Resources/Hole/iso7046.json @@ -4,13 +4,15 @@ "thread_type": "metric", "angle": 90, "data": [ - { "thread": "M2", "diameter": 4.3 }, - { "thread": "M2.5", "diameter": 5.3 }, + { "thread": "M1.6", "diameter": 3.6 }, + { "thread": "M2", "diameter": 4.4 }, + { "thread": "M2.5", "diameter": 5.5 }, { "thread": "M3", "diameter": 6.3 }, - { "thread": "M4", "diameter": 9.5 }, - { "thread": "M5", "diameter": 10.5 }, - { "thread": "M6", "diameter": 12.7 }, - { "thread": "M8", "diameter": 17.7 }, - { "thread": "M10", "diameter": 20.5 } + { "thread": "M3.5", "diameter": 8.2 }, + { "thread": "M4", "diameter": 9.4 }, + { "thread": "M5", "diameter": 10.4 }, + { "thread": "M6", "diameter": 12.6 }, + { "thread": "M8", "diameter": 17.3 }, + { "thread": "M10", "diameter": 20.0 } ] } diff --git a/src/Mod/Path/App/CommandPy.xml b/src/Mod/Path/App/CommandPy.xml index 14e6df4109..1588b9dbe1 100644 --- a/src/Mod/Path/App/CommandPy.xml +++ b/src/Mod/Path/App/CommandPy.xml @@ -43,7 +43,7 @@ pairs, or a placement, or a vector
- toGCode(): returns a GCode representation of the command + setFromGCode(): sets the path from the contents of the given GCode string @@ -51,5 +51,8 @@ pairs, or a placement, or a vector transform(Placement): returns a copy of this command transformed by the given placement + + mutable Py::Dict parameters_copy_dict; + diff --git a/src/Mod/Path/App/CommandPyImp.cpp b/src/Mod/Path/App/CommandPyImp.cpp index 98dc904264..88bd30fce5 100644 --- a/src/Mod/Path/App/CommandPyImp.cpp +++ b/src/Mod/Path/App/CommandPyImp.cpp @@ -56,6 +56,11 @@ std::string CommandPy::representation(void) const return str.str(); } +// +// Py::Dict parameters_copy_dict is now a class member to avoid delete/create/copy on every read access from python code +// Now the pre-filled Py::Dict is returned which is more consistent with normal python behaviour. +// It should be cleared whenever the c++ Parameters object is changed eg setParameters() or other objects invalidate its content, eg setPlacement() +// https://forum.freecadweb.org/viewtopic.php?f=15&t=50583 PyObject *CommandPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper { @@ -117,6 +122,7 @@ int CommandPy::PyInit(PyObject* args, PyObject* kwd) } getCommandPtr()->Parameters[ckey]=cvalue; } + parameters_copy_dict.clear(); return 0; } PyErr_Clear(); // set by PyArg_ParseTuple() @@ -157,11 +163,13 @@ void CommandPy::setName(Py::String arg) Py::Dict CommandPy::getParameters(void) const { - Py::Dict dict; - for(std::map::iterator i = getCommandPtr()->Parameters.begin(); i != getCommandPtr()->Parameters.end(); ++i) { - dict.setItem(i->first, Py::Float(i->second)); + // dict now a class member , https://forum.freecadweb.org/viewtopic.php?f=15&t=50583 + if (parameters_copy_dict.length()==0) { + for(std::map::iterator i = getCommandPtr()->Parameters.begin(); i != getCommandPtr()->Parameters.end(); ++i) { + parameters_copy_dict.setItem(i->first, Py::Float(i->second)); + } } - return dict; + return parameters_copy_dict; } void CommandPy::setParameters(Py::Dict arg) @@ -200,6 +208,7 @@ void CommandPy::setParameters(Py::Dict arg) throw Py::TypeError("The dictionary can only contain number values"); } getCommandPtr()->Parameters[ckey]=cvalue; + parameters_copy_dict.clear(); } } @@ -224,6 +233,7 @@ PyObject* CommandPy::setFromGCode(PyObject *args) std::string gcode(pstr); try { getCommandPtr()->setFromGCode(gcode); + parameters_copy_dict.clear(); } catch (const Base::Exception& e) { PyErr_SetString(PyExc_ValueError, e.what()); @@ -249,6 +259,7 @@ void CommandPy::setPlacement(Py::Object arg) Py::Type PlacementType(pyType.o); if(arg.isType(PlacementType)) { getCommandPtr()->setFromPlacement( *static_cast((*arg))->getPlacementPtr() ); + parameters_copy_dict.clear(); } else throw Py::TypeError("Argument must be a placement"); } @@ -259,6 +270,7 @@ PyObject* CommandPy::transform(PyObject *args) if ( PyArg_ParseTuple(args, "O!", &(Base::PlacementPy::Type), &placement) ) { Base::PlacementPy *p = static_cast(placement); Path::Command trCmd = getCommandPtr()->transform( *p->getPlacementPtr() ); + parameters_copy_dict.clear(); return new CommandPy(new Path::Command(trCmd)); } else throw Py::TypeError("Argument must be a placement"); @@ -302,6 +314,7 @@ int CommandPy::setCustomAttributes(const char* attr, PyObject* obj) return 0; } getCommandPtr()->Parameters[satt]=cvalue; + parameters_copy_dict.clear(); return 1; } } diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 444be1ecd4..1e1c4e305c 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -96,6 +96,10 @@ SET(PathScripts_SRCS PathScripts/PathProfileFaces.py PathScripts/PathProfileFacesGui.py PathScripts/PathProfileGui.py + PathScripts/PathProperty.py + PathScripts/PathPropertyBag.py + PathScripts/PathPropertyBagGui.py + PathScripts/PathPropertyEditor.py PathScripts/PathSanity.py PathScripts/PathSelection.py PathScripts/PathSetupSheet.py @@ -198,6 +202,7 @@ SET(PathTests_SRCS PathTests/TestPathOpTools.py PathTests/TestPathPost.py PathTests/TestPathPreferences.py + PathTests/TestPathPropertyBag.py PathTests/TestPathSetupSheet.py PathTests/TestPathStock.py PathTests/TestPathThreadMilling.py @@ -208,6 +213,9 @@ SET(PathTests_SRCS PathTests/TestPathUtil.py PathTests/TestPathVcarve.py PathTests/TestPathVoronoi.py + PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb + PathTests/Tools/Library/test-path-tool-bit-library-00.fctl + PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd PathTests/boxtest.fcstd PathTests/test_centroid_00.ngc PathTests/test_geomop.fcstd @@ -279,6 +287,14 @@ INSTALL( Mod/Path/PathTests ) +INSTALL( + DIRECTORY + PathTests/Tools + DESTINATION + Mod/Path/PathTests +) + + INSTALL( FILES ${PathScripts_post_SRCS} diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index c99246c667..832f74f868 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -121,6 +121,8 @@ panels/PageOpVcarveEdit.ui panels/PathEdit.ui panels/PointEdit.ui + panels/PropertyBag.ui + panels/PropertyCreate.ui panels/SetupGlobal.ui panels/SetupOp.ui panels/ToolBitEditor.ui diff --git a/src/Mod/Path/Gui/Resources/panels/PageBaseGeometryEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageBaseGeometryEdit.ui index 3ea92feef0..42441cbc2c 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageBaseGeometryEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageBaseGeometryEdit.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 250 + 476 + 342 @@ -23,94 +23,28 @@ :/icons/Path_BaseGeometry.svg:/icons/Path_BaseGeometry.svg - - - - - - - Import - - - - - - - <html><head/><body><p>Remove the selected list items from the list of base geometries. The operation will not be applied to them.</p></body></html> - - - Remove - - - - - - - - 0 - 0 - - - - <html><head/><body><p>List of operations with Base Geometry in current Job.</p></body></html> - - - - - - - true - - - - 0 - 0 - - - - <html><head/><body><p>Select one or more features in the 3d view and press 'Add' to add them as the base items for this operation.</p><p><br/></p><p>Selected features can be deleted entirely.</p></body></html> - - - QAbstractItemView::ExtendedSelection - - - - - - - <html><head/><body><p>Clears list of base geometries.</p></body></html> - - - Clear - - - - - - - <html><head/><body><p>Add selected features to the list of base geometries for this operation.</p></body></html> - - - Add - - - - - - - All objects will be processed using the same operation properties - - - Qt::AlignCenter - - - true - - - - + + + + + <html><head/><body><p>Add selected features to the list of base geometries for this operation.</p></body></html> + + + Add + + - + + + + <html><head/><body><p>Remove the selected list items from the list of base geometries. The operation will not be applied to them.</p></body></html> + + + Remove + + + + Qt::Vertical @@ -123,14 +57,70 @@ + + + + <html><head/><body><p>Clears list of base geometries.</p></body></html> + + + Clear + + + + + + + All objects will be processed using the same operation properties + + + Qt::AlignCenter + + + true + + + + + + + true + + + + 0 + 0 + + + + <html><head/><body><p>Select one or more features in the 3d view and press 'Add' to add them as the base items for this operation.</p><p><br/></p><p>Selected features can be deleted entirely.</p></body></html> + + + QAbstractItemView::ExtendedSelection + + + + + + + Import + + + + + + + + 0 + 0 + + + + <html><head/><body><p>List of operations with Base Geometry in current Job.</p></body></html> + + + - - baseList - addBase - deleteBase - clearBase - diff --git a/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui b/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui new file mode 100644 index 0000000000..ac9af5c8d5 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui @@ -0,0 +1,78 @@ + + + Form + + + + 0 + 0 + 552 + 651 + + + + Property Bag + + + + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::AllEditTriggers + + + true + + + true + + + false + + + + + + + + 0 + 0 + + + + + + + Remove + + + + + + + Modify... + + + + + + + Add... + + + + + + + + + + table + add + remove + + + + diff --git a/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui b/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui new file mode 100644 index 0000000000..c65c02ea82 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui @@ -0,0 +1,182 @@ + + + Dialog + + + + 0 + 0 + 480 + 452 + + + + Create Property + + + + + + Name + + + + + + + <html><head/><body><p>Name of property.</p></body></html> + + + + + + + <html><head/><body><p>The category group the property belongs to.</p></body></html> + + + true + + + + + + + Group + + + + + + + <html><head/><body><p>The type of the property value.</p></body></html> + + + + + + + Type + + + + + + + val1,val2,val3,... + + + + + + + <html><head/><body><p>ToolTip to be displayed when user hovers mouse over property.</p></body></html> + + + true + + + + + + + Enums + + + + + + + ToolTip + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p>Check if you want to create several properties in a batch.</p></body></html> + + + Create another + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + propertyName + propertyGroup + propertyType + propertyEnum + propertyInfo + createAnother + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index 1342da5649..d77a523a12 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -6,7 +6,7 @@ 0 0 - 401 + 489 715 @@ -17,7 +17,7 @@ - + 0 0 @@ -32,6 +32,12 @@ + + + 0 + 0 + + Tool Bit @@ -65,6 +71,12 @@ + + + 0 + 0 + + 0 @@ -83,6 +95,9 @@ <html><head/><body><p>The file which defines the type and shape of the Tool Bit.</p></body></html> + + path + @@ -104,7 +119,7 @@ - Bit Parameter + Parameter @@ -184,7 +199,7 @@ - + 0 @@ -200,9 +215,6 @@ QAbstractItemView::AllEditTriggers - - true - diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui index 70650bf817..805f7a3e3d 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -173,6 +173,12 @@ QFrame::Box + + QAbstractItemView::NoEditTriggers + + + false + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index e26c2fe0da..20e35d8590 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -154,6 +154,10 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], extracmdlist) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], ["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP("Path", "Utils")], + ["Path_PropertyBag"]) + self.dressupcmds = dressupcmdlist curveAccuracy = PathPreferences.defaultLibAreaCurveAccuracy() diff --git a/src/Mod/Path/PathScripts/PathAdaptive.py b/src/Mod/Path/PathScripts/PathAdaptive.py index 162406cfb3..e171813f80 100644 --- a/src/Mod/Path/PathScripts/PathAdaptive.py +++ b/src/Mod/Path/PathScripts/PathAdaptive.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2018 Kresimir Tusek * +# * Copyright (c) 2019-2021 Schildkroet * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -20,12 +21,6 @@ # * Suite 330, Boston, MA 02111-1307, USA * # * * # *************************************************************************** -# * * -# * Additional modifications and contributions beginning 2019 * -# * by Schildkroet. (https://github.com/Schildkroet) * -# * * -# *************************************************************************** - import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils @@ -88,6 +83,13 @@ def discretize(edge, flipDirection = False): return pts +def CalcHelixConePoint(height, cur_z, radius, angle): + x = ((height - cur_z) / height) * radius * math.cos(math.radians(angle)*cur_z) + y = ((height - cur_z) / height) * radius * math.sin(math.radians(angle)*cur_z) + z = cur_z + + return {'X': x, 'Y': y, 'Z': z} + def GenerateGCode(op,obj,adaptiveResults, helixDiameter): # pylint: disable=unused-argument if len(adaptiveResults) == 0 or len(adaptiveResults[0]["AdaptivePaths"]) == 0: @@ -112,6 +114,11 @@ def GenerateGCode(op,obj,adaptiveResults, helixDiameter): if float(obj.HelixAngle) < 1: obj.HelixAngle = 1 + if float(obj.HelixAngle) > 89: + obj.HelixAngle = 89 + + if float(obj.HelixConeAngle) < 0: + obj.HelixConeAngle = 0 helixAngleRad = math.pi * float(obj.HelixAngle) / 180.0 depthPerOneCircle = length * math.tan(helixAngleRad) @@ -121,7 +128,6 @@ def GenerateGCode(op,obj,adaptiveResults, helixDiameter): if stepUp < 0: stepUp = 0 - finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 if finish_step > stepDown: finish_step = stepDown @@ -136,7 +142,6 @@ def GenerateGCode(op,obj,adaptiveResults, helixDiameter): user_depths=None) - # ml: this is dangerous because it'll hide all unused variables hence forward # however, I don't know what lx and ly signify so I'll leave them for now # pylint: disable=unused-variable @@ -160,8 +165,8 @@ def GenerateGCode(op,obj,adaptiveResults, helixDiameter): p2 = region["StartPoint"] helixRadius = math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1]-p2[1]) * (p1[1]-p2[1])) - # helix ramp - if helixRadius > 0.001: + # Helix ramp + if helixRadius > 0.01: r = helixRadius - 0.01 maxfi = passDepth / depthPerOneCircle * 2 * math.pi @@ -183,26 +188,80 @@ def GenerateGCode(op,obj,adaptiveResults, helixDiameter): # move to start depth op.commandlist.append(Path.Command("G1", {"X": helixStart[0], "Y": helixStart[1], "Z": passStartDepth, "F": op.vertFeed})) - while fi < maxfi: - x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi) - y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi) - z = passStartDepth - fi / maxfi * (passStartDepth - passEndDepth) - op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "Z":z, "F": op.vertFeed})) - lx = x - ly = y - fi=fi+math.pi/16 + if obj.HelixConeAngle == 0: + while fi < maxfi: + x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi) + y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi) + z = passStartDepth - fi / maxfi * (passStartDepth - passEndDepth) + op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "Z":z, "F": op.vertFeed})) + lx = x + ly = y + fi=fi+math.pi/16 + + # one more circle at target depth to make sure center is cleared + maxfi = maxfi + 2*math.pi + while fi < maxfi: + x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi) + y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi) + z = passEndDepth + op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "Z":z, "F": op.horizFeed})) + lx = x + ly = y + fi = fi + math.pi/16 + + else: + # Cone + _HelixAngle = 360 - (float(obj.HelixAngle) * 4) + + if obj.HelixConeAngle > 6: + obj.HelixConeAngle = 6 + + helixRadius *= 0.9 + + # Calculate everything + helix_height = passStartDepth - passEndDepth + r_extra = helix_height * math.tan(math.radians(obj.HelixConeAngle)) + HelixTopRadius = helixRadius + r_extra + helix_full_height = HelixTopRadius * (math.cos(math.radians(obj.HelixConeAngle)) / math.sin(math.radians(obj.HelixConeAngle))) + + # Start height + z = passStartDepth + i = 0 + + # Default step down + z_step = 0.05 + + # Bigger angle, smaller step down + if _HelixAngle > 120: + z_step = 0.025 + if _HelixAngle > 240: + z_step = 0.015 + + p = None + # Calculate conical helix + while(z >= passEndDepth): + if z < passEndDepth: + z = passEndDepth + + p = CalcHelixConePoint(helix_full_height, i, HelixTopRadius, _HelixAngle) + op.commandlist.append(Path.Command("G1", { "X": p['X'] + region["HelixCenterPoint"][0], "Y": p['Y'] + region["HelixCenterPoint"][1], "Z": z, "F": op.vertFeed})) + z = z - z_step + i = i + z_step + + # Calculate some stuff for arcs at bottom + p['X'] = p['X'] + region["HelixCenterPoint"][0] + p['Y'] = p['Y'] + region["HelixCenterPoint"][1] + x_m = region["HelixCenterPoint"][0] - p['X'] + region["HelixCenterPoint"][0] + y_m = region["HelixCenterPoint"][1] - p['Y'] + region["HelixCenterPoint"][1] + i_off = (x_m - p['X']) / 2 + j_off = (y_m - p['Y']) / 2 + + # One more circle at target depth to make sure center is cleared + op.commandlist.append(Path.Command("G3", { "X": x_m, "Y": y_m, "Z": passEndDepth, "I": i_off, "J": j_off, "F": op.horizFeed})) + op.commandlist.append(Path.Command("G3", { "X": p['X'], "Y": p['Y'], "Z": passEndDepth, "I": -i_off, "J": -j_off, "F": op.horizFeed})) - # one more circle at target depth to make sure center is cleared - maxfi = maxfi + 2*math.pi - while fi < maxfi: - x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi) - y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi) - z = passEndDepth - op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "Z":z, "F": op.horizFeed})) - lx = x - ly = y - fi = fi + math.pi/16 else: + # Use arcs for helix - no conical shape support helixStart = [region["HelixCenterPoint"][0] + r, region["HelixCenterPoint"][1]] # rapid move to start point @@ -220,16 +279,16 @@ def GenerateGCode(op,obj,adaptiveResults, helixDiameter): curDep = passStartDepth while curDep > (passEndDepth + depthPerOneCircle): - op.commandlist.append(Path.Command("G2", { "X": x - (2*r), "Y": y, "Z": curDep - (depthPerOneCircle/2), "I": -r, "F": op.horizFeed})) - op.commandlist.append(Path.Command("G2", { "X": x, "Y": y, "Z": curDep - depthPerOneCircle, "I": r, "F": op.horizFeed})) + op.commandlist.append(Path.Command("G2", { "X": x - (2*r), "Y": y, "Z": curDep - (depthPerOneCircle/2), "I": -r, "F": op.vertFeed})) + op.commandlist.append(Path.Command("G2", { "X": x, "Y": y, "Z": curDep - depthPerOneCircle, "I": r, "F": op.vertFeed})) curDep = curDep - depthPerOneCircle lastStep = curDep - passEndDepth if lastStep > (depthPerOneCircle/2): - op.commandlist.append(Path.Command("G2", { "X": x - (2*r), "Y": y, "Z": curDep - (lastStep/2), "I": -r, "F": op.horizFeed})) - op.commandlist.append(Path.Command("G2", { "X": x, "Y": y, "Z": passEndDepth, "I": r, "F": op.horizFeed})) + op.commandlist.append(Path.Command("G2", { "X": x - (2*r), "Y": y, "Z": curDep - (lastStep/2), "I": -r, "F": op.vertFeed})) + op.commandlist.append(Path.Command("G2", { "X": x, "Y": y, "Z": passEndDepth, "I": r, "F": op.vertFeed})) else: - op.commandlist.append(Path.Command("G2", { "X": x - (2*r), "Y": y, "Z": passEndDepth, "I": -r, "F": op.horizFeed})) + op.commandlist.append(Path.Command("G2", { "X": x - (2*r), "Y": y, "Z": passEndDepth, "I": -r, "F": op.vertFeed})) op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "Z": passEndDepth, "F": op.vertFeed})) # one more circle at target depth to make sure center is cleared @@ -512,6 +571,7 @@ class PathAdaptive(PathOp.ObjectOp): obj.setEditorMode('AdaptiveInputState', 2) #hide this property obj.setEditorMode('AdaptiveOutputState', 2) #hide this property obj.addProperty("App::PropertyAngle", "HelixAngle", "Adaptive", "Helix ramp entry angle (degrees)") + obj.addProperty("App::PropertyAngle", "HelixConeAngle", "Adaptive", "Helix cone angle (degrees)") obj.addProperty("App::PropertyLength", "HelixDiameterLimit", "Adaptive", "Limit helix entry diameter, if limit larger than tool diameter or 0, tool diameter is used") @@ -527,6 +587,7 @@ class PathAdaptive(PathOp.ObjectOp): obj.Stopped = False obj.StopProcessing = False obj.HelixAngle = 5 + obj.HelixConeAngle = 0 obj.HelixDiameterLimit = 0.0 obj.AdaptiveInputState ="" obj.AdaptiveOutputState = "" diff --git a/src/Mod/Path/PathScripts/PathAdaptiveGui.py b/src/Mod/Path/PathScripts/PathAdaptiveGui.py index e8c6d8da3f..0e25e92128 100644 --- a/src/Mod/Path/PathScripts/PathAdaptiveGui.py +++ b/src/Mod/Path/PathScripts/PathAdaptiveGui.py @@ -78,13 +78,22 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): # helix angle form.HelixAngle = QtGui.QDoubleSpinBox() - form.HelixAngle.setMinimum(0.1) - form.HelixAngle.setMaximum(90) - form.HelixAngle.setSingleStep(0.1) + form.HelixAngle.setMinimum(1) + form.HelixAngle.setMaximum(89) + form.HelixAngle.setSingleStep(1) form.HelixAngle.setValue(5) form.HelixAngle.setToolTip("Angle of the helix ramp entry") formLayout.addRow(QtGui.QLabel("Helix Ramp Angle"), form.HelixAngle) + # helix cone angle + form.HelixConeAngle = QtGui.QDoubleSpinBox() + form.HelixConeAngle.setMinimum(0) + form.HelixConeAngle.setMaximum(6) + form.HelixConeAngle.setSingleStep(1) + form.HelixConeAngle.setValue(0) + form.HelixConeAngle.setToolTip("Angle of the helix entry cone") + formLayout.addRow(QtGui.QLabel("Helix Cone Angle"), form.HelixConeAngle) + # helix diam. limit form.HelixDiameterLimit = QtGui.QDoubleSpinBox() form.HelixDiameterLimit.setMinimum(0.0) @@ -151,6 +160,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.StepOver.valueChanged) signals.append(self.form.Tolerance.valueChanged) signals.append(self.form.HelixAngle.valueChanged) + signals.append(self.form.HelixConeAngle.valueChanged) signals.append(self.form.HelixDiameterLimit.valueChanged) signals.append(self.form.LiftDistance.valueChanged) signals.append(self.form.KeepToolDownRatio.valueChanged) @@ -169,6 +179,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.StepOver.setValue(obj.StepOver) self.form.Tolerance.setValue(int(obj.Tolerance * 100)) self.form.HelixAngle.setValue(obj.HelixAngle) + self.form.HelixConeAngle.setValue(obj.HelixConeAngle) self.form.HelixDiameterLimit.setValue(obj.HelixDiameterLimit) self.form.LiftDistance.setValue(obj.LiftDistance) if hasattr(obj, 'KeepToolDownRatio'): @@ -198,6 +209,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): obj.StepOver = self.form.StepOver.value() obj.Tolerance = 1.0 * self.form.Tolerance.value() / 100.0 obj.HelixAngle = self.form.HelixAngle.value() + obj.HelixConeAngle = self.form.HelixConeAngle.value() obj.HelixDiameterLimit = self.form.HelixDiameterLimit.value() obj.LiftDistance = self.form.LiftDistance.value() diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index d68e4676dd..c0854934a3 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -231,8 +231,6 @@ class ObjectOp(PathOp.ObjectOp): area.add(baseobject) areaParams = self.areaOpAreaParams(obj, isHole) # pylint: disable=assignment-from-no-return - if hasattr(obj, 'ExpandProfile') and obj.ExpandProfile != 0: - areaParams = self.areaOpAreaParamsExpandProfile(obj, isHole) # pylint: disable=assignment-from-no-return heights = [i for i in self.depthparams] PathLog.debug('depths: {}'.format(heights)) diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py index 85dbb81e0a..c768fd53ab 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathCustomGui.py b/src/Mod/Path/PathScripts/PathCustomGui.py index 987bcfb50e..658468a4e0 100644 --- a/src/Mod/Path/PathScripts/PathCustomGui.py +++ b/src/Mod/Path/PathScripts/PathCustomGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCustom as PathCustom import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathDeburrGui.py b/src/Mod/Path/PathScripts/PathDeburrGui.py index 1b0a0da690..d98642f4dd 100644 --- a/src/Mod/Path/PathScripts/PathDeburrGui.py +++ b/src/Mod/Path/PathScripts/PathDeburrGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathDeburr as PathDeburr import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py index 34cffb1d68..6152c51433 100644 --- a/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py +++ b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathDressupPathBoundary as PathDressupPathBoundary import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathDressupTagGui.py b/src/Mod/Path/PathScripts/PathDressupTagGui.py index 0a6c01dc6b..be312176a1 100644 --- a/src/Mod/Path/PathScripts/PathDressupTagGui.py +++ b/src/Mod/Path/PathScripts/PathDressupTagGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGeom as PathGeom import PathScripts.PathGetPoint as PathGetPoint import PathScripts.PathDressupHoldingTags as PathDressupTag @@ -418,7 +419,7 @@ class PathDressupTagViewProvider: '''this makes sure that the base operation is added back to the job and visible''' # pylint: disable=unused-argument PathLog.track() - if self.obj.Base.ViewObject: + if self.obj.Base and self.obj.Base.ViewObject: self.obj.Base.ViewObject.Visibility = True job = PathUtils.findParentJob(self.obj) if arg1.Object and arg1.Object.Base and job: diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py index 534d44e1a8..4ebee90d5f 100644 --- a/src/Mod/Path/PathScripts/PathDrillingGui.py +++ b/src/Mod/Path/PathScripts/PathDrillingGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui import PathScripts.PathDrilling as PathDrilling import PathScripts.PathGui as PathGui diff --git a/src/Mod/Path/PathScripts/PathEngraveGui.py b/src/Mod/Path/PathScripts/PathEngraveGui.py index 95d5ce9dc5..6301c4882d 100644 --- a/src/Mod/Path/PathScripts/PathEngraveGui.py +++ b/src/Mod/Path/PathScripts/PathEngraveGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathEngrave as PathEngrave import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathGui.py b/src/Mod/Path/PathScripts/PathGui.py index c9fee42062..fb37725bf3 100644 --- a/src/Mod/Path/PathScripts/PathGui.py +++ b/src/Mod/Path/PathScripts/PathGui.py @@ -48,6 +48,7 @@ def updateInputField(obj, prop, widget, onBeforeChange=None): If onBeforeChange is specified it is called before a new value is assigned to the property. Returns True if a new value was assigned, False otherwise (new value is the same as the current). ''' + PathLog.track() value = widget.property('rawValue') attr = PathUtil.getProperty(obj, prop) attrValue = attr.Value if hasattr(attr, 'Value') else attr @@ -98,10 +99,12 @@ class QuantitySpinBox: PathLog.track(widget) self.widget = widget self.onBeforeChange = onBeforeChange + self.prop = None self.attachTo(obj, prop) def attachTo(self, obj, prop = None): '''attachTo(obj, prop=None) ... use an existing editor for the given object and property''' + PathLog.track(self.prop, prop) self.obj = obj self.prop = prop if obj and prop: @@ -119,12 +122,14 @@ class QuantitySpinBox: def expression(self): '''expression() ... returns the expression if one is bound to the property''' + PathLog.track(self.prop, self.valid) if self.valid: return self.widget.property('expression') return '' def setMinimum(self, quantity): '''setMinimum(quantity) ... set the minimum''' + PathLog.track(self.prop, self.valid) if self.valid: value = quantity.Value if hasattr(quantity, 'Value') else quantity self.widget.setProperty('setMinimum', value) @@ -133,6 +138,7 @@ class QuantitySpinBox: '''updateSpinBox(quantity=None) ... update the display value of the spin box. If no value is provided the value of the bound property is used. quantity can be of type Quantity or Float.''' + PathLog.track(self.prop, self.valid) if self.valid: if quantity is None: quantity = PathUtil.getProperty(self.obj, self.prop) @@ -141,6 +147,7 @@ class QuantitySpinBox: def updateProperty(self): '''updateProperty() ... update the bound property with the value from the spin box''' + PathLog.track(self.prop, self.valid) if self.valid: return updateInputField(self.obj, self.prop, self.widget, self.onBeforeChange) return None diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py index cd48d41309..405dcb4fcc 100644 --- a/src/Mod/Path/PathScripts/PathGuiInit.py +++ b/src/Mod/Path/PathScripts/PathGuiInit.py @@ -66,6 +66,7 @@ def Startup(): # from PathScripts import PathProfileEdgesGui # from PathScripts import PathProfileFacesGui from PathScripts import PathProfileGui + from PathScripts import PathPropertyBagGui from PathScripts import PathSanity from PathScripts import PathSetupSheetGui from PathScripts import PathSimpleCopy diff --git a/src/Mod/Path/PathScripts/PathHelixGui.py b/src/Mod/Path/PathScripts/PathHelixGui.py index 141cce33d9..b65048ee9c 100644 --- a/src/Mod/Path/PathScripts/PathHelixGui.py +++ b/src/Mod/Path/PathScripts/PathHelixGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui import PathScripts.PathHelix as PathHelix import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathJobGui.py b/src/Mod/Path/PathScripts/PathJobGui.py index f5d2fa9db2..7b7fcc483b 100644 --- a/src/Mod/Path/PathScripts/PathJobGui.py +++ b/src/Mod/Path/PathScripts/PathJobGui.py @@ -31,6 +31,7 @@ from PySide import QtCore, QtGui import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathJob as PathJob import PathScripts.PathJobCmd as PathJobCmd import PathScripts.PathJobDlg as PathJobDlg @@ -521,6 +522,7 @@ class StockCreateCylinderEdit(StockEdit): class StockFromExistingEdit(StockEdit): Index = 3 StockType = PathStock.StockType.Unknown + StockLabelPrefix = 'Stock' def editorFrame(self): return self.form.stockFromExisting @@ -529,7 +531,7 @@ class StockFromExistingEdit(StockEdit): stock = self.form.stockExisting.itemData(self.form.stockExisting.currentIndex()) if not (hasattr(obj.Stock, 'Objects') and len(obj.Stock.Objects) == 1 and obj.Stock.Objects[0] == stock): if stock: - stock = PathJob.createResourceClone(obj, stock, 'Stock', 'Stock') + stock = PathJob.createResourceClone(obj, stock, self.StockLabelPrefix , 'Stock') stock.ViewObject.Visibility = True PathStock.SetupStockObject(stock, PathStock.StockType.Unknown) stock.Proxy.execute(stock) @@ -555,7 +557,9 @@ class StockFromExistingEdit(StockEdit): index = -1 for i, solid in enumerate(self.candidates(obj)): self.form.stockExisting.addItem(solid.Label, solid) - if solid.Label == stockName: + label="{}-{}".format(self.StockLabelPrefix, solid.Label) + + if label == stockName: index = i self.form.stockExisting.setCurrentIndex(index if index != -1 else 0) diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py index 8b2679a295..c0e24b03b4 100644 --- a/src/Mod/Path/PathScripts/PathOp.py +++ b/src/Mod/Path/PathScripts/PathOp.py @@ -422,7 +422,10 @@ class ObjectOp(object): zmax = max(zmax, bb.ZMax) for sub in sublist: try: - fbb = base.Shape.getElement(sub).BoundBox + if sub: + fbb = base.Shape.getElement(sub).BoundBox + else: + fbb = base.Shape.BoundBox zmin = max(zmin, faceZmin(bb, fbb)) zmax = max(zmax, fbb.ZMax) except Part.OCCError as e: diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index 64d93f2966..0aa304a5fc 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGeom as PathGeom import PathScripts.PathGetPoint as PathGetPoint import PathScripts.PathGui as PathGui @@ -42,13 +43,8 @@ __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Base classes and framework for Path operation's UI" -LOGLEVEL = False - -if LOGLEVEL: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) def translate(context, text, disambig=None): @@ -566,6 +562,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): return False def addBase(self): + PathLog.track() if self.addBaseGeometry(FreeCADGui.Selection.getSelectionEx()): # self.obj.Proxy.execute(self.obj) self.setFields(self.obj) @@ -636,11 +633,18 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): # Set base geometry list window to resize based on contents # Code reference: # https://stackoverflow.com/questions/6337589/qlistwidget-adjust-size-to-content + # ml: disabling this logic because I can't get it to work on HPD monitor. + # On my systems the values returned by the list object are also incorrect on + # creation, leading to a list object of size 15. count() always returns 0 until + # the list is actually displayed. The same is true for sizeHintForRow(0), which + # returns -1 until the widget is rendered. The widget claims to have a size of + # (100, 30), once it becomes visible the size is (535, 192). + # Leaving the framework here in case somebody figures out how to set this up + # properly. qList = self.form.baseList - # qList.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - col = qList.width() # 300 row = (qList.count() + qList.frameWidth()) * 15 - qList.setFixedSize(col, row) + #qList.setMinimumHeight(row) + PathLog.debug("baseList({}, {}) {} * {}".format(qList.size(), row, qList.count(), qList.sizeHintForRow(0))) class TaskPanelBaseLocationPage(TaskPanelPage): diff --git a/src/Mod/Path/PathScripts/PathPocketBaseGui.py b/src/Mod/Path/PathScripts/PathPocketBaseGui.py index 915b6ea71a..68920b0a5e 100644 --- a/src/Mod/Path/PathScripts/PathPocketBaseGui.py +++ b/src/Mod/Path/PathScripts/PathPocketBaseGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathPocketShapeGui.py b/src/Mod/Path/PathScripts/PathPocketShapeGui.py index 53bc26c336..4e1b651148 100644 --- a/src/Mod/Path/PathScripts/PathPocketShapeGui.py +++ b/src/Mod/Path/PathScripts/PathPocketShapeGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGeom as PathGeom import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index d914568631..a1c1e58fc5 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -151,26 +151,9 @@ def searchPathsPost(): return paths -def searchPathsTool(sub='Bit'): +def searchPathsTool(sub): paths = [] - - if 'Bit' == sub: - paths.append("{}/Bit".format(os.path.dirname(lastPathToolLibrary()))) - paths.append(lastPathToolBit()) - - if 'Library' == sub: - paths.append(lastPathToolLibrary()) - if 'Shape' == sub: - paths.append(lastPathToolShape()) - - def appendPath(p, sub): - if p: - paths.append(os.path.join(p, 'Tools', sub)) - paths.append(os.path.join(p, sub)) - paths.append(p) - appendPath(defaultFilePath(), sub) - appendPath(macroFilePath(), sub) - appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub) + paths.append(os.path.join(FreeCAD.getHomePath(), 'Mod', 'Path', 'Tools', sub)) return paths @@ -263,15 +246,19 @@ def setDefaultTaskPanelLayout(style): def experimentalFeaturesEnabled(): return preferences().GetBool(EnableExperimentalFeatures, False) + def suppressAllSpeedsWarning(): return preferences().GetBool(WarningSuppressAllSpeeds, True) + def suppressRapidSpeedsWarning(): return suppressAllSpeedsWarning() or preferences().GetBool(WarningSuppressRapidSpeeds, True) + def suppressSelectionModeWarning(): return preferences().GetBool(WarningSuppressSelectionMode, True) + def suppressOpenCamLibWarning(): return preferences().GetBool(WarningSuppressOpenCamLib, True) @@ -316,7 +303,8 @@ def lastPathToolLibrary(): def setLastPathToolLibrary(path): PathLog.track(path) curLib = lastFileToolLibrary() - if os.path.split(curLib)[0] != path: + PathLog.debug('curLib: {}'.format(curLib)) + if curLib and os.path.split(curLib)[0] != path: setLastFileToolLibrary('') # a path is known but not specific file return preferences().SetString(LastPathToolLibrary, path) diff --git a/src/Mod/Path/PathScripts/PathProbeGui.py b/src/Mod/Path/PathScripts/PathProbeGui.py index a00ec913e8..29c97d7adc 100644 --- a/src/Mod/Path/PathScripts/PathProbeGui.py +++ b/src/Mod/Path/PathScripts/PathProbeGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathProbe as PathProbe import PathScripts.PathOpGui as PathOpGui import PathScripts.PathGui as PathGui diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index 8112b81840..b62cb53e45 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -99,10 +99,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): return [ ("App::PropertyEnumeration", "Direction", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)")), - ("App::PropertyLength", "ExpandProfile", "Profile", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Extend the profile clearing beyond the Extra Offset.")), - ("App::PropertyPercent", "ExpandProfileStepOver", "Profile", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), ("App::PropertyEnumeration", "HandleMultipleFeatures", "Profile", QtCore.QT_TRANSLATE_NOOP("PathPocket", "Choose how to process multiple Base Geometry features.")), ("App::PropertyEnumeration", "JoinType", "Profile", @@ -149,8 +145,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): return { 'AttemptInverseAngle': True, 'Direction': 'CW', - 'ExpandProfile': 0.0, - 'ExpandProfileStepOver': 100, 'HandleMultipleFeatures': 'Individually', 'InverseAngle': False, 'JoinType': 'Round', @@ -261,29 +255,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): return params - def areaOpAreaParamsExpandProfile(self, obj, isHole): - '''areaOpPathParamsExpandProfile(obj, isHole) ... return dictionary with area parameters for expanded profile''' - params = {} - - params['Fill'] = 1 - params['Coplanar'] = 0 - params['PocketMode'] = 1 - params['SectionCount'] = -1 - # params['Angle'] = obj.ZigZagAngle - # params['FromCenter'] = (obj.StartAt == "Center") - params['PocketStepover'] = self.tool.Diameter * (float(obj.ExpandProfileStepOver) / 100.0) - extraOffset = obj.OffsetExtra.Value - if False: # self.pocketInvertExtraOffset(): # Method simply returns False - extraOffset = 0.0 - extraOffset - params['PocketExtraOffset'] = extraOffset - params['ToolRadius'] = self.radius - - # Pattern = ['ZigZag', 'Offset', 'Spiral', 'ZigZagOffset', 'Line', 'Grid', 'Triangle'] - params['PocketMode'] = 2 # Pattern.index(obj.OffsetPattern) + 1 - params['JoinType'] = 0 # jointype = ['Round', 'Square', 'Miter'] - - return params - def areaOpPathParams(self, obj, isHole): '''areaOpPathParams(obj, isHole) ... returns dictionary with path parameters. Do not overwrite.''' @@ -310,9 +281,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): def areaOpUseProjection(self, obj): '''areaOpUseProjection(obj) ... returns True''' - if obj.ExpandProfile.Value == 0.0: - return True - return False + return True def opUpdateDepths(self, obj): if hasattr(obj, 'Base') and obj.Base.__len__() == 0: @@ -331,7 +300,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False self.inaccessibleMsg = translate('PathProfile', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') self.offsetExtra = obj.OffsetExtra.Value - self.expandProfile = None if self.isDebug: for grpNm in ['tmpDebugGrp', 'tmpDebugGrp001']: @@ -343,11 +311,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): tmpGrpNm = self.tmpGrp.Name self.JOB = PathUtils.findParentJob(obj) - if obj.ExpandProfile.Value != 0.0: - import PathScripts.PathSurfaceSupport as PathSurfaceSupport - self.PathSurfaceSupport = PathSurfaceSupport - self.expandProfile = True - if obj.UseComp: self.useComp = True self.ofstRadius = self.radius + self.offsetExtra @@ -435,10 +398,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): if ot < 1: cont = True if cont: - if self.expandProfile: - shapeEnv = self._getExpandedProfileEnvelope(obj, f, True, obj.StartDepth.Value, finDep) - else: - shapeEnv = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams) + shapeEnv = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams) if shapeEnv: self._addDebugObject('HoleShapeEnvelope', shapeEnv) @@ -460,11 +420,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): custDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope try: - # env = PathUtils.getEnvelope(profileshape, depthparams=custDepthparams) - if self.expandProfile: - shapeEnv = self._getExpandedProfileEnvelope(obj, shape, False, obj.StartDepth.Value, finDep) - else: - shapeEnv = PathUtils.getEnvelope(profileshape, depthparams=custDepthparams) + shapeEnv = PathUtils.getEnvelope(profileshape, depthparams=custDepthparams) except Exception as ee: # pylint: disable=broad-except # PathUtils.getEnvelope() failed to return an object. msg = translate('Path', 'Unable to create path for face(s).') @@ -481,12 +437,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): finalDep = obj.FinalDepth.Value custDepthparams = self.depthparams self._addDebugObject('Rotation_Indiv_Shp', shape) - - if self.expandProfile: - shapeEnv = self._getExpandedProfileEnvelope(obj, shape, False, obj.StartDepth.Value, finalDep) - else: - shapeEnv = PathUtils.getEnvelope(shape, depthparams=custDepthparams) - + shapeEnv = PathUtils.getEnvelope(shape, depthparams=custDepthparams) if shapeEnv: self._addDebugObject('IndivCutShapeEnv', shapeEnv) tup = shapeEnv, False, 'pathProfile', angle, axis, strDep, finalDep @@ -499,11 +450,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): if 1 == len(self.model) and hasattr(self.model[0], "Proxy"): if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet - # Cancel ExpandProfile feature. Unavailable for ArchPanels. - if obj.ExpandProfile.Value != 0.0: - obj.ExpandProfile.Value == 0.0 - msg = translate('PathProfile', 'No ExpandProfile support for ArchPanel models.') - FreeCAD.Console.PrintWarning(msg + '\n') modelProxy = self.model[0].Proxy # Process circles and holes if requested by user if obj.processCircles or obj.processHoles: @@ -631,77 +577,14 @@ class ObjectProfile(PathAreaOp.ObjectOp): PathLog.debug('_openingType() ' + msg) return -2 - # Method for expanded profile - def _getExpandedProfileEnvelope(self, obj, faceShape, isHole, strDep, finalDep): - shapeZ = faceShape.BoundBox.ZMin - - def calculateOffsetValue(obj, isHole): - offset = obj.ExpandProfile.Value + obj.OffsetExtra.Value # 0.0 - if obj.UseComp: - offset = obj.OffsetExtra.Value + self.tool.Diameter - offset += obj.ExpandProfile.Value - if isHole: - if obj.Side == 'Outside': - offset = 0 - offset - else: - if obj.Side == 'Inside': - offset = 0 - offset - return offset - - faceEnv = self.PathSurfaceSupport.getShapeEnvelope(faceShape) - # newFace = self.PathSurfaceSupport.getSliceFromEnvelope(faceEnv) - newFace = self.PathSurfaceSupport.getShapeSlice(faceEnv) - # Compute necessary offset - offsetVal = calculateOffsetValue(obj, isHole) - expandedFace = self.PathSurfaceSupport.extractFaceOffset(newFace, offsetVal, newFace) - if expandedFace: - if shapeZ != 0.0: - expandedFace.translate(FreeCAD.Vector(0.0, 0.0, shapeZ)) - newFace.translate(FreeCAD.Vector(0.0, 0.0, shapeZ)) - - if isHole: - if obj.Side == 'Outside': - newFace = newFace.cut(expandedFace) - else: - newFace = expandedFace.cut(newFace) - else: - if obj.Side == 'Inside': - newFace = newFace.cut(expandedFace) - else: - newFace = expandedFace.cut(newFace) - - if finalDep - shapeZ != 0: - newFace.translate(FreeCAD.Vector(0.0, 0.0, finalDep - shapeZ)) - - if strDep - finalDep != 0: - if newFace.Area > 0: - return newFace.extrude(FreeCAD.Vector(0.0, 0.0, strDep - finalDep)) - else: - PathLog.debug('No expanded profile face shape.\n') - return False - else: - PathLog.debug(translate('PathProfile', 'Failed to extract offset(s) for expanded profile.') + '\n') - - PathLog.debug(translate('PathProfile', 'Failed to expand profile.') + '\n') - return False - # Method to handle each model as a whole, when no faces are selected - # It includes ExpandProfile implementation def _processEachModel(self, obj): shapeTups = list() for base in self.model: if hasattr(base, 'Shape'): env = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams) - if self.expandProfile: - eSlice = self.PathSurfaceSupport.getCrossSection(env) # getSliceFromEnvelope(env) - eSlice.translate(FreeCAD.Vector(0.0, 0.0, base.Shape.BoundBox.ZMin - env.BoundBox.ZMin)) - self._addDebugObject('ModelSlice', eSlice) - shapeEnv = self._getExpandedProfileEnvelope(obj, eSlice, False, obj.StartDepth.Value, obj.FinalDepth.Value) - else: - shapeEnv = env - - if shapeEnv: - shapeTups.append((shapeEnv, False)) + if env: + shapeTups.append((env, False)) return shapeTups # Edges pre-processing @@ -711,7 +594,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): basewires = list() delPairs = list() ezMin = None - self.cutOut = self.tool.Diameter * (float(obj.ExpandProfileStepOver) / 100.0) + self.cutOut = self.tool.Diameter for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] @@ -745,11 +628,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) f = flatWire.Wires[0] if f: - if self.expandProfile: - shapeEnv = self._getExpandedProfileEnvelope(obj, Part.Face(f), False, obj.StartDepth.Value, ezMin) - else: - shapeEnv = PathUtils.getEnvelope(Part.Face(f), depthparams=self.depthparams) - + shapeEnv = PathUtils.getEnvelope(Part.Face(f), depthparams=self.depthparams) if shapeEnv: tup = shapeEnv, False, 'Profile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) @@ -771,14 +650,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): self._addDebugObject('FlatWire', flatWire) - if self.expandProfile: - # Identify list of pass offset values for expanded profile paths - regularOfst = self.ofstRadius - targetOfst = regularOfst + obj.ExpandProfile.Value - while regularOfst < targetOfst: - regularOfst += self.cutOut - passOffsets.insert(0, regularOfst) - for po in passOffsets: self.ofstRadius = po cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index a7f77d7062..8a85a6abfa 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui import PathScripts.PathProfile as PathProfile diff --git a/src/Mod/Path/PathScripts/PathProperty.py b/src/Mod/Path/PathScripts/PathProperty.py new file mode 100644 index 0000000000..7e3eeb1e6f --- /dev/null +++ b/src/Mod/Path/PathScripts/PathProperty.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** + +import PathScripts.PathLog as PathLog + +__title__ = "Property type abstraction for editing purposes" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Prototype objects to allow extraction of setup sheet values and editing." + + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + +class Property(object): + '''Base class for all prototype properties''' + def __init__(self, name, propType, category, info): + self.name = name + self.propType = propType + self.category = category + self.info = info + self.editorMode = 0 + self.value = None + + def setValue(self, value): + self.value = value + def getValue(self): + return self.value + + def setEditorMode(self, mode): + self.editorMode = mode + + def displayString(self): + if self.value is None: + t = self.typeString() + p = 'an' if t[0] in ['A', 'E', 'I', 'O', 'U'] else 'a' + return "%s %s" % (p, t) + return self.value + + def typeString(self): + return "Property" + + def setupProperty(self, obj, name, category, value): + created = False + if not hasattr(obj, name): + obj.addProperty(self.propType, name, category, self.info) + self.initProperty(obj, name) + created = True + setattr(obj, name, value) + return created + + def initProperty(self, obj, name): + pass + + def setValueFromString(self, string): + self.setValue(self.valueFromString(string)) + + def valueFromString(self, string): + return string + +class PropertyEnumeration(Property): + def typeString(self): + return "Enumeration" + + def setValue(self, value): + if list == type(value): + self.enums = value # pylint: disable=attribute-defined-outside-init + else: + super(PropertyEnumeration, self).setValue(value) + + def getEnumValues(self): + return self.enums + + def initProperty(self, obj, name): + setattr(obj, name, self.enums) + +class PropertyQuantity(Property): + def displayString(self): + if self.value is None: + return Property.displayString(self) + return self.value.getUserPreferred()[0] + +class PropertyAngle(PropertyQuantity): + def typeString(self): + return "Angle" + +class PropertyDistance(PropertyQuantity): + def typeString(self): + return "Distance" + +class PropertyLength(PropertyQuantity): + def typeString(self): + return "Length" + +class PropertyPercent(Property): + def typeString(self): + return "Percent" + +class PropertyFloat(Property): + def typeString(self): + return "Float" + + def valueFromString(self, string): + return float(string) + +class PropertyInteger(Property): + def typeString(self): + return "Integer" + + def valueFromString(self, string): + return int(string) + +class PropertyBool(Property): + def typeString(self): + return "Bool" + + def valueFromString(self, string): + return bool(string) + +class PropertyString(Property): + def typeString(self): + return "String" + +class PropertyMap(Property): + def typeString(self): + return "Map" + + def displayString(self, value): + return str(value) + +class OpPrototype(object): + + PropertyType = { + 'App::PropertyAngle': PropertyAngle, + 'App::PropertyBool': PropertyBool, + 'App::PropertyDistance': PropertyDistance, + 'App::PropertyEnumeration': PropertyEnumeration, + 'App::PropertyFile': PropertyString, + 'App::PropertyFloat': PropertyFloat, + 'App::PropertyFloatConstraint': Property, + 'App::PropertyFloatList': Property, + 'App::PropertyInteger': PropertyInteger, + 'App::PropertyIntegerList': PropertyInteger, + 'App::PropertyLength': PropertyLength, + 'App::PropertyLink': Property, + 'App::PropertyLinkList': Property, + 'App::PropertyLinkSubListGlobal': Property, + 'App::PropertyMap': PropertyMap, + 'App::PropertyPercent': PropertyPercent, + 'App::PropertyString': PropertyString, + 'App::PropertyStringList': Property, + 'App::PropertyVectorDistance': Property, + 'App::PropertyVectorList': Property, + 'Part::PropertyPartShape': Property, + } + + def __init__(self, name): + self.Label = name + self.properties = {} + self.DoNotSetDefaultValues = True + self.Proxy = None + + def __setattr__(self, name, val): + if name in ['Label', 'DoNotSetDefaultValues', 'properties', 'Proxy']: + if name == 'Proxy': + val = None # make sure the proxy is never set + return super(OpPrototype, self).__setattr__(name, val) + self.properties[name].setValue(val) + + def addProperty(self, typeString, name, category, info = None): + prop = self.PropertyType[typeString](name, typeString, category, info) + self.properties[name] = prop + return self + + def setEditorMode(self, name, mode): + self.properties[name].setEditorMode(mode) + + def getProperty(self, name): + return self.properties[name] + + def setupProperties(self, setup): + return [p for p in self.properties if p.name in setup] diff --git a/src/Mod/Path/PathScripts/PathPropertyBag.py b/src/Mod/Path/PathScripts/PathPropertyBag.py new file mode 100644 index 0000000000..2b21be2c6b --- /dev/null +++ b/src/Mod/Path/PathScripts/PathPropertyBag.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** + +import FreeCAD +import PySide + +__title__ = 'Generic property container to store some values.' +__author__ = 'sliptonic (Brad Collette)' +__url__ = 'https://www.freecadweb.org' +__doc__ = 'A generic container for typed properties in arbitrary categories.' + +def translate(context, text, disambig=None): + return PySide.QtCore.QCoreApplication.translate(context, text, disambig) + + +SupportedPropertyType = { + 'Angle' : 'App::PropertyAngle', + 'Bool' : 'App::PropertyBool', + 'Distance' : 'App::PropertyDistance', + 'Enumeration' : 'App::PropertyEnumeration', + 'File' : 'App::PropertyFile', + 'Float' : 'App::PropertyFloat', + 'Integer' : 'App::PropertyInteger', + 'Length' : 'App::PropertyLength', + 'Percent' : 'App::PropertyPercent', + 'String' : 'App::PropertyString', + } + +def getPropertyTypeName(o): + for typ in SupportedPropertyType: + if SupportedPropertyType[typ] == o: + return typ + raise IndexError() + +class PropertyBag(object): + '''Property container object.''' + + CustomPropertyGroups = 'CustomPropertyGroups' + CustomPropertyGroupDefault = 'User' + + def __init__(self, obj): + obj.addProperty('App::PropertyStringList', self.CustomPropertyGroups, 'Base', PySide.QtCore.QT_TRANSLATE_NOOP('PathPropertyBag', 'List of custom property groups')) + self.onDocumentRestored(obj) + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def onDocumentRestored(self, obj): + self.obj = obj + obj.setEditorMode(self.CustomPropertyGroups, 2) # hide + + def getCustomProperties(self): + '''getCustomProperties() ... Return a list of all custom properties created in this container.''' + return [p for p in self.obj.PropertiesList if self.obj.getGroupOfProperty(p) in self.obj.CustomPropertyGroups] + + def addCustomProperty(self, propertyType, name, group=None, desc=None): + '''addCustomProperty(propertyType, name, group=None, desc=None) ... adds a custom property and tracks its group.''' + if desc is None: + desc = '' + if group is None: + group = self.CustomPropertyGroupDefault + groups = self.obj.CustomPropertyGroups + if not group in groups: + groups.append(group) + self.obj.CustomPropertyGroups = groups + self.obj.addProperty(propertyType, name, group, desc) + + def refreshCustomPropertyGroups(self): + '''refreshCustomPropertyGroups() ... removes empty property groups, should be called after deleting properties.''' + customGroups = [] + for p in self.obj.PropertiesList: + group = self.obj.getGroupOfProperty(p) + if group in self.obj.CustomPropertyGroups and not group in customGroups: + customGroups.append(group) + self.obj.CustomPropertyGroups = customGroups + + +def Create(name = 'PropertyBag'): + obj = FreeCAD.ActiveDocument.addObject('App::FeaturePython', name) + obj.Proxy = PropertyBag(obj) + return obj + +def IsPropertyBag(obj): + '''Returns True if the supplied object is a property container (or its Proxy).''' + + if type(obj) == PropertyBag: + return True + if hasattr(obj, 'Proxy'): + return IsPropertyBag(obj.Proxy) + return False + diff --git a/src/Mod/Path/PathScripts/PathPropertyBagGui.py b/src/Mod/Path/PathScripts/PathPropertyBagGui.py new file mode 100644 index 0000000000..54f55d4a56 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathPropertyBagGui.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded +import PathScripts.PathIconViewProvider as PathIconViewProvider +import PathScripts.PathLog as PathLog +import PathScripts.PathPropertyBag as PathPropertyBag +import PathScripts.PathPropertyEditor as PathPropertyEditor +import PathScripts.PathUtil as PathUtil + +from PySide import QtCore, QtGui + +__title__ = "Property Bag Editor" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Task panel editor for a PropertyBag" + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +class ViewProvider(object): + '''ViewProvider for a PropertyBag. + It's sole job is to provide an icon and invoke the TaskPanel on edit.''' + + def __init__(self, vobj, name): + PathLog.track(name) + vobj.Proxy = self + self.icon = name + # mode = 2 + self.obj = None + self.vobj = None + + def attach(self, vobj): + PathLog.track() + self.vobj = vobj + self.obj = vobj.Object + + def getIcon(self): + return ":/icons/Path-SetupSheet.svg" + + def __getstate__(self): + return None + + def __setstate__(self, state): + # pylint: disable=unused-argument + return None + + def getDisplayMode(self, mode): + # pylint: disable=unused-argument + return 'Default' + + def setEdit(self, vobj, mode=0): + # pylint: disable=unused-argument + PathLog.track() + taskPanel = TaskPanel(vobj) + FreeCADGui.Control.closeDialog() + FreeCADGui.Control.showDialog(taskPanel) + taskPanel.setupUi() + return True + + def unsetEdit(self, vobj, mode): + # pylint: disable=unused-argument + FreeCADGui.Control.closeDialog() + return + + def claimChildren(self): + return [] + + def doubleClicked(self, vobj): + self.setEdit(vobj) + +class Delegate(QtGui.QStyledItemDelegate): + RoleObject = QtCore.Qt.UserRole + 1 + RoleProperty = QtCore.Qt.UserRole + 2 + RoleEditor = QtCore.Qt.UserRole + 3 + + + #def paint(self, painter, option, index): + # #PathLog.track(index.column(), type(option)) + + def createEditor(self, parent, option, index): + # pylint: disable=unused-argument + editor = PathPropertyEditor.Editor(index.data(self.RoleObject), index.data(self.RoleProperty)) + index.model().setData(index, editor, self.RoleEditor) + return editor.widget(parent) + + def setEditorData(self, widget, index): + PathLog.track(index.row(), index.column()) + index.data(self.RoleEditor).setEditorData(widget) + + def setModelData(self, widget, model, index): + # pylint: disable=unused-argument + PathLog.track(index.row(), index.column()) + editor = index.data(self.RoleEditor) + editor.setModelData(widget) + index.model().setData(index, editor.displayString(), QtCore.Qt.DisplayRole) + + def updateEditorGeometry(self, widget, option, index): + # pylint: disable=unused-argument + widget.setGeometry(option.rect) + +class PropertyCreate(object): + + def __init__(self, obj, grp, typ, another): + self.obj = obj + self.form = FreeCADGui.PySideUic.loadUi(":panels/PropertyCreate.ui") + + obj.Proxy.refreshCustomPropertyGroups() + for g in sorted(obj.CustomPropertyGroups): + self.form.propertyGroup.addItem(g) + if grp: + self.form.propertyGroup.setCurrentText(grp) + + for t in sorted(PathPropertyBag.SupportedPropertyType): + self.form.propertyType.addItem(t) + if PathPropertyBag.SupportedPropertyType[t] == typ: + typ = t + if typ: + self.form.propertyType.setCurrentText(typ) + else: + self.form.propertyType.setCurrentText('String') + self.form.createAnother.setChecked(another) + + self.form.propertyGroup.currentTextChanged.connect(self.updateUI) + self.form.propertyGroup.currentIndexChanged.connect(self.updateUI) + self.form.propertyName.textChanged.connect(self.updateUI) + self.form.propertyType.currentIndexChanged.connect(self.updateUI) + self.form.propertyEnum.textChanged.connect(self.updateUI) + + def updateUI(self): + typeSet = True + if self.propertyIsEnumeration(): + self.form.labelEnum.setEnabled(True) + self.form.propertyEnum.setEnabled(True) + typeSet = self.form.propertyEnum.text().strip() != '' + else: + self.form.labelEnum.setEnabled(False) + self.form.propertyEnum.setEnabled(False) + if self.form.propertyEnum.text().strip(): + self.form.propertyEnum.setText('') + + ok = self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok) + + if typeSet and self.propertyName() and self.propertyGroup(): + ok.setEnabled(True) + else: + ok.setEnabled(False) + + def propertyName(self): + return self.form.propertyName.text().strip() + def propertyGroup(self): + return self.form.propertyGroup.currentText().strip() + def propertyType(self): + return PathPropertyBag.SupportedPropertyType[self.form.propertyType.currentText()].strip() + def propertyInfo(self): + return self.form.propertyInfo.toPlainText().strip() + def createAnother(self): + return self.form.createAnother.isChecked() + def propertyEnumerations(self): + return [s.strip() for s in self.form.propertyEnum.text().strip().split(',')] + def propertyIsEnumeration(self): + return self.propertyType() == 'App::PropertyEnumeration' + + def exec_(self, name): + if name: + # property exists - this is an edit operation + self.form.propertyName.setText(name) + if self.propertyIsEnumeration(): + self.form.propertyEnum.setText(','.join(self.obj.getEnumerationsOfProperty(name))) + self.form.propertyInfo.setText(self.obj.getDocumentationOfProperty(name)) + + self.form.labelName.setEnabled(False) + self.form.propertyName.setEnabled(False) + self.form.labelType.setEnabled(False) + self.form.propertyType.setEnabled(False) + self.form.createAnother.setEnabled(False) + + else: + self.form.propertyName.setText('') + self.form.propertyInfo.setText('') + self.form.propertyEnum.setText('') + #self.form.propertyName.setFocus() + + self.updateUI() + + return self.form.exec_() + +Panel = [] + +class TaskPanel(object): + ColumnName = 0 + #ColumnType = 1 + ColumnVal = 1 + #TableHeaders = ['Property', 'Type', 'Value'] + TableHeaders = ['Property', 'Value'] + + def __init__(self, vobj): + self.obj = vobj.Object + self.props = sorted(self.obj.Proxy.getCustomProperties()) + self.form = FreeCADGui.PySideUic.loadUi(":panels/PropertyBag.ui") + + # initialized later + self.model = None + self.delegate = None + FreeCAD.ActiveDocument.openTransaction(translate("PathPropertyBag", "Edit PropertyBag")) + Panel.append(self) + + def updateData(self, topLeft, bottomRight): + pass + + + def _setupProperty(self, i, name): + typ = PathPropertyBag.getPropertyTypeName(self.obj.getTypeIdOfProperty(name)) + val = PathUtil.getPropertyValueString(self.obj, name) + info = self.obj.getDocumentationOfProperty(name) + + self.model.setData(self.model.index(i, self.ColumnName), name, QtCore.Qt.EditRole) + #self.model.setData(self.model.index(i, self.ColumnType), typ, QtCore.Qt.EditRole) + self.model.setData(self.model.index(i, self.ColumnVal), self.obj, Delegate.RoleObject) + self.model.setData(self.model.index(i, self.ColumnVal), name, Delegate.RoleProperty) + self.model.setData(self.model.index(i, self.ColumnVal), val, QtCore.Qt.DisplayRole) + + self.model.setData(self.model.index(i, self.ColumnName), typ, QtCore.Qt.ToolTipRole) + #self.model.setData(self.model.index(i, self.ColumnType), info, QtCore.Qt.ToolTipRole) + self.model.setData(self.model.index(i, self.ColumnVal), info, QtCore.Qt.ToolTipRole) + + self.model.item(i, self.ColumnName).setEditable(False) + #self.model.item(i, self.ColumnType).setEditable(False) + + def setupUi(self): + PathLog.track() + + self.delegate = Delegate(self.form) + self.model = QtGui.QStandardItemModel(len(self.props), len(self.TableHeaders), self.form) + self.model.setHorizontalHeaderLabels(self.TableHeaders) + + for i,name in enumerate(self.props): + self._setupProperty(i, name) + + self.form.table.setModel(self.model) + self.form.table.setItemDelegateForColumn(self.ColumnVal, self.delegate) + self.form.table.resizeColumnsToContents() + + self.model.dataChanged.connect(self.updateData) + self.form.table.selectionModel().selectionChanged.connect(self.propertySelected) + self.form.add.clicked.connect(self.propertyAdd) + self.form.remove.clicked.connect(self.propertyRemove) + self.form.modify.clicked.connect(self.propertyModify) + self.form.table.doubleClicked.connect(self.propertyModifyIndex) + self.propertySelected([]) + + def accept(self): + FreeCAD.ActiveDocument.commitTransaction() + FreeCADGui.ActiveDocument.resetEdit() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def reject(self): + FreeCAD.ActiveDocument.abortTransaction() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def propertySelected(self, selection): + PathLog.track() + if selection: + self.form.modify.setEnabled(True) + self.form.remove.setEnabled(True) + else: + self.form.modify.setEnabled(False) + self.form.remove.setEnabled(False) + + def addCustomProperty(self, obj, dialog): + name = dialog.propertyName() + typ = dialog.propertyType() + grp = dialog.propertyGroup() + info = dialog.propertyInfo() + self.obj.Proxy.addCustomProperty(typ, name, grp, info) + if dialog.propertyIsEnumeration(): + setattr(self.obj, name, dialog.propertyEnumerations()) + return (name, info) + + def propertyAdd(self): + PathLog.track() + more = False + grp = None + typ = None + while True: + dialog = PropertyCreate(self.obj, grp, typ, more) + if dialog.exec_(None): + # if we block signals the view doesn't get updated, surprise, surprise + #self.model.blockSignals(True) + name, info = self.addCustomProperty(self.obj, dialog) + index = 0 + for i in range(self.model.rowCount()): + index = i + if self.model.item(i, self.ColumnName).data(QtCore.Qt.EditRole) > dialog.propertyName(): + break + self.model.insertRows(index, 1) + self._setupProperty(index, name) + self.form.table.selectionModel().setCurrentIndex(self.model.index(index, 0), QtCore.QItemSelectionModel.Rows) + #self.model.blockSignals(False) + more = dialog.createAnother() + else: + more = False + if not more: + break + + def propertyModifyIndex(self, index): + PathLog.track(index.row(), index.column()) + row = index.row() + + obj = self.model.item(row, self.ColumnVal).data(Delegate.RoleObject) + nam = self.model.item(row, self.ColumnVal).data(Delegate.RoleProperty) + grp = obj.getGroupOfProperty(nam) + typ = obj.getTypeIdOfProperty(nam) + + dialog = PropertyCreate(self.obj, grp, typ, False) + if dialog.exec_(nam): + val = getattr(obj, nam) + obj.removeProperty(nam) + name, info = self.addCustomProperty(self.obj, dialog) + try: + setattr(obj, nam, val) + except: + # this can happen if the old enumeration value doesn't exist anymore + pass + newVal = PathUtil.getPropertyValueString(obj, nam) + self.model.setData(self.model.index(row, self.ColumnVal), newVal, QtCore.Qt.DisplayRole) + + #self.model.setData(self.model.index(row, self.ColumnType), info, QtCore.Qt.ToolTipRole) + self.model.setData(self.model.index(row, self.ColumnVal), info, QtCore.Qt.ToolTipRole) + + def propertyModify(self): + PathLog.track() + rows = [] + for index in self.form.table.selectionModel().selectedIndexes(): + row = index.row() + if row in rows: + continue + rows.append(row) + + self.propertyModifyIndex(index) + + + def propertyRemove(self): + PathLog.track() + # first find all rows which need to be removed + rows = [] + for index in self.form.table.selectionModel().selectedIndexes(): + if not index.row() in rows: + rows.append(index.row()) + + # then remove them in reverse order so the indexes of the remaining rows + # to delete are still valid + for row in reversed(sorted(rows)): + self.obj.removeProperty(self.model.item(row).data(QtCore.Qt.EditRole)) + self.model.removeRow(row) + + +def Create(name = 'PropertyBag'): + '''Create(name = 'PropertyBag') ... creates a new setup sheet''' + FreeCAD.ActiveDocument.openTransaction(translate("PathPropertyBag", "Create PropertyBag")) + pcont = PathPropertyBag.Create(name) + PathIconViewProvider.Attach(pcont.ViewObject, name) + return pcont + +PathIconViewProvider.RegisterViewProvider('PropertyBag', ViewProvider) + +class PropertyBagCreateCommand(object): + '''Command to create a property container object''' + + def __init__(self): + pass + + def GetResources(self): + return {'MenuText': translate('PathPropertyBag', 'Property Bag'), + 'ToolTip': translate('PathPropertyBag', 'Creates an object which can be used to store reference properties.')} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + sel = FreeCADGui.Selection.getSelectionEx() + obj = Create() + body = None + if sel: + if 'PartDesign::Body' == sel[0].Object.TypeId: + body = sel[0].Object + elif hasattr(sel[0].Object, 'getParentGeoFeatureGroup'): + body = sel[0].Object.getParentGeoFeatureGroup() + if body: + obj.Label = 'Attributes' + group = body.Group + group.append(obj) + body.Group = group + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('Path_PropertyBag', PropertyBagCreateCommand()) + +FreeCAD.Console.PrintLog("Loading PathPropertyBagGui ... done\n") diff --git a/src/Mod/Path/PathScripts/PathPropertyEditor.py b/src/Mod/Path/PathScripts/PathPropertyEditor.py new file mode 100644 index 0000000000..f376e351fd --- /dev/null +++ b/src/Mod/Path/PathScripts/PathPropertyEditor.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** + +import FreeCAD +import PathScripts.PathLog as PathLog +import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype + +from PySide import QtCore, QtGui + +__title__ = "Path Property Editor" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Task panel editor for Properties" + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + + +class _PropertyEditor(object): + '''Base class of all property editors - just outlines the TableView delegate interface.''' + def __init__(self, obj, prop): + self.obj = obj + self.prop = prop + + def widget(self, parent): + '''widget(parent) ... called by the delegate to get a new editor widget. + Must be implemented by subclasses and return the widget.''' + pass # pylint: disable=unnecessary-pass + + def setEditorData(self, widget): + '''setEditorData(widget) ... called by the delegate to initialize the editor. + The widget is the object returned by widget(). + Must be implemented by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def setModelData(self, widget): + '''setModelData(widget) ... called by the delegate to store new values. + Must be implemented by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def propertyValue(self): + return self.obj.getPropertyByName(self.prop) + + def setProperty(self, value): + setattr(self.obj, self.prop, value) + + def displayString(self): + return self.propertyValue() + +class _PropertyEditorBool(_PropertyEditor): + '''Editor for boolean values - uses a combo box.''' + + def widget(self, parent): + return QtGui.QComboBox(parent) + + def setEditorData(self, widget): + widget.clear() + widget.addItems([str(False), str(True)]) + index = 1 if self.propertyValue() else 0 + widget.setCurrentIndex(index) + + def setModelData(self, widget): + self.setProperty(widget.currentText() == str(True)) + +class _PropertyEditorString(_PropertyEditor): + '''Editor for string values - uses a line edit.''' + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + text = '' if self.propertyValue() is None else self.propertyValue() + widget.setText(text) + + def setModelData(self, widget): + self.setProperty(widget.text()) + +class _PropertyEditorQuantity(_PropertyEditor): + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + quantity = self.propertyValue() + if quantity is None: + quantity = self.defaultQuantity() + widget.setText(quantity.getUserPreferred()[0]) + + def defaultQuantity(self): + pass + + def setModelData(self, widget): + self.setProperty(FreeCAD.Units.Quantity(widget.text())) + + def displayString(self): + if self.propertyValue() is None: + return '' + return self.propertyValue().getUserPreferred()[0] + +class _PropertyEditorAngle(_PropertyEditorQuantity): + '''Editor for angle values - uses a line edit''' + + def defaultQuantity(self): + return FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle) + +class _PropertyEditorLength(_PropertyEditorQuantity): + '''Editor for length values - uses a line edit.''' + + def defaultQuantity(self): + return FreeCAD.Units.Quantity(0, FreeCAD.Units.Length) + +class _PropertyEditorPercent(_PropertyEditor): + '''Editor for percent values - uses a spin box.''' + + def widget(self, parent): + return QtGui.QSpinBox(parent) + + def setEditorData(self, widget): + widget.setRange(0, 100) + value = self.propertyValue() + if value is None: + value = 0 + widget.setValue(value) + + def setModelData(self, widget): + self.setProperty(widget.value()) + +class _PropertyEditorInteger(_PropertyEditor): + '''Editor for integer values - uses a spin box.''' + + def widget(self, parent): + return QtGui.QSpinBox(parent) + + def setEditorData(self, widget): + value = self.propertyValue() + if value is None: + value = 0 + widget.setValue(value) + + def setModelData(self, widget): + self.setProperty(widget.value()) + +class _PropertyEditorFloat(_PropertyEditor): + '''Editor for float values - uses a double spin box.''' + + def widget(self, parent): + return QtGui.QDoubleSpinBox(parent) + + def setEditorData(self, widget): + value = self.propertyValue() + if value is None: + value = 0.0 + widget.setValue(value) + + def setModelData(self, widget): + self.setProperty(widget.value()) + +class _PropertyEditorFile(_PropertyEditor): + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + text = '' if self.propertyValue() is None else self.propertyValue() + widget.setText(text) + + def setModelData(self, widget): + self.setProperty(widget.text()) + +class _PropertyEditorEnumeration(_PropertyEditor): + + def widget(self, parent): + return QtGui.QComboBox(parent) + + def setEditorData(self, widget): + widget.clear() + widget.addItems(self.obj.getEnumerationsOfProperty(self.prop)) + widget.setCurrentText(self.propertyValue()) + + def setModelData(self, widget): + self.setProperty(widget.currentText()) + +_EditorFactory = { + 'App::PropertyAngle' : _PropertyEditorAngle, + 'App::PropertyBool' : _PropertyEditorBool, + 'App::PropertyDistance' : _PropertyEditorLength, + 'App::PropertyEnumeration' : _PropertyEditorEnumeration, + #'App::PropertyFile' : _PropertyEditorFile, + 'App::PropertyFloat' : _PropertyEditorFloat, + 'App::PropertyInteger' : _PropertyEditorInteger, + 'App::PropertyLength' : _PropertyEditorLength, + 'App::PropertyPercent' : _PropertyEditorPercent, + 'App::PropertyString' : _PropertyEditorString, + } + +def Types(): + '''Return the types of properties supported.''' + return [t for t in _EditorFactory] + +def Editor(obj, prop): + '''Returns an editor class to be used for the given property.''' + factory = _EditorFactory[obj.getTypeIdOfProperty(prop)] + if factory: + return factory(obj, prop) + return None diff --git a/src/Mod/Path/PathScripts/PathSetupSheetGui.py b/src/Mod/Path/PathScripts/PathSetupSheetGui.py index d2ceb3956b..1584b964a0 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetGui.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGui as PathGui import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathSimpleCopy.py b/src/Mod/Path/PathScripts/PathSimpleCopy.py index 2901166801..6c6cb5d5a6 100644 --- a/src/Mod/Path/PathScripts/PathSimpleCopy.py +++ b/src/Mod/Path/PathScripts/PathSimpleCopy.py @@ -67,8 +67,7 @@ class CommandPathSimpleCopy: FreeCADGui.addModule("PathScripts.PathUtils") FreeCADGui.addModule("PathScripts.PathCustom") - FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","' + selection[0].Name + '_SimpleCopy")') - FreeCADGui.doCommand('PathScripts.PathCustom.ObjectCustom(obj)') + FreeCADGui.doCommand('obj = PathScripts.PathCustom.Create("' + selection[0].Name + '_SimpleCopy")') FreeCADGui.doCommand('obj.ViewObject.Proxy = 0') FreeCADGui.doCommand('obj.Gcode = [c.toGCode() for c in srcpath.Commands]') FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py index 086db6b145..3a01944abc 100644 --- a/src/Mod/Path/PathScripts/PathSimulatorGui.py +++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py @@ -22,6 +22,7 @@ import FreeCAD import Path +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathSlotGui.py b/src/Mod/Path/PathScripts/PathSlotGui.py index 397ec14fdf..518209e562 100644 --- a/src/Mod/Path/PathScripts/PathSlotGui.py +++ b/src/Mod/Path/PathScripts/PathSlotGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathSlot as PathSlot import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py index 038a670c52..25a5cea8c7 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceGui.py +++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathSurface as PathSurface import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathThreadMillingGui.py b/src/Mod/Path/PathScripts/PathThreadMillingGui.py index fab21692a7..885f129a6b 100644 --- a/src/Mod/Path/PathScripts/PathThreadMillingGui.py +++ b/src/Mod/Path/PathScripts/PathThreadMillingGui.py @@ -23,6 +23,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui import PathScripts.PathThreadMilling as PathThreadMilling import PathScripts.PathGui as PathGui diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 5ffe7503dd..38e8a4ce4e 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -24,6 +24,7 @@ import FreeCAD import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences +import PathScripts.PathPropertyBag as PathPropertyBag import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype import PathScripts.PathUtil as PathUtil import PySide @@ -42,74 +43,73 @@ __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Class to deal with and represent a tool bit." -# PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -# PathLog.trackModule() +PropertyGroupShape = 'Shape' + +_DebugFindTool = False + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule() def translate(context, text, disambig=None): return PySide.QtCore.QCoreApplication.translate(context, text, disambig) +def _findToolFile(name, containerFile, typ): + PathLog.track(name) + if os.path.exists(name): # absolute reference + return name -ParameterTypeConstraint = { - 'Angle': 'App::PropertyAngle', - 'Distance': 'App::PropertyLength', - 'DistanceX': 'App::PropertyLength', - 'DistanceY': 'App::PropertyLength', - 'Radius': 'App::PropertyLength'} + if containerFile: + rootPath = os.path.dirname(os.path.dirname(containerFile)) + paths = [os.path.join(rootPath, typ)] + else: + paths = [] + paths.extend(PathPreferences.searchPathsTool(typ)) + + def _findFile(path, name): + PathLog.track(path, name) + fullPath = os.path.join(path, name) + if os.path.exists(fullPath): + return (True, fullPath) + for root, ds, fs in os.walk(path): + for d in ds: + found, fullPath = _findFile(d, name) + if found: + return (True, fullPath) + return (False, None) -def _findTool(path, typ, dbg=False): - if os.path.exists(path): # absolute reference - if dbg: - PathLog.debug("Found {} at {}".format(typ, path)) - return path - - def searchFor(pname, fname): - # PathLog.debug("pname: {} fname: {}".format(pname, fname)) - if dbg: - PathLog.debug("Looking for {}".format(pname)) - if fname: - for p in PathPreferences.searchPathsTool(typ): - PathLog.track(p) - f = os.path.join(p, fname) - if dbg: - PathLog.debug(" Checking {}".format(f)) - if os.path.exists(f): - if dbg: - PathLog.debug(" Found {} at {}".format(typ, f)) - return f - if pname and os.path.sep != pname: - PathLog.track(pname) - ppname, pfname = os.path.split(pname) - ffname = os.path.join(pfname, fname) if fname else pfname - return searchFor(ppname, ffname) - return None - - return searchFor(path, '') + for p in paths: + found, path = _findFile(p, name) + if found: + return path + return None -def findShape(path): - ''' - findShape(path) ... search for path, full and partially - in all known shape directories. - ''' - return _findTool(path, 'Shape') +def findToolShape(name, path=None): + '''findToolShape(name, path) ... search for name, if relative path look in path''' + PathLog.track(name, path) + return _findToolFile(name, path, 'Shape') -def findBit(path): - PathLog.track(path) - if path.endswith('.fctb'): - return _findTool(path, 'Bit') - return _findTool("{}.fctb".format(path), 'Bit') +def findToolBit(name, path=None): + '''findToolBit(name, path) ... search for name, if relative path look in path''' + PathLog.track(name, path) + if name.endswith('.fctb'): + return _findToolFile(name, path, 'Bit') + return _findToolFile("{}.fctb".format(name), path, 'Bit') -def findLibrary(path, dbg=False): - if path.endswith('.fctl'): - return _findTool(path, 'Library', dbg) - return _findTool("{}.fctl".format(path), 'Library', dbg) +def findToolLibrary(name, path=None): + '''findToolLibrary(name, path) ... search for name, if relative path look in path''' + PathLog.track(name, path) + if name.endswith('.fctl'): + return _findToolFile(name, path, 'Library') + return _findToolFile("{}.fctl".format(name), path, 'Library') def _findRelativePath(path, typ): + PathLog.track(path, typ) relative = path for p in PathPreferences.searchPathsTool(typ): if path.startswith(p): @@ -132,48 +132,19 @@ def findRelativePathTool(path): def findRelativePathLibrary(path): return _findRelativePath(path, 'Library') - -def updateConstraint(sketch, name, value): - for i, constraint in enumerate(sketch.Constraints): - if constraint.Name.split(';')[0] == name: - constr = None - if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius', 'Angle']: - constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value) - else: - print(constraint.Name, constraint.Type) - - if constr is not None: - if not PathGeom.isRoughly(constraint.Value, value.Value): - PathLog.track(name, constraint.Type, - 'update', i, "(%.2f -> %.2f)" - % (constraint.Value, value.Value)) - sketch.delConstraint(i) - sketch.recompute() - n = sketch.addConstraint(constr) - sketch.renameConstraint(n, constraint.Name) - else: - PathLog.track(name, constraint.Type, 'unchanged') - break - - -PropertyGroupBit = 'Bit' -PropertyGroupAttribute = 'Attribute' - - class ToolBit(object): - def __init__(self, obj, shapeFile): - PathLog.track(obj.Label, shapeFile) + def __init__(self, obj, shapeFile, path=None): + PathLog.track(obj.Label, shapeFile, path) self.obj = obj - obj.addProperty('App::PropertyFile', 'BitShape', 'Base', - translate('PathToolBit', 'Shape for bit shape')) - obj.addProperty('App::PropertyLink', 'BitBody', 'Base', - translate('PathToolBit', - 'The parametrized body representing the tool bit')) - obj.addProperty('App::PropertyFile', 'File', 'Base', - translate('PathToolBit', 'The file of the tool')) - obj.addProperty('App::PropertyString', 'ShapeName', 'Base', - translate('PathToolBit', 'The name of the shape file')) + obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) + obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) + obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) + obj.addProperty('App::PropertyString', 'ShapeName', 'Base', translate('PathToolBit', 'The name of the shape file')) + obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit')) + + if path: + obj.File = path if shapeFile is None: obj.BitShape = 'endmill.fcstd' self._setupBitShape(obj) @@ -193,12 +164,6 @@ class ToolBit(object): break return None - def propertyNamesBit(self, obj): - return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit] - - def propertyNamesAttribute(self, obj): - return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute] - def onDocumentRestored(self, obj): # when files are shared it is essential to be able to change/set the shape file, # otherwise the file is hard to use @@ -206,19 +171,39 @@ class ToolBit(object): obj.setEditorMode('BitBody', 2) obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) + if not hasattr(obj, 'BitPropertyNames'): + obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit')) + propNames = [] + for prop in obj.PropertiesList: + if obj.getGroupOfProperty(prop) == 'Bit': + val = obj.getPropertyByName(prop) + typ = obj.getTypeIdOfProperty(prop) + dsc = obj.getDocumentationOfProperty(prop) - for prop in self.propertyNamesBit(obj): - obj.setEditorMode(prop, 1) - # I currently don't see why these need to be read-only - # for prop in self.propertyNamesAttribute(obj): - # obj.setEditorMode(prop, 1) + obj.removeProperty(prop) + obj.addProperty(typ, prop, PropertyGroupShape, dsc) + + PathUtil.setProperty(obj, prop, val) + propNames.append(prop) + elif obj.getGroupOfProperty(prop) == 'Attribute': + propNames.append(prop) + obj.BitPropertyNames = propNames + obj.setEditorMode('BitPropertyNames', 2) + + for prop in obj.BitPropertyNames: + if obj.getGroupOfProperty(prop) == PropertyGroupShape: + # properties in the Shape group can only be modified while the actual + # shape is loaded, so we have to disable direct property editing + obj.setEditorMode(prop, 1) + else: + # all other custom properties can and should be edited directly in the + # property editor widget, not much value in re-implementing that + obj.setEditorMode(prop, 0) def onChanged(self, obj, prop): PathLog.track(obj.Label, prop) if prop == 'BitShape' and 'Restore' not in obj.State: self._setupBitShape(obj) - # elif obj.getGroupOfProperty(prop) == PropertyGroupBit: - # self._updateBitShape(obj, [prop]) def onDelete(self, obj, arg2=None): PathLog.track(obj.Label) @@ -227,12 +212,19 @@ class ToolBit(object): def _updateBitShape(self, obj, properties=None): if obj.BitBody is not None: - if not properties: - properties = self.propertyNamesBit(obj) - for prop in properties: - for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: - PathLog.track(obj.Label, sketch.Label, prop) - updateConstraint(sketch, prop, obj.getPropertyByName(prop)) + for attributes in [o for o in obj.BitBody.Group if hasattr(o, 'Proxy') and hasattr(o.Proxy, 'getCustomProperties')]: + for prop in attributes.Proxy.getCustomProperties(): + # the property might not exist in our local object (new attribute in shape) + # for such attributes we just keep the default + if hasattr(obj, prop): + setattr(attributes, prop, obj.getPropertyByName(prop)) + else: + # if the template shape has a new attribute defined we should add that + # to the local object + self._setupProperty(obj, prop, attributes) + propNames = obj.BitPropertyNames + propNames.append(prop) + obj.BitPropertyNames = propNames self._copyBitShape(obj) def _copyBitShape(self, obj): @@ -243,6 +235,7 @@ class ToolBit(object): obj.Shape = Part.Shape() def _loadBitBody(self, obj, path=None): + PathLog.track(obj.Label, path) p = path if path else obj.BitShape docOpened = False doc = None @@ -251,12 +244,15 @@ class ToolBit(object): doc = FreeCAD.getDocument(d) break if doc is None: - p = findShape(p) + p = findToolShape(p, path if path else obj.File) if not path and p != obj.BitShape: obj.BitShape = p + PathLog.debug("ToolBit {} using shape file: {}".format(obj.Label, p)) doc = FreeCAD.openDocument(p, True) obj.ShapeName = doc.Name docOpened = True + else: + PathLog.debug("ToolBit {} already open: {}".format(obj.Label, doc)) return (doc, docOpened) def _removeBitBody(self, obj): @@ -269,7 +265,7 @@ class ToolBit(object): PathLog.track(obj.Label) self._removeBitBody(obj) self._copyBitShape(obj) - for prop in self.propertyNamesBit(obj): + for prop in obj.BitPropertyNames: obj.removeProperty(prop) def loadBitBody(self, obj, force=False): @@ -287,6 +283,20 @@ class ToolBit(object): def unloadBitBody(self, obj): self._removeBitBody(obj) + def _setupProperty(self, obj, prop, orig): + # extract property parameters and values so it can be copied + val = orig.getPropertyByName(prop) + typ = orig.getTypeIdOfProperty(prop) + grp = orig.getGroupOfProperty(prop) + dsc = orig.getDocumentationOfProperty(prop) + + obj.addProperty(typ, prop, grp, dsc) + if 'App::PropertyEnumeration' == typ: + setattr(obj, prop, orig.getEnumerationsOfProperty(prop)) + + obj.setEditorMode(prop, 1) + PathUtil.setProperty(obj, prop, val) + def _setupBitShape(self, obj, path=None): PathLog.track(obj.Label) @@ -296,6 +306,8 @@ class ToolBit(object): obj.Label = doc.RootObjects[0].Label self._deleteBitSetup(obj) bitBody = obj.Document.copyObject(doc.RootObjects[0], True) + + docName = doc.Name if docOpened: FreeCAD.setActiveDocument(activeDoc.Name) FreeCAD.closeDocument(doc.Name) @@ -303,29 +315,44 @@ class ToolBit(object): if bitBody.ViewObject: bitBody.ViewObject.Visibility = False - for sketch in [o for o in bitBody.Group if o.TypeId == 'Sketcher::SketchObject']: - for constraint in [c for c in sketch.Constraints if c.Name != '']: - typ = ParameterTypeConstraint.get(constraint.Type) - PathLog.track(constraint, typ) - if typ is not None: - parts = [p.strip() for p in constraint.Name.split(';')] - prop = parts[0] - desc = '' - if len(parts) > 1: - desc = parts[1] - obj.addProperty(typ, prop, PropertyGroupBit, desc) - obj.setEditorMode(prop, 1) - value = constraint.Value - if constraint.Type == 'Angle': - value = value * 180 / math.pi - PathUtil.setProperty(obj, prop, value) + PathLog.debug("bitBody.{} ({}): {}".format(bitBody.Label, bitBody.Name, type(bitBody))) + + propNames = [] + for attributes in [o for o in bitBody.Group if PathPropertyBag.IsPropertyBag(o)]: + PathLog.debug("Process properties from {}".format(attributes.Label)) + for prop in attributes.Proxy.getCustomProperties(): + self._setupProperty(obj, prop, attributes) + propNames.append(prop) + if not propNames: + PathLog.error(translate('PathToolBit', 'Did not find a PropertyBag in {} - not a ToolBit shape?').format(docName)) + # has to happen last because it could trigger op.execute evaluations + obj.BitPropertyNames = propNames obj.BitBody = bitBody self._copyBitShape(obj) + def toolShapeProperties(self, obj): + '''toolShapeProperties(obj) ... return all properties defining it's shape''' + return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) == PropertyGroupShape]) + + def toolAdditionalProperties(self, obj): + '''toolShapeProperties(obj) ... return all properties unrelated to it's shape''' + return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) != PropertyGroupShape]) + + def toolGroupsAndProperties(self, obj, includeShape=True): + '''toolGroupsAndProperties(obj) ... returns a dictionary of group names with a list of property names.''' + category = {} + for prop in obj.BitPropertyNames: + group = obj.getGroupOfProperty(prop) + if includeShape or group != PropertyGroupShape: + properties = category.get(group, []) + properties.append(prop) + category[group] = properties + return category + def getBitThumbnail(self, obj): if obj.BitShape: - path = findShape(obj.BitShape) + path = findToolShape(obj.BitShape) if path: with open(path, 'rb') as fd: try: @@ -347,8 +374,7 @@ class ToolBit(object): obj.File = path return True except (OSError, IOError) as e: - PathLog.error("Could not save tool {} to {} ({})".format( - obj.Label, path, e)) + PathLog.error("Could not save tool {} to {} ({})".format(obj.Label, path, e)) raise def templateAttrs(self, obj): @@ -360,16 +386,10 @@ class ToolBit(object): else: attrs['shape'] = findRelativePathShape(obj.BitShape) params = {} - for name in self.propertyNamesBit(obj): + for name in obj.BitPropertyNames: params[name] = PathUtil.getPropertyValueString(obj, name) attrs['parameter'] = params params = {} - for name in self.propertyNamesAttribute(obj): - if name == "UserAttributes": - for key, value in obj.UserAttributes.items(): - params[key] = value - else: - params[name] = PathUtil.getPropertyValueString(obj, name) attrs['attribute'] = params return attrs @@ -380,73 +400,33 @@ def Declaration(path): return json.load(fp) -class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype): - - def __init__(self): - PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute') - self.addProperty('App::PropertyEnumeration', 'Material', - PropertyGroupAttribute, - translate('PathToolBit', 'Tool bit material')) - self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond', - 'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon'] - self.addProperty('App::PropertyDistance', 'LengthOffset', - PropertyGroupAttribute, translate('PathToolBit', - 'Length offset in Z direction')) - self.addProperty('App::PropertyInteger', 'Flutes', - PropertyGroupAttribute, translate('PathToolBit', - 'The number of flutes')) - self.addProperty('App::PropertyDistance', 'ChipLoad', - PropertyGroupAttribute, translate('PathToolBit', - 'Chipload as per manufacturer')) - self.addProperty('App::PropertyMap', 'UserAttributes', - PropertyGroupAttribute, translate('PathToolBit', - 'User Defined Values')) - self.addProperty('App::PropertyBool', 'SpindlePower', - PropertyGroupAttribute, translate('PathToolBit', - 'Whether Spindle Power should be allowed')) - - class ToolBitFactory(object): - def CreateFromAttrs(self, attrs, name='ToolBit'): - PathLog.debug(attrs) - obj = Factory.Create(name, attrs['shape']) + def CreateFromAttrs(self, attrs, name='ToolBit', path=None): + PathLog.track(attrs, path) + obj = Factory.Create(name, attrs['shape'], path) obj.Label = attrs['name'] params = attrs['parameter'] for prop in params: PathUtil.setProperty(obj, prop, params[prop]) obj.Proxy._updateBitShape(obj) obj.Proxy.unloadBitBody(obj) - params = attrs['attribute'] - proto = AttributePrototype() - uservals = {} - for pname in params: - try: - prop = proto.getProperty(pname) - prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname])) - except Exception: - prop = proto.getProperty("UserAttributes") - uservals.update({pname: params[pname]}) - - if len(uservals.items()) > 0: - prop.setupProperty(obj, "UserAttributes", - PropertyGroupAttribute, uservals) - return obj def CreateFrom(self, path, name='ToolBit'): + PathLog.track(name, path) try: data = Declaration(path) - bit = Factory.CreateFromAttrs(data, name) - bit.File = path + bit = Factory.CreateFromAttrs(data, name, path) return bit except (OSError, IOError) as e: PathLog.error("%s not a valid tool file (%s)" % (path, e)) raise - def Create(self, name='ToolBit', shapeFile=None): + def Create(self, name='ToolBit', shapeFile=None, path=None): + PathLog.track(name, shapeFile, path) obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) - obj.Proxy = ToolBit(obj, shapeFile) + obj.Proxy = ToolBit(obj, shapeFile, path) return obj diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 3488bcc95d..3028bd7bb1 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -20,12 +20,14 @@ # * * # *************************************************************************** +import FreeCAD import FreeCADGui import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences -import PathScripts.PathSetupSheetGui as PathSetupSheetGui +import PathScripts.PathPropertyEditor as PathPropertyEditor import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathUtil as PathUtil import os import re @@ -39,6 +41,31 @@ PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) +class _Delegate(QtGui.QStyledItemDelegate): + '''Handles the creation of an appropriate editing widget for a given property.''' + ObjectRole = QtCore.Qt.UserRole + 1 + PropertyRole = QtCore.Qt.UserRole + 2 + EditorRole = QtCore.Qt.UserRole + 3 + + def createEditor(self, parent, option, index): + editor = index.data(self.EditorRole) + if editor is None: + obj = index.data(self.ObjectRole) + prp = index.data(self.PropertyRole) + editor = PathPropertyEditor.Editor(obj, prp) + index.model().setData(index, editor, self.EditorRole) + return editor.widget(parent) + + def setEditorData(self, widget, index): + # called to update the widget with the current data + index.data(self.EditorRole).setEditorData(widget) + + def setModelData(self, widget, model, index): + # called to update the model with the data from the widget + editor = index.data(self.EditorRole) + editor.setModelData(widget) + index.model().setData(index, PathUtil.getPropertyValueString(editor.obj, editor.prop), QtCore.Qt.DisplayRole) + class ToolBitEditor(object): '''UI and controller for editing a ToolBit. @@ -84,32 +111,35 @@ class ToolBitEditor(object): layout = self.form.bitParams.layout() ui = FreeCADGui.UiLoader() - nr = 0 # for all properties either assign them to existing labels and editors # or create additional ones for them if not enough have already been # created. - for name in tool.PropertiesList: - if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit: - if nr < len(self.widgets): - PathLog.debug("re-use row: {} [{}]".format(nr, name)) - label, qsb, editor = self.widgets[nr] - label.setText(labelText(name)) - editor.attachTo(tool, name) - label.show() - qsb.show() - else: - qsb = ui.createWidget('Gui::QuantitySpinBox') - editor = PathGui.QuantitySpinBox(qsb, tool, name) - label = QtGui.QLabel(labelText(name)) - self.widgets.append((label, qsb, editor)) - PathLog.debug("create row: {} [{}]".format(nr, name)) - if nr >= layout.rowCount(): - layout.addRow(label, qsb) - nr = nr + 1 + usedRows = 0 + for nr, name in enumerate(tool.Proxy.toolShapeProperties(tool)): + if nr < len(self.widgets): + PathLog.debug("re-use row: {} [{}]".format(nr, name)) + label, qsb, editor = self.widgets[nr] + label.setText(labelText(name)) + editor.attachTo(tool, name) + label.show() + qsb.show() + else: + qsb = ui.createWidget('Gui::QuantitySpinBox') + editor = PathGui.QuantitySpinBox(qsb, tool, name) + label = QtGui.QLabel(labelText(name)) + self.widgets.append((label, qsb, editor)) + PathLog.debug("create row: {} [{}] {}".format(nr, name, type(qsb))) + if hasattr(qsb, 'editingFinished'): + qsb.editingFinished.connect(self.updateTool) + + if nr >= layout.rowCount(): + layout.addRow(label, qsb) + usedRows = usedRows + 1 # hide all rows which aren't being used - for i in range(nr, len(self.widgets)): + PathLog.track(usedRows, len(self.widgets)) + for i in range(usedRows, len(self.widgets)): label, qsb, editor = self.widgets[i] label.hide() qsb.hide() @@ -124,98 +154,48 @@ class ToolBitEditor(object): def setupAttributes(self, tool): PathLog.track() - self.proto = PathToolBit.AttributePrototype() - self.props = sorted(self.proto.properties) - self.delegate = PathSetupSheetGui.Delegate(self.form) - self.model = QtGui.QStandardItemModel(len(self.props)-1, 3, self.form) - self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value']) - for i, name in enumerate(self.props): - PathLog.debug("propname: %s " % name) + setup = True + if not hasattr(self, 'delegate'): + self.delegate = _Delegate(self.form.attrTree) + self.model = QtGui.QStandardItemModel(self.form.attrTree) + self.model.setHorizontalHeaderLabels(['Property', 'Value']) + else: + self.model.removeRows(0, self.model.rowCount()) + setup = False - prop = self.proto.getProperty(name) - isset = hasattr(tool, name) + attributes = tool.Proxy.toolGroupsAndProperties(tool, False) + for name in attributes: + group = QtGui.QStandardItem() + group.setData(name, QtCore.Qt.EditRole) + group.setEditable(False) + for prop in attributes[name]: + label = QtGui.QStandardItem() + label.setData(prop, QtCore.Qt.EditRole) + label.setEditable(False) - if isset: - prop.setValue(getattr(tool, name)) + value = QtGui.QStandardItem() + value.setData(PathUtil.getPropertyValueString(tool, prop), QtCore.Qt.DisplayRole) + value.setData(tool, _Delegate.ObjectRole) + value.setData(prop, _Delegate.PropertyRole) - if name == "UserAttributes": - continue + group.appendRow([label, value]) + self.model.appendRow(group) - else: - self.model.setData(self.model.index(i, 0), isset, - QtCore.Qt.EditRole) - self.model.setData(self.model.index(i, 1), name, - QtCore.Qt.EditRole) - self.model.setData(self.model.index(i, 2), prop, - PathSetupSheetGui.Delegate.PropertyRole) - self.model.setData(self.model.index(i, 2), - prop.displayString(), - QtCore.Qt.DisplayRole) - - self.model.item(i, 0).setCheckable(True) - self.model.item(i, 0).setText('') - self.model.item(i, 1).setEditable(False) - self.model.item(i, 1).setToolTip(prop.info) - self.model.item(i, 2).setToolTip(prop.info) - - if isset: - self.model.item(i, 0).setCheckState(QtCore.Qt.Checked) - else: - self.model.item(i, 0).setCheckState(QtCore.Qt.Unchecked) - self.model.item(i, 1).setEnabled(False) - self.model.item(i, 2).setEnabled(False) - - if hasattr(tool, "UserAttributes"): - for key, value in tool.UserAttributes.items(): - PathLog.debug(key, value) - c1 = QtGui.QStandardItem() - c1.setCheckable(False) - c1.setEditable(False) - c1.setCheckState(QtCore.Qt.CheckState.Checked) - - c1.setText('') - c2 = QtGui.QStandardItem(key) - c2.setEditable(False) - c3 = QtGui.QStandardItem(value) - c3.setEditable(False) - - self.model.appendRow([c1, c2, c3]) - - self.form.attrTable.setModel(self.model) - self.form.attrTable.setItemDelegateForColumn(2, self.delegate) - self.form.attrTable.resizeColumnsToContents() - self.form.attrTable.verticalHeader().hide() - - self.model.dataChanged.connect(self.updateData) - - def updateData(self, topLeft, bottomRight): - PathLog.track() - if 0 == topLeft.column(): - isset = self.model.item(topLeft.row(), - 0).checkState() == QtCore.Qt.Checked - self.model.item(topLeft.row(), 1).setEnabled(isset) - self.model.item(topLeft.row(), 2).setEnabled(isset) + if setup: + self.form.attrTree.setModel(self.model) + self.form.attrTree.setItemDelegateForColumn(1, self.delegate) + self.form.attrTree.expandAll() + self.form.attrTree.resizeColumnToContents(0) + self.form.attrTree.resizeColumnToContents(1) + #self.form.attrTree.collapseAll() def accept(self): PathLog.track() self.refresh() self.tool.Proxy.unloadBitBody(self.tool) - # get the attributes - for i, name in enumerate(self.props): - PathLog.debug('in accept: {}'.format(name)) - prop = self.proto.getProperty(name) - if self.model.item(i, 0) is not None: - enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked - if enabled and not prop.getValue() is None: - prop.setupProperty(self.tool, name, - PathToolBit.PropertyGroupAttribute, - prop.getValue()) - elif hasattr(self.tool, name): - self.tool.removeProperty(name) - def reject(self): PathLog.track() self.tool.Proxy.unloadBitBody(self.tool) @@ -228,27 +208,45 @@ class ToolBitEditor(object): for lbl, qsb, editor in self.widgets: editor.updateSpinBox() + def _updateBitShape(self, shapePath): + # Only need to go through this exercise if the shape actually changed. + if self.tool.BitShape != shapePath: + # Before setting a new bitshape we need to make sure that none of + # editors fires an event and tries to access its old property, which + # might not exist anymore. + for lbl, qsb, editor in self.widgets: + editor.attachTo(self.tool, 'File') + self.tool.BitShape = shapePath + self.setupTool(self.tool) + self.form.toolName.setText(self.tool.Label) + if self.tool.BitBody and self.tool.BitBody.ViewObject: + if not self.tool.BitBody.ViewObject.Visibility: + self.tool.BitBody.ViewObject.Visibility = True + self.setupAttributes(self.tool) + return True + return False + def updateShape(self): PathLog.track() shapePath = str(self.form.shapePath.text()) # Only need to go through this exercise if the shape actually changed. - if self.tool.BitShape != shapePath: - self.tool.BitShape = shapePath - self.setupTool(self.tool) - self.form.toolName.setText(self.tool.Label) - + if self._updateBitShape(shapePath): for lbl, qsb, editor in self.widgets: editor.updateSpinBox() def updateTool(self): PathLog.track() - self.tool.Label = str(self.form.toolName.text()) - self.tool.BitShape = str(self.form.shapePath.text()) + + label = str(self.form.toolName.text()) + shape = str(self.form.shapePath.text()) + if self.tool.Label != label: + self.tool.Label = label + self._updateBitShape(shape) for lbl, qsb, editor in self.widgets: editor.updateProperty() - # self.tool.Proxy._updateBitShape(self.tool) + self.tool.Proxy._updateBitShape(self.tool) def refresh(self): PathLog.track() @@ -262,10 +260,7 @@ class ToolBitEditor(object): path = self.tool.BitShape if not path: path = PathPreferences.lastPathToolShape() - foo = QtGui.QFileDialog.getOpenFileName(self.form, - "Path - Tool Shape", - path, - "*.fcstd") + foo = QtGui.QFileDialog.getOpenFileName(self.form, "Path - Tool Shape", path, "*.fcstd") if foo and foo[0]: PathPreferences.setLastPathToolShape(os.path.dirname(foo[0])) self.form.shapePath.setText(foo[0]) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 1fe1df5370..1bf56aea68 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -170,13 +170,13 @@ class TaskPanel: class ToolBitGuiFactory(PathToolBit.ToolBitFactory): - def Create(self, name='ToolBit', shapeFile=None): + def Create(self, name='ToolBit', shapeFile=None, path=None): '''Create(name = 'ToolBit') ... creates a new tool bit. It is assumed the tool will be edited immediately so the internal bit body is still attached.''' - FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', - 'Create ToolBit')) - tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile) + PathLog.track(name, shapeFile, path) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) + tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile, path) PathIconViewProvider.Attach(tool.ViewObject, name) FreeCAD.ActiveDocument.commitTransaction() return tool @@ -247,7 +247,8 @@ def GetToolShapeFile(parent=None): location, '*.fcstd') if fname and fname[0]: if fname != location: - PathPreferences.setLastPathToolShape(location) + newloc = os.path.dirname(fname[0]) + PathPreferences.setLastPathToolShape(newloc) return fname[0] else: return None diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 33e8fb8e39..0abc2a6ae4 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -24,23 +24,25 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit -import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolBitEdit as PathToolBitEdit +import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolControllerGui as PathToolControllerGui import PathScripts.PathUtilsGui as PathUtilsGui -from PySide import QtCore, QtGui import PySide +import glob import json import os -import glob -import uuid as UUID -from functools import partial import shutil +import uuid as UUID -# PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +from functools import partial + + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # PathLog.trackModule(PathLog.thisModule()) _UuidRole = PySide.QtCore.Qt.UserRole + 1 @@ -50,6 +52,75 @@ _PathRole = PySide.QtCore.Qt.UserRole + 2 def translate(context, text, disambig=None): return PySide.QtCore.QCoreApplication.translate(context, text, disambig) +def checkWorkingDir(): + # users shouldn't use the example toolbits and libraries. + # working directory should be writable + PathLog.track() + + workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary()) + defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath()) + + PathLog.debug('workingdir: {} defaultdir: {}'.format(workingdir, defaultdir)) + + dirOK = lambda : workingdir != defaultdir and (os.access(workingdir, os.W_OK)) + + if dirOK(): + return True + + qm = PySide.QtGui.QMessageBox + ret = qm.question(None,'', "Toolbit working directory not set up. Do that now?", qm.Yes | qm.No) + + if ret == qm.No: + return False + + msg = translate("Path", "Choose a writable location for your toolbits", None) + while not dirOK(): + workingdir = PySide.QtGui.QFileDialog.getExistingDirectory(None, msg, + PathPreferences.filePath()) + + if workingdir[-8:] == os.path.sep + 'Library': + workingdir = workingdir[:-8] # trim off trailing /Library if user chose it + + PathPreferences.setLastPathToolLibrary("{}{}Library".format(workingdir, os.path.sep)) + PathPreferences.setLastPathToolBit("{}{}Bit".format(workingdir, os.path.sep)) + PathLog.debug('setting workingdir to: {}'.format(workingdir)) + + subdirlist = ['Bit', 'Library', 'Shape'] + mode = 0o777 + for dir in subdirlist.copy(): + subdir = "{}{}{}".format(workingdir, os.path.sep, dir) + if os.path.exists(subdir): + subdirlist.remove(dir) + + if len(subdirlist) >= 1: + needed = ', '.join([str(d) for d in subdirlist]) + qm = PySide.QtGui.QMessageBox + ret = qm.question(None,'', "Toolbit Working directory {} needs these sudirectories:\n {} \n Create them?".format(workingdir, needed), qm.Yes | qm.No) + + if ret == qm.No: + return False + else: + for dir in subdirlist: + subdir = "{}{}{}".format(workingdir, os.path.sep, dir) + os.mkdir(subdir, mode) + if dir != 'Shape': + qm = PySide.QtGui.QMessageBox + ret = qm.question(None,'', "Copy example files to new {} directory?".format(dir), qm.Yes | qm.No) + if ret == qm.Yes: + src="{}{}{}".format(defaultdir, os.path.sep, dir) + src_files = os.listdir(src) + for file_name in src_files: + full_file_name = os.path.join(src, file_name) + if os.path.isfile(full_file_name): + shutil.copy(full_file_name, subdir) + + + # if no library is set, choose the first one in the Library directory + if PathPreferences.lastFileToolLibrary() is None: + libFiles = [f for f in glob.glob(PathPreferences.lastPathToolLibrary() + os.path.sep + '*.fctl')] + PathPreferences.setLastFileToolLibrary(libFiles[0]) + + return True class _TableView(PySide.QtGui.QTableView): '''Subclass of QTableView to support rearrange and copying of ToolBits''' @@ -141,7 +212,7 @@ class ModelFactory(object): for toolBit in library['tools']: try: nr = toolBit['nr'] - bit = PathToolBit.findBit(toolBit['path']) + bit = PathToolBit.findToolBit(toolBit['path'], path) if bit: PathLog.track(bit) tool = PathToolBit.Declaration(bit) @@ -203,15 +274,15 @@ class ModelFactory(object): path = PathPreferences.lastPathToolLibrary() if os.path.isdir(path): # opening all tables in a directory - libFiles = [f for f in glob.glob(path + '/*.fctl')] + libFiles = [f for f in glob.glob(path + os.path.sep + '*.fctl')] libFiles.sort() for libFile in libFiles: loc, fnlong = os.path.split(libFile) fn, ext = os.path.splitext(fnlong) - libItem = QtGui.QStandardItem(fn) + libItem = PySide.QtGui.QStandardItem(fn) libItem.setToolTip(loc) libItem.setData(libFile, _PathRole) - libItem.setIcon(QtGui.QPixmap(':/icons/Path_ToolTable.svg')) + libItem.setIcon(PySide.QtGui.QPixmap(':/icons/Path_ToolTable.svg')) model.appendRow(libItem) PathLog.debug('model rows: {}'.format(model.rowCount())) @@ -241,6 +312,7 @@ class ToolBitSelector(object): '''Controller for displaying a library and creating ToolControllers''' def __init__(self): + checkWorkingDir() self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui') self.factory = ModelFactory() self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames())) @@ -250,20 +322,20 @@ class ToolBitSelector(object): def columnNames(self): return ['#', 'Tool'] - def curLib(self): + def currentLibrary(self, shortNameOnly): libfile = PathPreferences.lastFileToolLibrary() if libfile is None or libfile == "": return "" - else: - libfile = os.path.split(PathPreferences.lastFileToolLibrary())[1] - libfile = os.path.splitext(libfile)[0] + elif shortNameOnly: + return os.path.splitext(os.path.basename(libfile))[0] return libfile def loadData(self): PathLog.track() self.toolModel.clear() self.toolModel.setHorizontalHeaderLabels(self.columnNames()) - self.form.lblLibrary.setText(self.curLib()) + self.form.lblLibrary.setText(self.currentLibrary(True)) + self.form.lblLibrary.setToolTip(self.currentLibrary(False)) self.factory.libraryOpen(self.toolModel) self.toolModel.takeColumn(3) self.toolModel.takeColumn(2) @@ -271,7 +343,6 @@ class ToolBitSelector(object): def setupUI(self): PathLog.track() self.loadData() - self.form.tools.setModel(self.toolModel) self.form.tools.selectionModel().selectionChanged.connect(self.enableButtons) self.form.tools.doubleClicked.connect(partial(self.selectedOrAllToolControllers)) @@ -340,7 +411,7 @@ class ToolBitSelector(object): def open(self, path=None): ''' load library stored in path and bring up ui''' - docs = FreeCADGui.getMainWindow().findChildren(QtGui.QDockWidget) + docs = FreeCADGui.getMainWindow().findChildren(PySide.QtGui.QDockWidget) for doc in docs: if doc.objectName() == "ToolSelector": if doc.isVisible(): @@ -351,7 +422,7 @@ class ToolBitSelector(object): return mw = FreeCADGui.getMainWindow() - mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.form, + mw.addDockWidget(PySide.QtCore.Qt.RightDockWidgetArea, self.form, PySide.QtCore.Qt.Orientation.Vertical) @@ -361,9 +432,7 @@ class ToolBitLibrary(object): def __init__(self): PathLog.track() - if not self.checkWorkingDir(): - return - + checkWorkingDir() self.factory = ModelFactory() self.temptool = None self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames())) @@ -375,55 +444,6 @@ class ToolBitLibrary(object): self.setupUI() self.title = self.form.windowTitle() - def checkWorkingDir(self): - # users shouldn't use the example toolbits and libraries. - # working directory should be writable - PathLog.track() - - workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary()) - defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath()) - - dirOK = lambda : workingdir != defaultdir and (os.access(workingdir, os.W_OK)) - - if dirOK(): - return True - - qm = PySide.QtGui.QMessageBox - ret = qm.question(None,'', "Toolbit working directory not set up. Do that now?", qm.Yes | qm.No) - - if ret == qm.No: - return False - - msg = translate("Path", "Choose a writable location for your toolbits", None) - while not dirOK(): - workingdir = PySide.QtGui.QFileDialog.getExistingDirectory(None, msg, - PathPreferences.filePath()) - - PathPreferences.setLastPathToolLibrary("{}/Library".format(workingdir)) - - subdirlist = ['Bit', 'Library', 'Shape'] - mode = 0o777 - for dir in subdirlist: - subdir = "{}/{}".format(workingdir, dir) - if not os.path.exists(subdir): - qm = PySide.QtGui.QMessageBox - ret = qm.question(None,'', "Toolbit Working directory {} should contain a '{}' subdirectory. Create it?".format(workingdir, dir), qm.Yes | qm.No) - - if ret == qm.Yes: - os.mkdir(subdir, mode) - qm = PySide.QtGui.QMessageBox - ret = qm.question(None,'', "Copy example files to new {} directory?".format(dir), qm.Yes | qm.No) - if ret == qm.Yes: - src="{}/{}".format(defaultdir, dir) - src_files = os.listdir(src) - for file_name in src_files: - full_file_name = os.path.join(src, file_name) - if os.path.isfile(full_file_name): - shutil.copy(full_file_name, subdir) - - return True - - def toolBitNew(self): PathLog.track() @@ -439,7 +459,7 @@ class ToolBitLibrary(object): # Parse out the name of the file and write the structure loc, fil = os.path.split(filename) fname = os.path.splitext(fil)[0] - fullpath = "{}/{}.fctb".format(loc, fname) + fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname) PathLog.debug(fullpath) self.temptool = PathToolBit.ToolBitFactory().Create(name=fname) @@ -464,7 +484,7 @@ class ToolBitLibrary(object): loc, fil = os.path.split(f) fname = os.path.splitext(fil)[0] - fullpath = "{}/{}.fctb".format(loc, fname) + fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname) self.factory.newTool(self.toolModel, fullpath) @@ -557,8 +577,8 @@ class ToolBitLibrary(object): self.temptool = PathToolBit.ToolBitFactory().CreateFrom(tbpath, 'temptool') self.editor = PathToolBitEdit.ToolBitEditor(self.temptool, self.form.toolTableGroup, loadBitBody=False) - QBtn = QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel - buttonBox = QtGui.QDialogButtonBox(QBtn) + QBtn = PySide.QtGui.QDialogButtonBox.Ok | PySide.QtGui.QDialogButtonBox.Cancel + buttonBox = PySide.QtGui.QDialogButtonBox(QBtn) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) @@ -604,8 +624,9 @@ class ToolBitLibrary(object): else: tools.append({'nr': toolNr, 'path': PathToolBit.findRelativePathTool(toolPath)}) - with open(self.path, 'w') as fp: - json.dump(library, fp, sort_keys=True, indent=2) + if self.path is not None: + with open(self.path, 'w') as fp: + json.dump(library, fp, sort_keys=True, indent=2) def libraryOk(self): self.librarySave() @@ -652,7 +673,7 @@ class ToolBitLibrary(object): if curIndex: sm = self.form.TableList.selectionModel() - sm.select(curIndex, QtCore.QItemSelectionModel.Select) + sm.select(curIndex, PySide.QtCore.QItemSelectionModel.Select) self.toolTableView.setUpdatesEnabled(True) self.form.TableList.setUpdatesEnabled(True) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 199d5cdf22..3f8405ccd3 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -250,7 +250,8 @@ def Create(name='TC: Default Tool', tool=None, toolNumber=1, assignViewProvider= if tool.ViewObject: tool.ViewObject.Visibility = False - obj.Tool = tool + if tool: + obj.Tool = tool obj.ToolNumber = toolNumber return obj @@ -260,7 +261,7 @@ def FromTemplate(template, assignViewProvider=True): PathLog.track() name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label) - obj = Create(name, assignViewProvider=True) + obj = Create(name, tool=False, assignViewProvider=True) obj.Proxy.setFromTemplate(obj, template) return obj diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 33e735a08d..905888026e 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 40e59a78af..db43b466e5 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -71,6 +71,11 @@ def getPropertyValueString(obj, prop): def setProperty(obj, prop, value): '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.''' o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable + if not attr is None and type(value) == str: + if type(attr) == int: + value = int(value, 0) + elif type(attr) == bool: + value = value.lower() in ['true', '1', 'yes', 'ok'] if o and name: setattr(o, name, value) diff --git a/src/Mod/Path/PathScripts/PathUtilsGui.py b/src/Mod/Path/PathScripts/PathUtilsGui.py index da33987017..a5df387eb3 100644 --- a/src/Mod/Path/PathScripts/PathUtilsGui.py +++ b/src/Mod/Path/PathScripts/PathUtilsGui.py @@ -21,6 +21,7 @@ # *************************************************************************** import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts import PathScripts.PathJobCmd as PathJobCmd import PathScripts.PathUtils as PathUtils diff --git a/src/Mod/Path/PathScripts/PathVcarve.py b/src/Mod/Path/PathScripts/PathVcarve.py index dd713923d6..22c974d108 100644 --- a/src/Mod/Path/PathScripts/PathVcarve.py +++ b/src/Mod/Path/PathScripts/PathVcarve.py @@ -210,11 +210,6 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Additional base objects to be engraved")) obj.setEditorMode('BaseShapes', 2) # hide - if not hasattr(obj, 'BaseObject'): - obj.addProperty("App::PropertyLink", "BaseObject", "Path", - QtCore.QT_TRANSLATE_NOOP("PathVcarve", - "Additional base objects to be engraved")) - obj.setEditorMode('BaseObject', 2) # hide def initOperation(self, obj): '''initOperation(obj) ... create vcarve specific properties.''' @@ -242,7 +237,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): edges.append(_getPartEdge(e, geom)) return edges - def buildPathMedial(self, obj, Faces): + def buildPathMedial(self, obj, faces): '''constructs a medial axis path using openvoronoi''' def insert_many_wires(vd, wires): @@ -271,7 +266,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): VD.clear() voronoiWires = [] - for f in Faces: + for f in faces: vd = Path.Voronoi() insert_many_wires(vd, f.Wires) @@ -311,41 +306,39 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): PathLog.track() if not hasattr(obj.ToolController.Tool, "CuttingEdgeAngle"): - FreeCAD.Console.PrintError( - translate("Path_Vcarve", "VCarve requires an engraving \ - cutter with CuttingEdgeAngle") + "\n") + PathLog.error(translate("Path_Vcarve", "VCarve requires an engraving cutter with CuttingEdgeAngle")) if obj.ToolController.Tool.CuttingEdgeAngle >= 180.0: - FreeCAD.Console.PrintError( - translate("Path_Vcarve", - "Engraver Cutting Edge Angle must be < 180 degrees.") + "\n") + PathLog.error(translate("Path_Vcarve", "Engraver Cutting Edge Angle must be < 180 degrees.")) return + try: - if obj.Base: - PathLog.track() - for base in obj.Base: - faces = [] - for sub in base[1]: - shape = getattr(base[0].Shape, sub) - if isinstance(shape, Part.Face): - faces.append(shape) + faces = [] - modelshape = Part.makeCompound(faces) + for base in obj.BaseShapes: + faces.extend(base.Shape.Faces) - elif len(self.model) == 1 and self.model[0].isDerivedFrom('Sketcher::SketchObject') or \ - self.model[0].isDerivedFrom('Part::Part2DObject'): - PathLog.track() + for base in obj.Base: + for sub in base[1]: + shape = getattr(base[0].Shape, sub) + if isinstance(shape, Part.Face): + faces.append(shape) - modelshape = self.model[0].Shape - self.buildPathMedial(obj, modelshape.Faces) + if not faces: + for model in self.model: + if model.isDerivedFrom('Sketcher::SketchObject') or model.isDerivedFrom('Part::Part2DObject'): + faces.extend(model.Shape.Faces) + + if faces: + self.buildPathMedial(obj, faces) + else: + PathLog.error(translate('PathVcarve', 'The Job Base Object has no engraveable element. Engraving operation will produce no output.')) except Exception as e: - PathLog.error(e) - traceback.print_exc() - PathLog.error(translate('PathVcarve', 'The Job Base Object has \ -no engraveable element. Engraving \ -operation will produce no output.')) - raise e + #PathLog.error(e) + #traceback.print_exc() + PathLog.error(translate('PathVcarve', 'Error processing Base object. Engraving operation will produce no output.')) + #raise e def opUpdateDepths(self, obj, ignoreErrors=False): '''updateDepths(obj) ... engraving is always done at the top most z-value''' diff --git a/src/Mod/Path/PathScripts/PathVcarveGui.py b/src/Mod/Path/PathScripts/PathVcarveGui.py index 5254a78a22..286cce9b28 100644 --- a/src/Mod/Path/PathScripts/PathVcarveGui.py +++ b/src/Mod/Path/PathScripts/PathVcarveGui.py @@ -23,6 +23,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathVcarve as PathVcarve import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui @@ -53,6 +54,7 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): return super(TaskPanelBaseGeometryPage, self) def addBaseGeometry(self, selection): + PathLog.track(selection) added = False shapes = self.obj.BaseShapes for sel in selection: @@ -78,10 +80,12 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): shapes.append(base) self.obj.BaseShapes = shapes added = True - else: - # user wants us to engrave an edge of face of a base model - base = self.super().addBaseGeometry(selection) - added = added or base + + if not added: + # user wants us to engrave an edge of face of a base model + PathLog.info(" call default") + base = self.super().addBaseGeometry(selection) + added = added or base return added diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py index 07958536fd..089e15256a 100644 --- a/src/Mod/Path/PathScripts/PathWaterlineGui.py +++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py @@ -24,6 +24,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathWaterline as PathWaterline import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/post/fanuc_post.py b/src/Mod/Path/PathScripts/post/fanuc_post.py new file mode 100644 index 0000000000..d2bb6acb0e --- /dev/null +++ b/src/Mod/Path/PathScripts/post/fanuc_post.py @@ -0,0 +1,513 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2021 shadowbane1000 * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * 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 Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# ***************************************************************************/ +from __future__ import print_function +import FreeCAD +from FreeCAD import Units +import Path +import argparse +import datetime +import shlex +import os.path +from PathScripts import PostUtils +from PathScripts import PathUtils + +TOOLTIP = ''' +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-gcode fragment outputted by a Path object, and output +real GCode suitable should be suitable for most Fanuc controllers. +It has only been tested on a 21i-MB controller on a 3 axis mill. +This postprocessor, once placed in the appropriate PathScripts folder, +can be used directly from inside FreeCAD, via the GUI importer or via +python scripts with: + +import fanuc_post +fanuc_post.export(object,"/path/to/file.ncc","") +''' + +now = datetime.datetime.now() + +parser = argparse.ArgumentParser(prog='fanuc', add_help=False) +parser.add_argument('--no-header', action='store_true', help='suppress header output') +parser.add_argument('--no-comments', action='store_true', help='suppress comment output') +parser.add_argument('--line-numbers', action='store_true', help='prefix with line numbers') +parser.add_argument('--no-show-editor', action='store_true', help='don\'t pop up editor before writing output') +parser.add_argument('--precision', default='3', help='number of digits of precision, default=3') +parser.add_argument('--preamble', help='set commands to be issued before the first command, default="G17\nG90"') +parser.add_argument('--postamble', help='set commands to be issued after the last command, default="M05\nG17 G90\nM2"') +parser.add_argument('--inches', action='store_true', help='Convert output for US imperial mode (G20)') +parser.add_argument('--no-modal', action='store_true', help='Don\'t output the Same G-command Name USE NonModal Mode') +parser.add_argument('--no-axis-modal', action='store_true', help='Don\'t output the Same Axis Value Mode') +parser.add_argument('--no-tlo', action='store_true', help='suppress tool length offset (G43) following tool changes') + +TOOLTIP_ARGS = parser.format_help() + +# These globals set common customization preferences +OUTPUT_COMMENTS = True +OUTPUT_HEADER = True +OUTPUT_LINE_NUMBERS = False +SHOW_EDITOR = True +MODAL = True # if true commands are suppressed if the same as previous line. +USE_TLO = True # if true G43 will be output following tool changes +OUTPUT_DOUBLES = False # if false duplicate axis values are suppressed if the same as previous line. +COMMAND_SPACE = " " +LINENR = 100 # line number starting value + +# These globals will be reflected in the Machine configuration of the project +UNITS = "G21" # G21 for metric, G20 for us standard +UNIT_SPEED_FORMAT = 'mm/min' +UNIT_FORMAT = 'mm' + +MACHINE_NAME = "fanuc" +CORNER_MIN = {'x': 0, 'y': 0, 'z': 0} +CORNER_MAX = {'x': 500, 'y': 300, 'z': 300} +PRECISION = 3 + +# this global is used to pass spindle speed from the tool command into the machining command for +# rigid tapping. +tapSpeed = 0 + +# Preamble text will appear at the beginning of the GCODE output file. +PREAMBLE = '''G17 G54 G40 G49 G80 G90 +''' + +# Postamble text will appear following the last operation. +POSTAMBLE = '''M05 +G17 G54 G90 G80 G40 +M6 T0 +M2 +''' + +# Pre operation text will be inserted before every operation +PRE_OPERATION = '''''' + +# Post operation text will be inserted after every operation +POST_OPERATION = '''''' + +# Tool Change commands will be inserted before a tool change +TOOL_CHANGE = '''''' + +# to distinguish python built-in open function from the one declared below +if open.__module__ in ['__builtin__','io']: + pythonopen = open + + +def processArguments(argstring): + # pylint: disable=global-statement + global OUTPUT_HEADER + global OUTPUT_COMMENTS + global OUTPUT_LINE_NUMBERS + global SHOW_EDITOR + global PRECISION + global PREAMBLE + global POSTAMBLE + global UNITS + global UNIT_SPEED_FORMAT + global UNIT_FORMAT + global MODAL + global USE_TLO + global OUTPUT_DOUBLES + + try: + args = parser.parse_args(shlex.split(argstring)) + if args.no_header: + OUTPUT_HEADER = False + if args.no_comments: + OUTPUT_COMMENTS = False + if args.line_numbers: + OUTPUT_LINE_NUMBERS = True + if args.no_show_editor: + SHOW_EDITOR = False + print("Show editor = %d" % SHOW_EDITOR) + PRECISION = args.precision + if args.preamble is not None: + PREAMBLE = args.preamble + if args.postamble is not None: + POSTAMBLE = args.postamble + if args.inches: + UNITS = 'G20' + UNIT_SPEED_FORMAT = 'in/min' + UNIT_FORMAT = 'in' + PRECISION = 4 + if args.no_modal: + MODAL = False + if args.no_tlo: + USE_TLO = False + if args.no_axis_modal: + OUTPUT_DOUBLES = true + + except Exception: # pylint: disable=broad-except + return False + + return True + + +def export(objectslist, filename, argstring): + # pylint: disable=global-statement + if not processArguments(argstring): + return None + global UNITS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + global HORIZRAPID + global VERTRAPID + + for obj in objectslist: + if not hasattr(obj, "Path"): + print("the object " + obj.Name + " is not a path. Please select only path and Compounds.") + return None + + print("postprocessing...") + gcode = "" + + # write header + if OUTPUT_HEADER: + gcode += "%\n" + gcode += ";\n" + gcode += os.path.split(filename)[-1]+" ("+"FREECAD-FILENAME-GOES-HERE" + ", " + "JOB-NAME-GOES-HERE"+")\n" + gcode += linenumber() + "("+filename.upper()+",EXPORTED BY FREECAD!)\n" + gcode += linenumber() + "(POST PROCESSOR: " + __name__.upper() + ")\n" + gcode += linenumber() + "(OUTPUT TIME:" + str(now).upper() + ")\n" + + # Write the preamble + if OUTPUT_COMMENTS: + gcode += linenumber() + "(BEGIN PREAMBLE)\n" + for line in PREAMBLE.splitlines(False): + gcode += linenumber() + line + "\n" + gcode += linenumber() + UNITS + "\n" + + for obj in objectslist: + + # Skip inactive operations + if hasattr(obj, 'Active'): + if not obj.Active: + continue + if hasattr(obj, 'Base') and hasattr(obj.Base, 'Active'): + if not obj.Base.Active: + continue + + # fetch machine details + job = PathUtils.findParentJob(obj) + + myMachine = 'not set' + + if hasattr(job, "MachineName"): + myMachine = job.MachineName + + if hasattr(job, "MachineUnits"): + if job.MachineUnits == "Metric": + UNITS = "G21" + UNIT_FORMAT = 'mm' + UNIT_SPEED_FORMAT = 'mm/min' + else: + UNITS = "G20" + UNIT_FORMAT = 'in' + UNIT_SPEED_FORMAT = 'in/min' + + if hasattr(job, "SetupSheet"): + if hasattr(job.SetupSheet, "HorizRapid"): + HORIZRAPID = Units.Quantity(job.SetupSheet.HorizRapid, FreeCAD.Units.Velocity) + if hasattr(job.SetupSheet, "VertRapid"): + VERTRAPID = Units.Quantity(job.SetupSheet.HorizRapid, FreeCAD.Units.Velocity) + + # do the pre_op + if OUTPUT_COMMENTS: + gcode += linenumber() + "(BEGIN OPERATION: %s)\n" % obj.Label.upper() + gcode += linenumber() + "(MACHINE: %s, %s)\n" % (myMachine.upper(), UNIT_SPEED_FORMAT.upper()) + for line in PRE_OPERATION.splitlines(True): + gcode += linenumber() + line + + # get coolant mode + coolantMode = 'None' + if hasattr(obj, "CoolantMode") or hasattr(obj, 'Base') and hasattr(obj.Base, "CoolantMode"): + if hasattr(obj, "CoolantMode"): + coolantMode = obj.CoolantMode + else: + coolantMode = obj.Base.CoolantMode + + # turn coolant on if required + if OUTPUT_COMMENTS: + if not coolantMode == 'None': + gcode += linenumber() + '(COOLANT ON:' + coolantMode.upper() + ')\n' + if coolantMode == 'Flood': + gcode += linenumber() + 'M8' + '\n' + if coolantMode == 'Mist': + gcode += linenumber() + 'M7' + '\n' + + # process the operation gcode + gcode += parse(obj) + + # do the post_op + if OUTPUT_COMMENTS: + gcode += linenumber() + "(FINISH OPERATION: %s)\n" % obj.Label.upper() + for line in POST_OPERATION.splitlines(True): + gcode += linenumber() + line + + # turn coolant off if required + if not coolantMode == 'None': + if OUTPUT_COMMENTS: + gcode += linenumber() + '(COOLANT OFF:' + coolantMode.upper() + ')\n' + gcode += linenumber() +'M9' + '\n' + + # do the post_amble + if OUTPUT_COMMENTS: + gcode += "(BEGIN POSTAMBLE)\n" + for line in POSTAMBLE.splitlines(True): + gcode += linenumber() + line + gcode += "%\n" + + if FreeCAD.GuiUp and SHOW_EDITOR: + dia = PostUtils.GCodeEditorDialog() + dia.editor.setText(gcode) + result = dia.exec_() + if result: + final = dia.editor.toPlainText() + else: + final = gcode + else: + final = gcode + + print("done postprocessing.") + + if not filename == '-': + gfile = pythonopen(filename, "w") + gfile.write(final) + gfile.close() + + return final + + +def linenumber(): + # pylint: disable=global-statement + global LINENR + if OUTPUT_LINE_NUMBERS is True: + LINENR += 10 + return "N" + str(LINENR) + " " + return "" + + +def parse(pathobj): + # pylint: disable=global-statement + global PRECISION + global MODAL + global OUTPUT_DOUBLES + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + global tapSpeed + + out = "" + lastcommand = None + precision_string = '.' + str(PRECISION) + 'f' + currLocation = {} # keep track for no doubles + print("Startup!") + + # the order of parameters + # arcs need work. original code from mach3_4 doesn't want K properties on XY plane. Not sure + # what fanuc does here. + params = ['X', 'Y', 'Z', 'A', 'B', 'C', 'I', 'J', 'F', 'S', 'T', 'Q', 'R', 'L', 'H', 'D', 'P'] + firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) + currLocation.update(firstmove.Parameters) # set First location Parameters + + if hasattr(pathobj, "Group"): # We have a compound or project. + # if OUTPUT_COMMENTS: + # out += linenumber() + "(compound: " + pathobj.Label + ")\n" + for p in pathobj.Group: + out += parse(p) + return out + else: # parsing simple path + + # groups might contain non-path things like stock. + if not hasattr(pathobj, "Path"): + return out + + # if OUTPUT_COMMENTS: + # out += linenumber() + "(" + pathobj.Label + ")\n" + + adaptiveOp = False + opHorizRapid = 0 + opVertRapid = 0 + + if 'Adaptive' in pathobj.Name: + adaptiveOp = True + if hasattr(pathobj, 'ToolController'): + if hasattr(pathobj.ToolController, 'HorizRapid') and pathobj.ToolController.HorizRapid > 0: + opHorizRapid = Units.Quantity(pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity) + else: + FreeCAD.Console.PrintWarning('Tool Controller Horizontal Rapid Values are unset'+ '\n') + + if hasattr(pathobj.ToolController, 'VertRapid') and pathobj.ToolController.VertRapid > 0: + opVertRapid = Units.Quantity(pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity) + else: + FreeCAD.Console.PrintWarning('Tool Controller Vertical Rapid Values are unset'+ '\n') + + for index,c in enumerate(pathobj.Path.Commands): + + outstring = [] + command = c.Name + if index+1 == len(pathobj.Path.Commands): + nextcommand = "" + else: + nextcommand = pathobj.Path.Commands[index+1].Name + + if adaptiveOp and c.Name in ["G0", "G00"]: + if opHorizRapid and opVertRapid: + command = 'G1' + else: + outstring.append('(TOOL CONTROLLER RAPID VALUES ARE UNSET)' + '\n') + + # suppress moves in fixture selection + if pathobj.Label == "Fixture": + if command == "G0": + continue + + # if it's a tap, we rigid tap, so don't start the spindle yet... + if command == "M03" or command == "M3": + if pathobj.Tool.ToolType == "Tap": + tapSpeed = int(pathobj.SpindleSpeed) + continue + + # convert drill cycles to tap cycles if tool is a tap + if command == "G81" or command == "G83": + if hasattr(pathobj, 'ToolController') and pathobj.ToolController.Tool.ToolType == "Tap": + command = "G84" + out += linenumber() + "G95\n" + paramstring = "" + for param in [ "X", "Y" ]: + if param in c.Parameters: + if (not OUTPUT_DOUBLES) and (param in currLocation) and (currLocation[param] == c.Parameters[param]): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += " " + param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) + if paramstring != "": + out += linenumber() + "G00"+paramstring+"\n" + + if "S" in c.Parameters: + tapSpeed = int(c.Parameters['S']) + out += "M29 S"+str(tapSpeed)+"\n" + + for param in [ "Z", "R" ]: + if param in c.Parameters: + if (not OUTPUT_DOUBLES) and (param in currLocation) and (currLocation[param] == c.Parameters[param]): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += " " + param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) + # in this mode, F is the distance per revolution of the thread (pitch) + # P is the dwell time in seconds at the bottom of the thread + # Q is the peck depth of the threading operation + for param in [ "F", "P", "Q" ]: + if param in c.Parameters: + value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += " " + param + format(float(value.getValueAs(UNIT_FORMAT)), precision_string) + + out += linenumber() + "G84" + paramstring + "\n" + out += linenumber() + "G80\n" + out += linenumber() + "G94\n" + continue + + + outstring.append(command) + + # if modal: suppress the command if it is the same as the last one + if MODAL is True: + if command == lastcommand: + outstring.pop(0) + + # suppress a G80 between two identical command + if command == "G80" and lastcommand == nextcommand: + continue + + if c.Name[0] == '(' and not OUTPUT_COMMENTS: # command is a comment + continue + + # Now add the remaining parameters in order + for param in params: + if param in c.Parameters: + if param == 'F' and (currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES): + if c.Name not in ["G0", "G00"]: # fanuc doesn't use rapid speeds + speed = Units.Quantity(c.Parameters['F'], FreeCAD.Units.Velocity) + if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: + outstring.append(param + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string)) + else: + continue + elif param == 'T': + outstring.append(param + str(int(c.Parameters['T']))) + elif param == 'H': + outstring.append(param + str(int(c.Parameters['H']))) + elif param == 'D': + outstring.append(param + str(int(c.Parameters['D']))) + elif param == 'S': + outstring.append(param + str(int(c.Parameters['S']))) + currentSpeed = int(c.Parameters['S']) + else: + if (not OUTPUT_DOUBLES) and (param in currLocation) and (currLocation[param] == c.Parameters[param]): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + outstring.append( + param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)) + + if adaptiveOp and c.Name in ["G0", "G00"]: + if opHorizRapid and opVertRapid: + if 'Z' not in c.Parameters: + outstring.append('F' + format(float(opHorizRapid.getValueAs(UNIT_SPEED_FORMAT)), precision_string)) + else: + outstring.append('F' + format(float(opVertRapid.getValueAs(UNIT_SPEED_FORMAT)), precision_string)) + + # store the latest command + lastcommand = command + currLocation.update(c.Parameters) + + # Check for Tool Change: + if command == 'M6': + # stop the spindle + currentSpeed = 0 + out += linenumber() + "M5\n" + for line in TOOL_CHANGE.splitlines(True): + out += linenumber() + line + + # add height offset + if USE_TLO: + tool_height = '\nG43 H' + str(int(c.Parameters['T'])) + outstring.append(tool_height) + + if command == "message": + if OUTPUT_COMMENTS is False: + out = [] + else: + outstring.pop(0) # remove the command + + # prepend a line number and append a newline + if len(outstring) >= 1: + if OUTPUT_LINE_NUMBERS: + outstring.insert(0, (linenumber())) + + # append the line to the final output + for w in outstring: + out += w.upper() + COMMAND_SPACE + out = out.strip() + "\n" + + return out + +# print(__name__ + " gcode postprocessor loaded.") diff --git a/src/Mod/Path/PathScripts/post/philips_post.py b/src/Mod/Path/PathScripts/post/philips_post.py index 41203ef075..f06d2d666d 100644 --- a/src/Mod/Path/PathScripts/post/philips_post.py +++ b/src/Mod/Path/PathScripts/post/philips_post.py @@ -384,7 +384,7 @@ def export(objectslist, filename, argstring): # #\better: append iff MODAL == False # if command == lastcommand: # outstring.pop(0) - if c.Parameters >= 1: + if len(c.Parameters) >= 1: for param in params: # test print("param: " + param + ", command: " + command) if param in c.Parameters: diff --git a/src/Mod/Path/PathTests/TestPathPropertyBag.py b/src/Mod/Path/PathTests/TestPathPropertyBag.py new file mode 100644 index 0000000000..974ca3bd6c --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathPropertyBag.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** + +import FreeCAD +import PathScripts.PathPropertyBag as PathPropertyBag +import PathTests.PathTestUtils as PathTestUtils + +class TestPathPropertyBag(PathTestUtils.PathTestBase): + + def setUp(self): + self.doc = FreeCAD.newDocument('test-property-bag') + + def tearDown(self): + FreeCAD.closeDocument(self.doc.Name) + + def test00(self): + '''basic PropertyBag creation and access test''' + bag = PathPropertyBag.Create() + self.assertTrue(hasattr(bag, 'Proxy')) + self.assertEqual(bag.Proxy.getCustomProperties(), []) + self.assertEqual(bag.CustomPropertyGroups, []) + + def test01(self): + '''adding properties to a PropertyBag is tracked properly''' + bag = PathPropertyBag.Create() + proxy = bag.Proxy + proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description') + self.assertTrue(hasattr(bag, 'Title')) + bag.Title = 'Madame' + self.assertEqual(bag.Title, 'Madame') + self.assertEqual(bag.Proxy.getCustomProperties(), ['Title']) + self.assertEqual(bag.CustomPropertyGroups, ['Address']) + + def test02(self): + '''refreshCustomPropertyGroups deletes empty groups''' + bag = PathPropertyBag.Create() + proxy = bag.Proxy + proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description') + bag.Title = 'Madame' + bag.removeProperty('Title') + proxy.refreshCustomPropertyGroups() + self.assertEqual(bag.Proxy.getCustomProperties(), []) + self.assertEqual(bag.CustomPropertyGroups, []) + + def test03(self): + '''refreshCustomPropertyGroups does not delete non-empty groups''' + bag = PathPropertyBag.Create() + proxy = bag.Proxy + proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description') + proxy.addCustomProperty('App::PropertyString', 'Gender', 'Attributes') + bag.Title = 'Madame' + bag.Gender = 'Female' + bag.removeProperty('Gender') + proxy.refreshCustomPropertyGroups() + self.assertEqual(bag.Proxy.getCustomProperties(), ['Title']) + self.assertEqual(bag.CustomPropertyGroups, ['Address']) + diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index dd82c7fc63..7a684b3697 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -22,37 +22,143 @@ import PathScripts.PathToolBit as PathToolBit import PathTests.PathTestUtils as PathTestUtils +import glob +import os +TestToolDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Tools') +TestInvalidDir = os.path.join(TestToolDir, 'some', 'silly', 'path', 'that', 'should', 'not', 'exist') + +TestToolBitName = 'test-path-tool-bit-bit-00.fctb' +TestToolShapeName = 'test-path-tool-bit-shape-00.fcstd' +TestToolLibraryName = 'test-path-tool-bit-library-00.fctl' + +def testToolShape(path = TestToolDir, name = TestToolShapeName): + return os.path.join(path, 'Shape', name) + +def testToolBit(path = TestToolDir, name = TestToolBitName): + return os.path.join(path, 'Bit', name) + +def testToolLibrary(path = TestToolDir, name = TestToolLibraryName): + return os.path.join(path, 'Library', name) + +def printTree(path, indent): + print("{} {}".format(indent, os.path.basename(path))) + if os.path.isdir(path): + if os.path.basename(path).startswith('__'): + print("{} ...".format(indent)) + else: + for foo in sorted(glob.glob(os.path.join(path, '*'))): + printTree(foo, "{} ".format(indent)) class TestPathToolBit(PathTestUtils.PathTestBase): - def test00(self): - '''Find a tool shapee from file name''' + def test(self): + '''Log test setup directory structure''' + # Enable this test if there are errors showing up in the build system with the + # paths that work OK locally. It'll print out the directory tree, and if it + # doesn't look right you know where to look for it + print() + print("realpath : {}".format(os.path.realpath(__file__))) + print(" Tools : {}".format(TestToolDir)) + print(" dir : {}".format(os.path.dirname(os.path.realpath(__file__)))) + printTree(os.path.dirname(os.path.realpath(__file__)), " :") - path = PathToolBit.findShape('endmill.fcstd') + def test00(self): + '''Find a tool shape from file name''' + path = PathToolBit.findToolShape('endmill.fcstd') self.assertIsNot(path, None) self.assertNotEqual(path, 'endmill.fcstd') - def test01(self): - '''Find a tool shapee from an invalid absolute path.''' - path = PathToolBit.findShape('/this/is/unlikely/a/valid/path/v-bit.fcstd') + def test01(self): + '''Not find a relative path shape if not stored in default location''' + path = PathToolBit.findToolShape(TestToolShapeName) + self.assertIsNone(path) + + + def test02(self): + '''Find a relative path shape if it's local to a bit path''' + path = PathToolBit.findToolShape(TestToolShapeName, testToolBit()) self.assertIsNot(path, None) - self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') + self.assertEqual(path, testToolShape()) + + + def test03(self): + '''Not find a tool shape from an invalid absolute path.''' + path = PathToolBit.findToolShape(testToolShape(TestInvalidDir)) + self.assertIsNone(path) + + + def test04(self): + '''Find a tool shape from a valid absolute path.''' + path = PathToolBit.findToolShape(testToolShape()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolShape()) def test10(self): - '''find the relative path of a tool bit''' - shape = 'endmill.fcstd' - path = PathToolBit.findShape(shape) + '''Find a tool bit from file name''' + path = PathToolBit.findToolBit('5mm_Endmill.fctb') self.assertIsNot(path, None) - self.assertGreater(len(path), len(shape)) - rel = PathToolBit.findRelativePathShape(path) - self.assertEqual(rel, shape) + self.assertNotEqual(path, '5mm_Endmill.fctb') + def test11(self): - '''store full path if relative path isn't found''' - path = '/this/is/unlikely/a/valid/path/v-bit.fcstd' - rel = PathToolBit.findRelativePathShape(path) - self.assertEqual(rel, path) + '''Not find a relative path bit if not stored in default location''' + path = PathToolBit.findToolBit(TestToolBitName) + self.assertIsNone(path) + + + def test12(self): + '''Find a relative path bit if it's local to a library path''' + path = PathToolBit.findToolBit(TestToolBitName, testToolLibrary()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolBit()) + + + def test13(self): + '''Not find a tool bit from an invalid absolute path.''' + path = PathToolBit.findToolBit(testToolBit(TestInvalidDir)) + self.assertIsNone(path) + + + def test14(self): + '''Find a tool bit from a valid absolute path.''' + path = PathToolBit.findToolBit(testToolBit()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolBit()) + + + + def test20(self): + '''Find a tool library from file name''' + path = PathToolBit.findToolLibrary('Default.fctl') + self.assertIsNot(path, None) + self.assertNotEqual(path, 'Default.fctl') + + + def test21(self): + '''Not find a relative path library if not stored in default location''' + path = PathToolBit.findToolLibrary(TestToolLibraryName) + self.assertIsNone(path) + + + def test22(self): + '''[skipped] Find a relative path library if it's local to ''' + # this is not a valid test for libraries because t + self.assertTrue(True) + + + def test23(self): + '''Not find a tool library from an invalid absolute path.''' + path = PathToolBit.findToolLibrary(testToolLibrary(TestInvalidDir)) + self.assertIsNone(path) + + + def test24(self): + '''Find a tool library from a valid absolute path.''' + path = PathToolBit.findToolBit(testToolBit()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolBit()) + diff --git a/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb b/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb new file mode 100644 index 0000000000..7cc72ba33c --- /dev/null +++ b/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb @@ -0,0 +1,12 @@ +{ + "version": 2, + "name": "5mm Endmill", + "shape": "endmill.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.0000 mm", + "Diameter": "5.0000 mm", + "Length": "50.0000 mm", + "ShankDiameter": "3.0000 mm" + }, + "attribute": {} +} diff --git a/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl b/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl new file mode 100644 index 0000000000..60de98e08e --- /dev/null +++ b/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl @@ -0,0 +1,41 @@ +{ + "tools": [ + { + "nr": 1, + "path": "5mm_Endmill.fctb" + }, + { + "nr": 2, + "path": "5mm_Drill.fctb" + }, + { + "nr": 3, + "path": "6mm_Ball_End.fctb" + }, + { + "nr": 4, + "path": "6mm_Bullnose.fctb" + }, + { + "nr": 5, + "path": "60degree_Vbit.fctb" + }, + { + "nr": 6, + "path": "45degree_chamfer.fctb" + }, + { + "nr": 7, + "path": "slittingsaw.fctb" + }, + { + "nr": 8, + "path": "probe.fctb" + }, + { + "nr": 9, + "path": "5mm-thread-cutter.fctb" + } + ], + "version": 1 +} diff --git a/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd b/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd new file mode 100644 index 0000000000..5b5a76dc41 Binary files /dev/null and b/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd differ diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 0adebedfc6..0be621b040 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -24,6 +24,7 @@ import TestApp from PathTests.TestPathLog import TestPathLog from PathTests.TestPathPreferences import TestPathPreferences +from PathTests.TestPathPropertyBag import TestPathPropertyBag from PathTests.TestPathCore import TestPathCore #from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathGeom import TestPathGeom @@ -66,4 +67,5 @@ False if TestPathToolBit.__name__ else True False if TestPathVoronoi.__name__ else True False if TestPathThreadMilling.__name__ else True False if TestPathVcarve.__name__ else True +False if TestPathPropertyBag.__name__ else True diff --git a/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb b/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb index 6c1231ed0f..77e53e523c 100644 --- a/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb +++ b/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb @@ -11,4 +11,4 @@ "ShankDiameter": "6.3500 mm" }, "attribute": {} -} \ No newline at end of file +} diff --git a/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb index 265978053b..2a5f0253cd 100644 --- a/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb +++ b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb @@ -1,14 +1,14 @@ { "version": 2, - "name": "3mm-thread-cutter", + "name": "5mm-thread-cutter", "shape": "thread-mill.fcstd", "parameter": { "Crest": "0.10 mm", "Diameter": "5.00 mm", "Length": "50.00 mm", "NeckDiameter": "3.00 mm", - "NeckHeight": "20.00 mm", + "NeckLength": "20.00 mm", "ShankDiameter": "5.00 mm" }, "attribute": {} -} \ No newline at end of file +} diff --git a/src/Mod/Path/Tools/Bit/probe.fctb b/src/Mod/Path/Tools/Bit/probe.fctb index b92828e7ac..31fa354ff6 100644 --- a/src/Mod/Path/Tools/Bit/probe.fctb +++ b/src/Mod/Path/Tools/Bit/probe.fctb @@ -1,6 +1,6 @@ { "version": 2, - "name": "Probe004", + "name": "Probe", "shape": "probe.fcstd", "parameter": { "Diameter": "6.0000 mm", diff --git a/src/Mod/Path/Tools/Bit/slittingsaw.fctb b/src/Mod/Path/Tools/Bit/slittingsaw.fctb index e9d33fe571..5b63c060ed 100644 --- a/src/Mod/Path/Tools/Bit/slittingsaw.fctb +++ b/src/Mod/Path/Tools/Bit/slittingsaw.fctb @@ -4,8 +4,8 @@ "shape": "slittingsaw.fcstd", "parameter": { "BladeThickness": "3.0000 mm", - "BoltHeight": "3.0000 mm", - "BoltWidth": "8.0000 mm", + "CapHeight": "3.0000 mm", + "CapDiameter": "8.0000 mm", "Diameter": "76.2000 mm", "Length": "50.0000 mm", "ShankDiameter": "19.0500 mm" diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index cdaed5a5b4..e71218303b 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -1,6 +1,6 @@ # Tools -Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template. +Each tool is stored as a JSON file which has the shape's path and values for all attributes of the shape. It also includes all additional parameters and their values. Storing a tool as a JSON file sounds great but eliminates the option of an accurate thumbnail. On the other hand, @@ -8,10 +8,10 @@ storing each tool as a `*.fcstd` file requires more space and does not allow for extensive tool aresenal they might want to script the generation of tools which is easily done for a `*.json` file but practically impossible for `*.fcstd` files. -When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according -to the values from the JSON file. All additional parameters are created as properties on the object. This provides the -the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms (and -potentially simulation). +When a tool is instantiated in a job the PDN body is created from the shape and the attributes and constraints are set +according to the values from the JSON file. All additional parameters are created as properties on the object. This +provides the the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced +algorithms (and potentially simulation). # Tool Libraries @@ -55,33 +55,34 @@ TechDraw's templates. ## How to create a new tool 1. Set the tool's Label, this will show up in the object tree -1. Select a tool shape from the existing templates. If your tool doesn't exist, you'll have to create a new template, +1. Select a tool shape from the existing shape files. If your tool doesn't exist, you'll have to create a new shape, see below for details. -1. Each template has its own set of parameters, fill them with the tool's values. +1. Each tool bit shape has its own set of parameters, fill them with the tool's values. 1. Select additional parameters 1. Save the tool under path/file that makes sense to you ## How to create a new tool bit Shape -A tool bit template represents the physical shape of a tool. It does not completely describe the bit - for that some -additional parameters are needed which will be added when an actual bit is parametrized from the template. +The shape file for a tool bit is expected to contain a PD body which represents the tool as a 3d solid. The PD body +should be parametric based on a a PropertyBag object so that, when the properties of the PropertyBag are changed the +solid is updated to the correct representation. 1. Create a new FreeCAD document 1. Open the `PartDesign` workbench, create a body and give the body a label you want to show up in the bit selection. -1. Create a sketch in the XZ plane and draw half the profile of the bit. - * Put the top center of the bit on the origin (0,0) -1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint - * The name is the label of the input field - * Names are split at CamelCase boundaries into words in the edit dialog - * Use a `;` in the name to add help text which will show up as the entry fields tool tip - * If the tool is used by legacy ops it should at least have one constraint called `Diameter` - * Use construction lines for constraints that are not directly accessible, like `Diameter` and `Angle` -1. Any unnamed constraint will not be editable for a specific tool -1. Once the sketch is fully constrained, close the sketch -1. Rotate the sketch around the z-axis +1. Open the Path workbench and (with the PD body selected) create a PropertyBag, + menu 'Path' -> 'Utils' -> 'Property Bag' + * this creates a PropertyBag object inside the Body (assuming it was selected) + * add properties to which define the tool bit's shape and put those into the group 'Shape' + * add any other properties to the bag which might be useful for the tool bit +1. Construct the body of the tool bit and assign expressions referencing properties from the PropertyBag (in the + `Shape` Group) for all constraints. + * Position the tip of the tool bit on the origin (0,0) 1. Save the document as a new file in the Shape directory * Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in FreeCAD's preferences. * Also make sure to switch to _Front View_ and _Fit content to screen_ - * Whatever you see when saving the document will end up being the visual representation of the template + * Whatever you see when saving the document will end up being the visual representation of tool bits with this shape + +Not that 'Shape' is the only property group which has special meaning for tool bits. All other property groups are +copied verbatim to the ToolBit object when one is created. diff --git a/src/Mod/Path/Tools/Shape/ballend.fcstd b/src/Mod/Path/Tools/Shape/ballend.fcstd index c3c5ee8b25..14b2a65efa 100644 Binary files a/src/Mod/Path/Tools/Shape/ballend.fcstd and b/src/Mod/Path/Tools/Shape/ballend.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/bullnose.fcstd b/src/Mod/Path/Tools/Shape/bullnose.fcstd index c35ae78d81..63a50c4969 100644 Binary files a/src/Mod/Path/Tools/Shape/bullnose.fcstd and b/src/Mod/Path/Tools/Shape/bullnose.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/chamfer.fcstd b/src/Mod/Path/Tools/Shape/chamfer.fcstd index 3470b4a3c6..59c7cb1a3a 100644 Binary files a/src/Mod/Path/Tools/Shape/chamfer.fcstd and b/src/Mod/Path/Tools/Shape/chamfer.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/drill.fcstd b/src/Mod/Path/Tools/Shape/drill.fcstd index aa2a626d02..da1e277778 100644 Binary files a/src/Mod/Path/Tools/Shape/drill.fcstd and b/src/Mod/Path/Tools/Shape/drill.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/endmill.fcstd b/src/Mod/Path/Tools/Shape/endmill.fcstd index 98888db1e3..b3b1ae18a4 100644 Binary files a/src/Mod/Path/Tools/Shape/endmill.fcstd and b/src/Mod/Path/Tools/Shape/endmill.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/probe.fcstd b/src/Mod/Path/Tools/Shape/probe.fcstd index 5f853f415b..b4f20fa949 100644 Binary files a/src/Mod/Path/Tools/Shape/probe.fcstd and b/src/Mod/Path/Tools/Shape/probe.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/slittingsaw.fcstd b/src/Mod/Path/Tools/Shape/slittingsaw.fcstd index 81d3d7f3d9..6694d911f7 100644 Binary files a/src/Mod/Path/Tools/Shape/slittingsaw.fcstd and b/src/Mod/Path/Tools/Shape/slittingsaw.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/thread-mill.fcstd b/src/Mod/Path/Tools/Shape/thread-mill.fcstd index d97e56241f..c69e915396 100644 Binary files a/src/Mod/Path/Tools/Shape/thread-mill.fcstd and b/src/Mod/Path/Tools/Shape/thread-mill.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd index 3168e5fe8d..51db09d7c7 100644 Binary files a/src/Mod/Path/Tools/Shape/v-bit.fcstd and b/src/Mod/Path/Tools/Shape/v-bit.fcstd differ diff --git a/src/Mod/Path/Tools/toolbit-attributes.py b/src/Mod/Path/Tools/toolbit-attributes.py new file mode 100755 index 0000000000..2f04ed6a58 --- /dev/null +++ b/src/Mod/Path/Tools/toolbit-attributes.py @@ -0,0 +1,152 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** +# +# Script for manipulating toolbit shapes in a batch. This is handy for making +# attributes consistent over multiple or all shape file. +# +# Most commands are straight forward, except for --set which can be used to +# set the actual value of a property, or, in the case of enumerations it can +# also be used to set the enum values. This might be particularly useful when +# adding more materials. +# +# The following example moves all properties from the "Extra" group into the +# "Attributes" group. Note that the Attributes group might or might not +# already exist. If it does exist the specified group gets merged in. +# +# ./toolbit-attributes.py --move 'Extra:Attributes' src/Mod/Path/Tools/Shape/*.fcstd +# +# This example sets the Flutes value of all Shapes to 0: +# +# ./toolbit-attributes.py --set Flutes=0 src/Mod/Path/Tools/Shape/*.fcstd +# +# Finally, this example sets the enumerations of the Material attribute: +# +# ./toolbit-attributes.py --set 'Material=[HSS,Carbide,Tool Steel,Titanium]' src/Mod/Path/Tools/Shape/*.fcstd +# +# After running this tool it might be necessary to open the shape files +# manually and make sure they are visible and the thumbprint image is +# saved. +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# !!! Final note: this is a dangerous tool and should not be shipped with FC. !!!! +# !!! It's sole purpose is to make life of the developers easier and ensure !!!! +# !!! consistent attributes across all toobit shapes. !!!! +# !!! A single typo can ruin a lot of toolbit shapes - make sure to only use !!!! +# !!! it if those shape files are under a version control system and you can !!!! +# !!! back out the changes easily. !!!! +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +import argparse +import os +import sys + +parser = argparse.ArgumentParser() +parser.add_argument('path', nargs='+', help='Shape file to process') +parser.add_argument('--move', metavar=':', help='Move attributes from group 1 into group 2') +parser.add_argument('--delete', metavar='prop', help='Delete the given attribute') +parser.add_argument('--set', metavar='prop=value', help='Set property value') +parser.add_argument('--print', action='store_true', help='If set attributes are printed as discovered') +parser.add_argument('--print-all', action='store_true', help='If set Shape attributes are also printed') +parser.add_argument('--print-groups', action='store_true', help='If set all custom property groups are printed') +parser.add_argument('--save-changes', action='store_true', help='Unless specified the file is not saved') +parser.add_argument('--freecad', help='Directory FreeCAD binaries (libFreeCAD.so) if not installed') +args = parser.parse_args() + +if args.freecad: + sys.path.append(args.freecad) + +import FreeCAD +import Path +import PathScripts.PathPropertyBag as PathPropertyBag +import PathScripts.PathUtil as PathUtil + +set_var=None +set_val=None + +GroupMap = {} +if args.move: + g = args.move.split(':') + if len(g) != 2: + print("ERROR: {} not a valid group mapping".format(args.move)) + sys.exit(1) + GroupMap[g[0]] = g[1] + +if args.set: + s = args.set.split('=') + if len(s) != 2: + print("ERROR: {} not a valid group mapping".format(args.move)) + sys.exit(1) + set_var = s[0] + set_val = s[1] + +for i, fname in enumerate(args.path): + #print(fname) + doc = FreeCAD.openDocument(fname, False) + print("{}:".format(doc.Name)) + for o in doc.Objects: + if PathPropertyBag.IsPropertyBag(o): + if args.print_groups: + print(" {}: {}".format(o.Label, sorted(o.CustomPropertyGroups))) + else: + print(" {}:".format(o.Label)) + for p in o.Proxy.getCustomProperties(): + grp = o.getGroupOfProperty(p) + typ = o.getTypeIdOfProperty(p) + ttp = PathPropertyBag.getPropertyTypeName(typ) + val = PathUtil.getProperty(o, p) + dsc = o.getDocumentationOfProperty(p) + enm = '' + enum = [] + if ttp == 'Enumeration': + enum = o.getEnumerationsOfProperty(p) + enm = "{}".format(','.join(enum)) + if GroupMap.get(grp): + group = GroupMap.get(grp) + print("move: {}.{} -> {}".format(grp, p, group)) + o.removeProperty(p) + o.Proxy.addCustomProperty(typ, p, group, dsc) + if enum: + print("enum {}.{}: {}".format(group, p, enum)) + setattr(o, p, enum) + PathUtil.setProperty(o, p, val) + if p == set_var: + print("set {}.{} = {}".format(grp, p, set_val)) + if ttp == 'Enumeration' and set_val[0] == '[': + enum = set_val[1:-1].split(',') + setattr(o, p, enum) + else: + PathUtil.setProperty(o, p, set_val) + if p == args.delete: + print("delete {}.{}".format(grp, p)) + o.removeProperty(p) + if not args.print_all and grp == 'Shape': + continue + if args.print or args.print_all: + print(" {:10} {:20} {:20} {:10} {}".format(grp, p, ttp, str(val), enm)) + o.Proxy.refreshCustomPropertyGroups() + if args.save_changes: + doc.recompute() + doc.save() + FreeCAD.closeDocument(doc.Name) + +print('-done-') diff --git a/src/Mod/Sketcher/App/Constraint.h b/src/Mod/Sketcher/App/Constraint.h index 7a3316b08a..6e526030cc 100644 --- a/src/Mod/Sketcher/App/Constraint.h +++ b/src/Mod/Sketcher/App/Constraint.h @@ -75,7 +75,13 @@ enum InternalAlignmentType { BSplineKnotPoint = 10, }; -/// define if you want to use the end or start point +/*! PointPos lets us refer to different aspects of a piece of geometry. sketcher::none refers + * to an edge itself (eg., for a Perpendicular constraint on two lines). sketcher::start and + * sketcher::end denote the endpoints of lines or bounded curves. sketcher::mid denotes + * geometries with geometrical centers (eg., circle, ellipse). Bare points use 'start'. More + * complex geometries like parabola focus or b-spline knots use InternalAlignment constraints + * in addition to PointPos. + */ enum PointPos { none, start, end, mid }; class SketcherExport Constraint : public Base::Persistence diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index e87e51d7a7..6edd7e74e3 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -1542,6 +1542,172 @@ int SketchObject::delConstraintOnPoint(int GeoId, PointPos PosId, bool onlyCoinc return -1; // no such constraint } +void SketchObject::transferFilletConstraints(int geoId1, PointPos posId1, int geoId2, PointPos posId2) { + // If the lines don't intersect, there's no original corner to work with so + // don't try to transfer the constraints. But we should delete line length and equal + // constraints and constraints on the affected endpoints because they're about + // to move unpredictably. + if (!arePointsCoincident(geoId1, posId1, geoId2, posId2)) { + // Delete constraints on the endpoints + delConstraintOnPoint(geoId1, posId1, false); + delConstraintOnPoint(geoId2, posId2, false); + + // Delete line length and equal constraints + const std::vector &constraints = this->Constraints.getValues(); + std::vector deleteme; + for (int i=0; i < int(constraints.size()); i++) { + const Constraint *c = constraints[i]; + if (c->Type == Sketcher::Distance || c->Type == Sketcher::Equal) { + bool line1 = c->First == geoId1 && c->FirstPos == none; + bool line2 = c->First == geoId2 && c->FirstPos == none; + if (line1 || line2) { + deleteme.push_back(i); + } + } + } + delConstraints(deleteme, false); + return; + } + + // If the lines aren't straight, don't try to transfer the constraints. + // TODO: Add support for curved lines. + const Part::Geometry *geo1 = getGeometry(geoId1); + const Part::Geometry *geo2 = getGeometry(geoId2); + if (geo1->getTypeId() != Part::GeomLineSegment::getClassTypeId() || + geo2->getTypeId() != Part::GeomLineSegment::getClassTypeId() ) { + delConstraintOnPoint(geoId1, posId1, false); + delConstraintOnPoint(geoId2, posId2, false); + return; + } + + // Add a vertex to preserve the original intersection of the filleted lines + Part::GeomPoint *originalCorner = new Part::GeomPoint(getPoint(geoId1, posId1)); + int originalCornerId = addGeometry(originalCorner); + delete originalCorner; + + // Constrain the vertex to the two lines + Sketcher::Constraint *cornerToLine1 = new Sketcher::Constraint(); + cornerToLine1->Type = Sketcher::PointOnObject; + cornerToLine1->First = originalCornerId; + cornerToLine1->FirstPos = start; + cornerToLine1->Second = geoId1; + cornerToLine1->SecondPos = none; + addConstraint(cornerToLine1); + delete cornerToLine1; + Sketcher::Constraint *cornerToLine2 = new Sketcher::Constraint(); + cornerToLine2->Type = Sketcher::PointOnObject; + cornerToLine2->First = originalCornerId; + cornerToLine2->FirstPos = start; + cornerToLine2->Second = geoId2; + cornerToLine2->SecondPos = none; + addConstraint(cornerToLine2); + delete cornerToLine2; + + Base::StateLocker lock(managedoperation, true); + + // Loop through all the constraints and try to do reasonable things with the affected ones + std::vector newConstraints; + for (auto c : this->Constraints.getValues()) { + // Keep track of whether the affected lines and endpoints appear in this constraint + bool point1First = c->First == geoId1 && c->FirstPos == posId1; + bool point2First = c->First == geoId2 && c->FirstPos == posId2; + bool point1Second = c->Second == geoId1 && c->SecondPos == posId1; + bool point2Second = c->Second == geoId2 && c->SecondPos == posId2; + bool point1Third = c->Third == geoId1 && c->ThirdPos == posId1; + bool point2Third = c->Third == geoId2 && c->ThirdPos == posId2; + bool line1First = c->First == geoId1 && c->FirstPos == none; + bool line2First = c->First == geoId2 && c->FirstPos == none; + bool line1Second = c->Second == geoId1 && c->SecondPos == none; + bool line2Second = c->Second == geoId2 && c->SecondPos == none; + + if (c->Type == Sketcher::Coincident) { + if ((point1First && point2Second) || (point2First && point1Second)) { + // This is the constraint holding the two edges together that are about to be filleted. This constraint + // goes away because the edges will touch the fillet instead. + continue; + } + if (point1First || point2First) { + // Move the coincident constraint to the new corner point + c->First = originalCornerId; + c->FirstPos = start; + } + if (point1Second || point2Second) { + // Move the coincident constraint to the new corner point + c->Second = originalCornerId; + c->SecondPos = start; + } + } else if (c->Type == Sketcher::Horizontal || c->Type == Sketcher::Vertical) { + // Point-to-point horizontal or vertical constraint, move to new corner point + if (point1First || point2First) { + c->First = originalCornerId; + c->FirstPos = start; + } + if (point1Second || point2Second) { + c->Second = originalCornerId; + c->SecondPos = start; + } + } else if (c->Type == Sketcher::Distance || c->Type == Sketcher::DistanceX || c->Type == Sketcher::DistanceY) { + // Point-to-point distance constraint. Move it to the new corner point + if (point1First || point2First) { + c->First = originalCornerId; + c->FirstPos = start; + } + if (point1Second || point2Second) { + c->Second = originalCornerId; + c->SecondPos = start; + } + + // Distance constraint on the line itself. Change it to point-point between the far end of the line + // and the new corner + if (line1First) { + c->FirstPos = (posId1 == start) ? end : start; + c->Second = originalCornerId; + c->SecondPos = start; + } + if (line2First) { + c->FirstPos = (posId2 == start) ? end : start; + c->Second = originalCornerId; + c->SecondPos = start; + } + } else if (c->Type == Sketcher::PointOnObject) { + // The corner to be filleted was touching some other object. + if (point1First || point2First) { + c->First = originalCornerId; + c->FirstPos = start; + } + } else if (c->Type == Sketcher::Equal) { + // Equal length constraints are dicey because the lines are getting shorter. Safer to + // delete them and let the user notice the underconstraint. + if (line1First || line2First || line1Second || line2Second) { + continue; + } + } else if (c->Type == Sketcher::Symmetric) { + // Symmetries should probably be preserved relative to the original corner + if (point1First || point2First) { + c->First = originalCornerId; + c->FirstPos = start; + } else if (point1Second || point2Second) { + c->Second = originalCornerId; + c->SecondPos = start; + } else if (point1Third || point2Third) { + c->Third = originalCornerId; + c->ThirdPos = start; + } + } else if (c->Type == Sketcher::SnellsLaw) { + // Can't imagine any cases where you'd fillet a vertex going through a lens, so let's + // delete to be safe. + continue; + } else if (point1First || point2First || point1Second || point2Second || point1Third || point2Third) { + // Delete any other point-based constraints on the relevant points + continue; + } + + // Default: keep all other constraints + newConstraints.push_back(c->clone()); + } + this->Constraints.setValues(std::move(newConstraints)); +} + int SketchObject::transferConstraints(int fromGeoId, PointPos fromPosId, int toGeoId, PointPos toPosId) { Base::StateLocker lock(managedoperation, true); // no need to check input data validity as this is an sketchobject managed operation. @@ -1553,6 +1719,7 @@ int SketchObject::transferConstraints(int fromGeoId, PointPos fromPosId, int toG if (vals[i]->First == fromGeoId && vals[i]->FirstPos == fromPosId && !(vals[i]->Second == toGeoId && vals[i]->SecondPos == toPosId) && !(toGeoId < 0 && vals[i]->Second <0) ) { + // Nothing guarantees that a tangent can be freely transferred to another coincident point, as // the transfer destination edge most likely won't be intended to be tangent. However, if it is // an end to end point tangency, the user expects it to be substituted by a coincidence constraint. @@ -1607,7 +1774,7 @@ int SketchObject::transferConstraints(int fromGeoId, PointPos fromPosId, int toG return 0; } -int SketchObject::fillet(int GeoId, PointPos PosId, double radius, bool trim) +int SketchObject::fillet(int GeoId, PointPos PosId, double radius, bool trim, bool createCorner) { if (GeoId < 0 || GeoId > getHighestCurveIndex()) return -1; @@ -1628,7 +1795,7 @@ int SketchObject::fillet(int GeoId, PointPos PosId, double radius, bool trim) Base::Vector3d midPnt1 = (lineSeg1->getStartPoint() + lineSeg1->getEndPoint()) / 2 ; Base::Vector3d midPnt2 = (lineSeg2->getStartPoint() + lineSeg2->getEndPoint()) / 2 ; - return fillet(GeoIdList[0], GeoIdList[1], midPnt1, midPnt2, radius, trim); + return fillet(GeoIdList[0], GeoIdList[1], midPnt1, midPnt2, radius, trim, createCorner); } } @@ -1637,14 +1804,19 @@ int SketchObject::fillet(int GeoId, PointPos PosId, double radius, bool trim) int SketchObject::fillet(int GeoId1, int GeoId2, const Base::Vector3d& refPnt1, const Base::Vector3d& refPnt2, - double radius, bool trim) + double radius, bool trim, bool createCorner) { if (GeoId1 < 0 || GeoId1 > getHighestCurveIndex() || GeoId2 < 0 || GeoId2 > getHighestCurveIndex()) return -1; + // If either of the two input lines are locked, don't try to trim since it won't work anyway const Part::Geometry *geo1 = getGeometry(GeoId1); const Part::Geometry *geo2 = getGeometry(GeoId2); + if (trim && (GeometryFacade::getBlocked(geo1) || GeometryFacade::getBlocked(geo2))) { + trim = false; + } + if (geo1->getTypeId() == Part::GeomLineSegment::getClassTypeId() && geo2->getTypeId() == Part::GeomLineSegment::getClassTypeId() ) { const Part::GeomLineSegment *lineSeg1 = static_cast(geo1); @@ -1661,63 +1833,59 @@ int SketchObject::fillet(int GeoId1, int GeoId2, // create arc from known parameters and lines int filletId; - Part::GeomArcOfCircle *arc = Part::createFilletGeometry(lineSeg1, lineSeg2, filletCenter, radius); - if (arc) { - // calculate intersection and distances before we invalidate lineSeg1 and lineSeg2 - if (!find2DLinesIntersection(lineSeg1, lineSeg2, intersection)) { - delete arc; - return -1; - } - dist1.ProjectToLine(arc->getStartPoint(/*emulateCCW=*/true)-intersection, dir1); - dist2.ProjectToLine(arc->getStartPoint(/*emulateCCW=*/true)-intersection, dir2); - Part::Geometry *newgeo = arc; - filletId = addGeometry(newgeo); - if (filletId < 0) { - delete arc; - return -1; - } - } - else + std::unique_ptr arc( + Part::createFilletGeometry(lineSeg1, lineSeg2, filletCenter, radius)); + if (!arc) return -1; + + // calculate intersection and distances before we invalidate lineSeg1 and lineSeg2 + if (!find2DLinesIntersection(lineSeg1, lineSeg2, intersection)) { return -1; + } + + dist1.ProjectToLine(arc->getStartPoint(/*emulateCCW=*/true)-intersection, dir1); + dist2.ProjectToLine(arc->getStartPoint(/*emulateCCW=*/true)-intersection, dir2); + Part::Geometry *newgeo = arc.get(); + filletId = addGeometry(newgeo); if (trim) { PointPos PosId1 = (filletCenter-intersection)*dir1 > 0 ? start : end; PointPos PosId2 = (filletCenter-intersection)*dir2 > 0 ? start : end; - delConstraintOnPoint(GeoId1, PosId1, false); - delConstraintOnPoint(GeoId2, PosId2, false); - Sketcher::Constraint *tangent1 = new Sketcher::Constraint(); - Sketcher::Constraint *tangent2 = new Sketcher::Constraint(); + if (createCorner) { + transferFilletConstraints(GeoId1, PosId1, GeoId2, PosId2); + } else { + delConstraintOnPoint(GeoId1, PosId1, false); + delConstraintOnPoint(GeoId2, PosId2, false); + } - tangent1->Type = Sketcher::Tangent; - tangent1->First = GeoId1; - tangent1->FirstPos = PosId1; - tangent1->Second = filletId; + Sketcher::Constraint tangent1, tangent2; - tangent2->Type = Sketcher::Tangent; - tangent2->First = GeoId2; - tangent2->FirstPos = PosId2; - tangent2->Second = filletId; + tangent1.Type = Sketcher::Tangent; + tangent1.First = GeoId1; + tangent1.FirstPos = PosId1; + tangent1.Second = filletId; + + tangent2.Type = Sketcher::Tangent; + tangent2.First = GeoId2; + tangent2.FirstPos = PosId2; + tangent2.Second = filletId; if (dist1.Length() < dist2.Length()) { - tangent1->SecondPos = start; - tangent2->SecondPos = end; + tangent1.SecondPos = start; + tangent2.SecondPos = end; movePoint(GeoId1, PosId1, arc->getStartPoint(/*emulateCCW=*/true),false,true); movePoint(GeoId2, PosId2, arc->getEndPoint(/*emulateCCW=*/true),false,true); } else { - tangent1->SecondPos = end; - tangent2->SecondPos = start; + tangent1.SecondPos = end; + tangent2.SecondPos = start; movePoint(GeoId1, PosId1, arc->getEndPoint(/*emulateCCW=*/true),false,true); movePoint(GeoId2, PosId2, arc->getStartPoint(/*emulateCCW=*/true),false,true); } - addConstraint(tangent1); - addConstraint(tangent2); - delete tangent1; - delete tangent2; + addConstraint(&tangent1); + addConstraint(&tangent2); } - delete arc; if (noRecomputes) // if we do not have a recompute after the geometry creation, the sketch must be solved to update the DoF of the solver solve(); diff --git a/src/Mod/Sketcher/App/SketchObject.h b/src/Mod/Sketcher/App/SketchObject.h index aeae1bd9a8..0a5957f495 100644 --- a/src/Mod/Sketcher/App/SketchObject.h +++ b/src/Mod/Sketcher/App/SketchObject.h @@ -64,6 +64,14 @@ public: ~SketchObject(); /// Property + /** + The Geometry list contains the non-external Part::Geometry objects in the sketch. The list + may be accessed directly, or indirectly via getInternalGeometry(). + + Many of the methods in this class take geoId and posId parameters. A GeoId is a unique identifier for + geometry in the Sketch. geoId >= 0 means an index in the Geometry list. geoId < 0 refers to sketch + axes and external geometry. posId is a PointPos enum, documented in Constraint.h. + */ Part ::PropertyGeometryList Geometry; Sketcher::PropertyConstraintList Constraints; App ::PropertyLinkSubList ExternalGeometry; @@ -97,9 +105,19 @@ public: \retval bool - true if the geometry is supported */ bool isSupportedGeometry(const Part::Geometry *geo) const; - /// add unspecified geometry + /*! + \brief Add geometry to a sketch + \param geo - geometry to add + \param construction - true for construction lines + \retval int - GeoId of added element + */ int addGeometry(const Part::Geometry *geo, bool construction=false); - /// add unspecified geometry + /*! + \brief Add multiple geometry elements to a sketch + \param geoList - geometry to add + \param construction - true for construction lines + \retval int - GeoId of last added element + */ int addGeometry(const std::vector &geoList, bool construction=false); /*! \brief Deletes indicated geometry (by geoid). @@ -218,11 +236,28 @@ public: int toggleConstruction(int GeoId); int setConstruction(int GeoId, bool on); - /// create a fillet - int fillet(int geoId, PointPos pos, double radius, bool trim=true); + /*! + \brief Create a sketch fillet from the point at the intersection of two lines + \param geoId, pos - one of the (exactly) two coincident endpoints + \param radius - fillet radius + \param trim - if false, leaves the original lines untouched + \param createCorner - keep geoId/pos as a Point and keep as many constraints as possible + \retval - 0 on success, -1 on failure + */ + int fillet(int geoId, PointPos pos, double radius, bool trim=true, bool preserveCorner=false); + /*! + \brief More general form of fillet + \param geoId1, geoId2 - geoId for two lines (which don't necessarily have to coincide) + \param refPnt1, refPnt2 - reference points on the input geometry, used to influence the free fillet variables + \param radius - fillet radius + \param trim - if false, leaves the original lines untouched + \param preserveCorner - if the lines are coincident, place a Point where they meet and keep as many + of the existing constraints as possible + \retval - 0 on success, -1 on failure + */ int fillet(int geoId1, int geoId2, const Base::Vector3d& refPnt1, const Base::Vector3d& refPnt2, - double radius, bool trim=true); + double radius, bool trim=true, bool createCorner=false); /// trim a curve int trim(int geoId, const Base::Vector3d& point); @@ -479,6 +514,19 @@ protected: std::vector supportedGeometry(const std::vector &geoList) const; + /*! + \brief Transfer constraints on lines being filleted. + + Since filleting moves the endpoints of the input geometry, existing constraints may no longer be + sensible. If fillet() was called with preserveCorner=false, the constraints are simply deleted. + But if the lines are coincident and preserveCorner=true, we can preserve most constraints on the + old end points by moving them to the preserved corner, or transforming distance constraints on + straight lines into point-to-point distance constraints. + + \param geoId1, podId1, geoId2, posId2 - The two lines that have just been filleted + */ + void transferFilletConstraints(int geoId1, PointPos posId1, int geoId2, PointPos posId2); + // refactoring functions // check whether constraint may be changed driving status int testDrivingChange(int ConstrId, bool isdriving); diff --git a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp index 2d6cb7f481..315af5a79c 100644 --- a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp +++ b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp @@ -1043,16 +1043,21 @@ PyObject* SketchObjectPy::getAxis(PyObject *args) PyObject* SketchObjectPy::fillet(PyObject *args) { PyObject *pcObj1, *pcObj2; - int geoId1, geoId2, posId1, trim=1; + int geoId1, geoId2, posId1; + int trim=true; + PyObject* createCorner = Py_False; double radius; // Two Lines, radius - if (PyArg_ParseTuple(args, "iiO!O!d|i", &geoId1, &geoId2, &(Base::VectorPy::Type), &pcObj1, &(Base::VectorPy::Type), &pcObj2, &radius, &trim)) { + if (PyArg_ParseTuple(args, "iiO!O!d|iO!", &geoId1, &geoId2, &(Base::VectorPy::Type), &pcObj1, &(Base::VectorPy::Type), &pcObj2, + &radius, &trim, &PyBool_Type, &createCorner)) { + // The i for &trim should probably have been a bool like &createCorner, but we'll leave it an int for backward + // compatibility (and because python will accept a bool there anyway) Base::Vector3d v1 = static_cast(pcObj1)->value(); Base::Vector3d v2 = static_cast(pcObj2)->value(); - if (this->getSketchObjectPtr()->fillet(geoId1, geoId2, v1, v2, radius, trim?true:false)) { + if (this->getSketchObjectPtr()->fillet(geoId1, geoId2, v1, v2, radius, trim, PyObject_IsTrue(createCorner))) { std::stringstream str; str << "Not able to fillet curves with ids : (" << geoId1 << ", " << geoId2 << ") and points (" << v1.x << ", " << v1.y << ", " << v1.z << ") & " << "(" << v2.x << ", " << v2.y << ", " << v2.z << ")"; @@ -1064,8 +1069,9 @@ PyObject* SketchObjectPy::fillet(PyObject *args) PyErr_Clear(); // Point, radius - if (PyArg_ParseTuple(args, "iid|i", &geoId1, &posId1, &radius, &trim)) { - if (this->getSketchObjectPtr()->fillet(geoId1, (Sketcher::PointPos) posId1, radius, trim?true:false)) { + if (PyArg_ParseTuple(args, "iid|iO!", &geoId1, &posId1, &radius, &trim, &PyBool_Type, &createCorner)) { + if (this->getSketchObjectPtr()->fillet(geoId1, (Sketcher::PointPos) posId1, radius, trim, + PyObject_IsTrue(createCorner))) { std::stringstream str; str << "Not able to fillet point with ( geoId: " << geoId1 << ", PointPos: " << posId1 << " )"; PyErr_SetString(PyExc_ValueError, str.str().c_str()); @@ -1075,8 +1081,8 @@ PyObject* SketchObjectPy::fillet(PyObject *args) } PyErr_SetString(PyExc_TypeError, "fillet() method accepts:\n" - "-- int,int,Vector,Vector,float,[int]\n" - "-- int,int,float,[int]\n"); + "-- int,int,Vector,Vector,float,[bool],[bool]\n" + "-- int,int,float,[bool],[bool]\n"); return 0; } diff --git a/src/Mod/Sketcher/App/planegcs/Constraints.cpp b/src/Mod/Sketcher/App/planegcs/Constraints.cpp index 0829e09cde..9260cd2c83 100644 --- a/src/Mod/Sketcher/App/planegcs/Constraints.cpp +++ b/src/Mod/Sketcher/App/planegcs/Constraints.cpp @@ -2085,7 +2085,7 @@ void ConstraintEqualLineLength::errorgrad(double *err, double *grad, double *par // // So here we maintain the very small derivative of 1e-10 when the gradient is under such value, such // that the diagnose function with pivot threshold of 1e-13 treats the value as non-zero and correctly - // detects and can tell appart when a parameter is fully constrained or just locked into a maximum/minimum + // detects and can tell apart when a parameter is fully constrained or just locked into a maximum/minimum if(fabs(*grad) < 1e-10) { double surrogate = 1e-10; if( param == l1.p1.x ) diff --git a/src/Mod/Sketcher/CMakeLists.txt b/src/Mod/Sketcher/CMakeLists.txt index 4be9472e06..740b3ff9ec 100644 --- a/src/Mod/Sketcher/CMakeLists.txt +++ b/src/Mod/Sketcher/CMakeLists.txt @@ -11,6 +11,12 @@ set(Sketcher_Scripts Profiles.py ) +set(Sketcher_TestScripts + SketcherTests/__init__.py + SketcherTests/TestSketchFillet.py + SketcherTests/TestSketcherSolver.py +) + if(BUILD_GUI) list (APPEND Sketcher_Scripts InitGui.py @@ -25,7 +31,7 @@ set(Sketcher_Profiles ) add_custom_target(SketcherScripts ALL - SOURCES ${Sketcher_Scripts} ${Sketcher_Profiles} + SOURCES ${Sketcher_Scripts} ${Sketcher_Profiles} ${Sketcher_TestScripts} ) fc_target_copy_resource(SketcherScripts @@ -33,6 +39,7 @@ fc_target_copy_resource(SketcherScripts ${CMAKE_BINARY_DIR}/Mod/Sketcher ${Sketcher_Scripts} ${Sketcher_Profiles} + ${Sketcher_TestScripts} ) INSTALL( @@ -48,3 +55,10 @@ INSTALL( DESTINATION Mod/Sketcher/ProfileLib ) + +INSTALL( + FILES + ${Sketcher_TestScripts} + DESTINATION + Mod/Sketcher/SketcherTests + ) diff --git a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp index d53caf646a..a491efc692 100644 --- a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp +++ b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp @@ -5016,7 +5016,6 @@ bool CmdSketcherCreateDraftLine::isActive(void) return false; } - // ====================================================================================== namespace SketcherGui { @@ -5064,11 +5063,17 @@ namespace SketcherGui { class DrawSketchHandlerFillet: public DrawSketchHandler { public: - DrawSketchHandlerFillet() : Mode(STATUS_SEEK_First), firstCurve(0) {} + enum FilletType { + SimpleFillet, + ConstraintPreservingFillet + }; + + DrawSketchHandlerFillet(FilletType filletType) : filletType(filletType), Mode(STATUS_SEEK_First), firstCurve(0) {} virtual ~DrawSketchHandlerFillet() { Gui::Selection().rmvSelectionGate(); } + enum SelectMode{ STATUS_SEEK_First, STATUS_SEEK_Second @@ -5135,8 +5140,10 @@ public: int currentgeoid= getHighestCurveIndex(); // create fillet at point try { + bool pointFillet = (filletType == 1); Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Create fillet")); - Gui::cmdAppObjectArgs(sketchgui->getObject(), "fillet(%d,%d,%f)", GeoId, PosId, radius); + Gui::cmdAppObjectArgs(sketchgui->getObject(), "fillet(%d,%d,%f,%s,%s)", GeoId, PosId, radius, "True", + pointFillet ? "True":"False"); if (construction) { Gui::cmdAppObjectArgs(sketchgui->getObject(), "toggleConstruction(%d) ", currentgeoid+1); @@ -5211,11 +5218,13 @@ public: // create fillet between lines try { + bool pointFillet = (filletType == 1); Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Create fillet")); - Gui::cmdAppObjectArgs(sketchgui->getObject(), "fillet(%d,%d,App.Vector(%f,%f,0),App.Vector(%f,%f,0),%f)", + Gui::cmdAppObjectArgs(sketchgui->getObject(), "fillet(%d,%d,App.Vector(%f,%f,0),App.Vector(%f,%f,0),%f,%s,%s)", firstCurve, secondCurve, firstPos.x, firstPos.y, - secondPos.x, secondPos.y, radius); + secondPos.x, secondPos.y, radius, + "True", pointFillet ? "True":"False"); Gui::Command::commitCommand(); } catch (const Base::CADKernelError& e) { @@ -5256,6 +5265,7 @@ public: } protected: + int filletType; SelectMode Mode; int firstCurve; Base::Vector2d firstPos; @@ -5280,7 +5290,7 @@ CmdSketcherCreateFillet::CmdSketcherCreateFillet() void CmdSketcherCreateFillet::activated(int iMsg) { Q_UNUSED(iMsg); - ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerFillet()); + ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerFillet(DrawSketchHandlerFillet::SimpleFillet)); } bool CmdSketcherCreateFillet::isActive(void) @@ -5288,6 +5298,135 @@ bool CmdSketcherCreateFillet::isActive(void) return isCreateGeoActive(getActiveGuiDocument()); } +// ====================================================================================== + +DEF_STD_CMD_A(CmdSketcherCreatePointFillet) + +CmdSketcherCreatePointFillet::CmdSketcherCreatePointFillet() + : Command("Sketcher_CreatePointFillet") +{ + sAppModule = "Sketcher"; + sGroup = QT_TR_NOOP("Sketcher"); + sMenuText = QT_TR_NOOP("Create corner-preserving fillet"); + sToolTipText = QT_TR_NOOP("Fillet that preserves intersection point and most constraints"); + sWhatsThis = "Sketcher_CreatePointFillet"; + sStatusTip = sToolTipText; + sPixmap = "Sketcher_CreateFillet"; + sAccel = ""; + eType = ForEdit; +} + +void CmdSketcherCreatePointFillet::activated(int iMsg) +{ + Q_UNUSED(iMsg); + ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerFillet(DrawSketchHandlerFillet::ConstraintPreservingFillet)); +} + +bool CmdSketcherCreatePointFillet::isActive(void) +{ + return isCreateGeoActive(getActiveGuiDocument()); +} + +/// @brief Macro that declares a new sketcher command class 'CmdSketcherCompCreateFillets' +DEF_STD_CMD_ACLU(CmdSketcherCompCreateFillets) + +/** + * @brief ctor + */ +CmdSketcherCompCreateFillets::CmdSketcherCompCreateFillets() + : Command("Sketcher_CompCreateFillets") +{ + sAppModule = "Sketcher"; + sGroup = QT_TR_NOOP("Sketcher"); + sMenuText = QT_TR_NOOP("Fillets"); + sToolTipText = QT_TR_NOOP("Create a fillet between two lines"); + sWhatsThis = "Sketcher_CompCreateFillets"; + sStatusTip = sToolTipText; + eType = ForEdit; +} + +/** + * @brief Instantiates the fillet handler when the fillet command activated + * @param int iMsg + */ +void CmdSketcherCompCreateFillets::activated(int iMsg) +{ + if (iMsg == 0) { + ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerFillet(DrawSketchHandlerFillet::SimpleFillet)); + } else if (iMsg == 1) { + ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerFillet(DrawSketchHandlerFillet::ConstraintPreservingFillet)); + } else { + return; + } + + // Since the default icon is reset when enabling/disabling the command we have + // to explicitly set the icon of the used command. + Gui::ActionGroup* pcAction = qobject_cast(_pcAction); + QList a = pcAction->actions(); + + assert(iMsg < a.size()); + pcAction->setIcon(a[iMsg]->icon()); +} + +Gui::Action * CmdSketcherCompCreateFillets::createAction(void) +{ + Gui::ActionGroup* pcAction = new Gui::ActionGroup(this, Gui::getMainWindow()); + pcAction->setDropDownMenu(true); + applyCommandData(this->className(), pcAction); + + QAction* oldFillet = pcAction->addAction(QString()); + oldFillet->setIcon(Gui::BitmapFactory().iconFromTheme("Sketcher_CreateFillet")); + + QAction* pointFillet = pcAction->addAction(QString()); + pointFillet->setIcon(Gui::BitmapFactory().iconFromTheme("Sketcher_CreatePointFillet")); + + _pcAction = pcAction; + languageChange(); + + pcAction->setIcon(Gui::BitmapFactory().iconFromTheme("Sketcher_CreateFillet")); + int defaultId = 0; + pcAction->setProperty("defaultAction", QVariant(defaultId)); + + return pcAction; +} + +void CmdSketcherCompCreateFillets::updateAction(int mode) +{ + Q_UNUSED(mode); + Gui::ActionGroup* pcAction = qobject_cast(getAction()); + if (!pcAction) + return; + + QList a = pcAction->actions(); + int index = pcAction->property("defaultAction").toInt(); + a[0]->setIcon(Gui::BitmapFactory().iconFromTheme("Sketcher_CreateFillet")); + a[1]->setIcon(Gui::BitmapFactory().iconFromTheme("Sketcher_CreatePointFillet")); + getAction()->setIcon(a[index]->icon()); +} + +void CmdSketcherCompCreateFillets::languageChange() +{ + Command::languageChange(); + + if (!_pcAction) + return; + Gui::ActionGroup* pcAction = qobject_cast(_pcAction); + QList a = pcAction->actions(); + + QAction* oldFillet = a[0]; + oldFillet->setText(QApplication::translate("CmdSketcherCompCreateFillets","Sketch fillet")); + oldFillet->setToolTip(QApplication::translate("Sketcher_CreateFillet","Creates a radius between two lines")); + oldFillet->setStatusTip(QApplication::translate("Sketcher_CreateFillet","Creates a radius between two lines")); + QAction* pointFillet = a[1]; + pointFillet->setText(QApplication::translate("CmdSketcherCompCreateFillets","Constraint-preserving sketch fillet")); + pointFillet->setToolTip(QApplication::translate("Sketcher_CreatePointFillet","Fillet that preserves constraints and intersection point")); + pointFillet->setStatusTip(QApplication::translate("Sketcher_CreatePointFillet","Fillet that preserves constraints and intersection point")); +} + +bool CmdSketcherCompCreateFillets::isActive(void) +{ + return isCreateGeoActive(getActiveGuiDocument()); +} // ====================================================================================== @@ -6874,7 +7013,9 @@ void CreateSketcherCommandsCreateGeo(void) rcCmdMgr.addCommand(new CmdSketcherCreateOctagon()); rcCmdMgr.addCommand(new CmdSketcherCreateRegularPolygon()); rcCmdMgr.addCommand(new CmdSketcherCreateSlot()); + rcCmdMgr.addCommand(new CmdSketcherCompCreateFillets()); rcCmdMgr.addCommand(new CmdSketcherCreateFillet()); + rcCmdMgr.addCommand(new CmdSketcherCreatePointFillet()); //rcCmdMgr.addCommand(new CmdSketcherCreateText()); //rcCmdMgr.addCommand(new CmdSketcherCreateDraftLine()); rcCmdMgr.addCommand(new CmdSketcherTrimming()); diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandler.cpp b/src/Mod/Sketcher/Gui/DrawSketchHandler.cpp index 7db982dcb4..16d32274bd 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandler.cpp +++ b/src/Mod/Sketcher/Gui/DrawSketchHandler.cpp @@ -29,6 +29,9 @@ # include # include # include +# if QT_VERSION >= 0x050000 +# include +# endif # include #endif // #ifndef _PreComp_ @@ -113,12 +116,15 @@ void DrawSketchHandler::setSvgCursor(const QString & cursorName, int x, int y, c qreal pRatio = devicePixelRatio(); bool isRatioOne = (pRatio == 1.0); qreal defaultCursorSize = isRatioOne ? 64 : 32; -#if defined(Q_OS_WIN32) || defined(Q_OS_MAC) qreal hotX = x; qreal hotY = y; -#else - qreal hotX = x * pRatio; - qreal hotY = y * pRatio; +#if QT_VERSION >= 0x050000 +#if !defined(Q_OS_WIN32) && !defined(Q_OS_MAC) + if (qGuiApp->platformName() == QLatin1String("xcb")) { + hotX *= pRatio; + hotY *= pRatio; + } +#endif #endif qreal cursorSize = defaultCursorSize * pRatio; @@ -155,12 +161,15 @@ void DrawSketchHandler::setCursor(const QPixmap &p,int x,int y, bool autoScale) #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) p1.setDevicePixelRatio(pRatio); #endif -#if defined(Q_OS_WIN32) || defined(Q_OS_MAC) qreal hotX = x; qreal hotY = y; -#else - qreal hotX = x * pRatio; - qreal hotY = y * pRatio; +#if QT_VERSION >= 0x050000 +#if !defined(Q_OS_WIN32) && !defined(Q_OS_MAC) + if (qGuiApp->platformName() == QLatin1String("xcb")) { + hotX *= pRatio; + hotY *= pRatio; + } +#endif #endif cursor = QCursor(p1, hotX, hotY); } else { diff --git a/src/Mod/Sketcher/Gui/PreCompiled.h b/src/Mod/Sketcher/Gui/PreCompiled.h index 9fe66a48d5..4521f5ce15 100644 --- a/src/Mod/Sketcher/Gui/PreCompiled.h +++ b/src/Mod/Sketcher/Gui/PreCompiled.h @@ -95,6 +95,7 @@ # include #endif +# include # include #include #include diff --git a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc index 6ef59d52a5..4dcefcaaf7 100644 --- a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc +++ b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc @@ -128,6 +128,7 @@ icons/geometry/Sketcher_CreateElliptical_Arc.svg icons/geometry/Sketcher_CreateElliptical_Arc_Constr.svg icons/geometry/Sketcher_CreateFillet.svg + icons/geometry/Sketcher_CreatePointFillet.svg icons/geometry/Sketcher_CreateHeptagon.svg icons/geometry/Sketcher_CreateHeptagon_Constr.svg icons/geometry/Sketcher_CreateHexagon.svg @@ -266,4 +267,4 @@ translations/Sketcher_zh-CN.qm translations/Sketcher_zh-TW.qm - \ No newline at end of file + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreatePointFillet.svg b/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreatePointFillet.svg new file mode 100644 index 0000000000..84e0baa261 --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreatePointFillet.svg @@ -0,0 +1,342 @@ + + + Sketcher_PointFillet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Sketcher_PointFillet + + + [bitacovir] + + + Sketcher_CreateFillet + 31-01-2021 + + + + FreeCAD + + + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + Based on a wmayer's design and [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/SketchMirrorDialog.cpp b/src/Mod/Sketcher/Gui/SketchMirrorDialog.cpp index 149f9abc48..c26801db82 100644 --- a/src/Mod/Sketcher/Gui/SketchMirrorDialog.cpp +++ b/src/Mod/Sketcher/Gui/SketchMirrorDialog.cpp @@ -49,7 +49,6 @@ SketchMirrorDialog::SketchMirrorDialog(void) SketchMirrorDialog::~SketchMirrorDialog() { - delete ui; } void SketchMirrorDialog::accept() diff --git a/src/Mod/Sketcher/Gui/SketchMirrorDialog.h b/src/Mod/Sketcher/Gui/SketchMirrorDialog.h index a59a3ecaba..debea06eec 100644 --- a/src/Mod/Sketcher/Gui/SketchMirrorDialog.h +++ b/src/Mod/Sketcher/Gui/SketchMirrorDialog.h @@ -43,7 +43,7 @@ public: void accept(); private: - Ui_SketchMirrorDialog* ui; + std::unique_ptr ui; }; } diff --git a/src/Mod/Sketcher/Gui/SketchOrientationDialog.cpp b/src/Mod/Sketcher/Gui/SketchOrientationDialog.cpp index 3b4f86e731..329ffe0276 100644 --- a/src/Mod/Sketcher/Gui/SketchOrientationDialog.cpp +++ b/src/Mod/Sketcher/Gui/SketchOrientationDialog.cpp @@ -52,7 +52,6 @@ SketchOrientationDialog::SketchOrientationDialog(void) SketchOrientationDialog::~SketchOrientationDialog() { - delete ui; } void SketchOrientationDialog::accept() diff --git a/src/Mod/Sketcher/Gui/SketchOrientationDialog.h b/src/Mod/Sketcher/Gui/SketchOrientationDialog.h index a2782c0e4e..f8629cf2f0 100644 --- a/src/Mod/Sketcher/Gui/SketchOrientationDialog.h +++ b/src/Mod/Sketcher/Gui/SketchOrientationDialog.h @@ -46,7 +46,7 @@ protected Q_SLOTS: void onPreview(); private: - Ui_SketchOrientationDialog* ui; + std::unique_ptr ui; }; } diff --git a/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.cpp b/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.cpp index cc37c5f59c..fdd23e42df 100644 --- a/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.cpp +++ b/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.cpp @@ -53,7 +53,6 @@ SketchRectangularArrayDialog::SketchRectangularArrayDialog(void) SketchRectangularArrayDialog::~SketchRectangularArrayDialog() { - delete ui; } void SketchRectangularArrayDialog::accept() diff --git a/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.h b/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.h index 620c7e7796..602fb9fdc6 100644 --- a/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.h +++ b/src/Mod/Sketcher/Gui/SketchRectangularArrayDialog.h @@ -48,7 +48,7 @@ public: protected: void updateValues(void); private: - Ui_SketchRectangularArrayDialog* ui; + std::unique_ptr ui; }; } diff --git a/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.cpp b/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.cpp index bf7dc3ef88..91bbe2bd5a 100644 --- a/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.cpp +++ b/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.cpp @@ -49,7 +49,6 @@ SketcherRegularPolygonDialog::SketcherRegularPolygonDialog(void) SketcherRegularPolygonDialog::~SketcherRegularPolygonDialog() { - delete ui; } void SketcherRegularPolygonDialog::accept() diff --git a/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.h b/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.h index f450693620..1a21de3945 100644 --- a/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.h +++ b/src/Mod/Sketcher/Gui/SketcherRegularPolygonDialog.h @@ -44,7 +44,7 @@ public: protected: void updateValues(void); private: - Ui_SketcherRegularPolygonDialog* ui; + std::unique_ptr ui; }; } diff --git a/src/Mod/Sketcher/Gui/SketcherSettings.cpp b/src/Mod/Sketcher/Gui/SketcherSettings.cpp index e63f8e2b3b..02e41de8fe 100644 --- a/src/Mod/Sketcher/Gui/SketcherSettings.cpp +++ b/src/Mod/Sketcher/Gui/SketcherSettings.cpp @@ -62,7 +62,6 @@ SketcherSettings::SketcherSettings(QWidget* parent) SketcherSettings::~SketcherSettings() { // no need to delete child widgets, Qt does it all for us - delete ui; } void SketcherSettings::saveSettings() @@ -141,7 +140,6 @@ SketcherSettingsDisplay::SketcherSettingsDisplay(QWidget* parent) SketcherSettingsDisplay::~SketcherSettingsDisplay() { // no need to delete child widgets, Qt does it all for us - delete ui; } void SketcherSettingsDisplay::saveSettings() @@ -241,7 +239,6 @@ SketcherSettingsColors::SketcherSettingsColors(QWidget* parent) SketcherSettingsColors::~SketcherSettingsColors() { // no need to delete child widgets, Qt does it all for us - delete ui; } void SketcherSettingsColors::saveSettings() diff --git a/src/Mod/Sketcher/Gui/SketcherSettings.h b/src/Mod/Sketcher/Gui/SketcherSettings.h index 4d94156920..003c0fe0b9 100644 --- a/src/Mod/Sketcher/Gui/SketcherSettings.h +++ b/src/Mod/Sketcher/Gui/SketcherSettings.h @@ -25,6 +25,7 @@ #define SKETCHERGUI_SKETCHERSETTINGS_H #include +#include namespace SketcherGui { class Ui_SketcherSettings; @@ -50,7 +51,7 @@ protected: void changeEvent(QEvent *e); private: - Ui_SketcherSettings* ui; + std::unique_ptr ui; SketcherGeneralWidget* form; }; @@ -76,7 +77,7 @@ private Q_SLOTS: void onBtnTVApplyClicked(bool); private: - Ui_SketcherSettingsDisplay* ui; + std::unique_ptr ui; }; /** @@ -98,7 +99,7 @@ protected: void changeEvent(QEvent *e); private: - Ui_SketcherSettingsColors* ui; + std::unique_ptr ui; }; } // namespace SketcherGui diff --git a/src/Mod/Sketcher/Gui/TaskSketcherConstrains.cpp b/src/Mod/Sketcher/Gui/TaskSketcherConstrains.cpp index 72378330df..1134dcce36 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherConstrains.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherConstrains.cpp @@ -634,13 +634,13 @@ void ConstraintView::swapNamedOfSelectedItems() // ---------------------------------------------------------------------------- -TaskSketcherConstrains::TaskSketcherConstrains(ViewProviderSketch *sketchView) - : TaskBox(Gui::BitmapFactory().pixmap("document-new"),tr("Constraints"),true, 0) - , sketchView(sketchView), inEditMode(false) +TaskSketcherConstrains::TaskSketcherConstrains(ViewProviderSketch *sketchView) : + TaskBox(Gui::BitmapFactory().pixmap("document-new"), tr("Constraints"), true, 0), + sketchView(sketchView), inEditMode(false), + ui(new Ui_TaskSketcherConstrains) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskSketcherConstrains(); ui->setupUi(proxy); ui->listWidgetConstraints->setSelectionMode(QAbstractItemView::ExtendedSelection); ui->listWidgetConstraints->setEditTriggers(QListWidget::EditKeyPressed); @@ -700,7 +700,6 @@ TaskSketcherConstrains::~TaskSketcherConstrains() this->ui->filterInternalAlignment->onSave(); this->ui->extendedInformation->onSave(); connectionConstraintsChanged.disconnect(); - delete ui; } void TaskSketcherConstrains::onSelectionChanged(const Gui::SelectionChanges& msg) diff --git a/src/Mod/Sketcher/Gui/TaskSketcherConstrains.h b/src/Mod/Sketcher/Gui/TaskSketcherConstrains.h index fc52458bf1..27864995d9 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherConstrains.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherConstrains.h @@ -101,7 +101,7 @@ protected: private: QWidget* proxy; bool inEditMode; - Ui_TaskSketcherConstrains* ui; + std::unique_ptr ui; }; } //namespace SketcherGui diff --git a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp index d833fba3e6..4b6082bff3 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp @@ -342,7 +342,6 @@ TaskSketcherElements::~TaskSketcherElements() } connectionElementsChanged.disconnect(); - delete ui; } void TaskSketcherElements::onSelectionChanged(const Gui::SelectionChanges& msg) diff --git a/src/Mod/Sketcher/Gui/TaskSketcherElements.h b/src/Mod/Sketcher/Gui/TaskSketcherElements.h index 86e69c47e3..0eff5aa1e3 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherElements.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherElements.h @@ -139,7 +139,7 @@ protected: private: QWidget* proxy; - Ui_TaskSketcherElements* ui; + std::unique_ptr ui; int focusItemIndex; int previouslySelectedItemIndex; diff --git a/src/Mod/Sketcher/Gui/TaskSketcherGeneral.cpp b/src/Mod/Sketcher/Gui/TaskSketcherGeneral.cpp index cb42687c45..1799bf1f2e 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherGeneral.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherGeneral.cpp @@ -69,7 +69,6 @@ SketcherGeneralWidget::SketcherGeneralWidget(QWidget *parent) SketcherGeneralWidget::~SketcherGeneralWidget() { - delete ui; } bool SketcherGeneralWidget::eventFilter(QObject *object, QEvent *event) diff --git a/src/Mod/Sketcher/Gui/TaskSketcherGeneral.h b/src/Mod/Sketcher/Gui/TaskSketcherGeneral.h index 1cdd92923a..2c267714f4 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherGeneral.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherGeneral.h @@ -28,8 +28,6 @@ #include #include -class Ui_TaskSketcherGeneral; - namespace App { class Property; } @@ -40,6 +38,7 @@ class ViewProvider; namespace SketcherGui { +class Ui_TaskSketcherGeneral; class ViewProviderSketch; class SketcherGeneralWidget : public QWidget @@ -76,7 +75,7 @@ protected: void changeEvent(QEvent *e); private: - Ui_TaskSketcherGeneral* ui; + std::unique_ptr ui; }; class TaskSketcherGeneral : public Gui::TaskView::TaskBox, diff --git a/src/Mod/Sketcher/Gui/TaskSketcherMessages.cpp b/src/Mod/Sketcher/Gui/TaskSketcherMessages.cpp index 730256a978..14a3b4cb21 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherMessages.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherMessages.cpp @@ -47,13 +47,13 @@ using namespace SketcherGui; using namespace Gui::TaskView; namespace bp = boost::placeholders; -TaskSketcherMessages::TaskSketcherMessages(ViewProviderSketch *sketchView) - : TaskBox(Gui::BitmapFactory().pixmap("document-new"),tr("Solver messages"),true, 0) - , sketchView(sketchView) +TaskSketcherMessages::TaskSketcherMessages(ViewProviderSketch *sketchView) : + TaskBox(Gui::BitmapFactory().pixmap("document-new"), tr("Solver messages"), true, 0), + sketchView(sketchView), + ui(new Ui_TaskSketcherMessages) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskSketcherMessages(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -90,7 +90,6 @@ TaskSketcherMessages::~TaskSketcherMessages() { connectionSetUp.disconnect(); connectionSolved.disconnect(); - delete ui; } void TaskSketcherMessages::slotSetUp(QString msg) diff --git a/src/Mod/Sketcher/Gui/TaskSketcherMessages.h b/src/Mod/Sketcher/Gui/TaskSketcherMessages.h index 09193959d6..30aae3fbb0 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherMessages.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherMessages.h @@ -63,7 +63,7 @@ protected: private: QWidget* proxy; - Ui_TaskSketcherMessages* ui; + std::unique_ptr ui; }; } //namespace SketcherGui diff --git a/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.cpp b/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.cpp index c5f6b4edb3..1b59b7e1b9 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.cpp @@ -60,13 +60,13 @@ using namespace SketcherGui; using namespace Gui::TaskView; -TaskSketcherSolverAdvanced::TaskSketcherSolverAdvanced(ViewProviderSketch *sketchView) - : TaskBox(Gui::BitmapFactory().pixmap("document-new"),tr("Advanced solver control"),true, 0) - , sketchView(sketchView) +TaskSketcherSolverAdvanced::TaskSketcherSolverAdvanced(ViewProviderSketch *sketchView) : + TaskBox(Gui::BitmapFactory().pixmap("document-new"), tr("Advanced solver control"), true, 0), + sketchView(sketchView), + ui(new Ui_TaskSketcherSolverAdvanced) { // we need a separate container widget to add all controls to proxy = new QWidget(this); - ui = new Ui_TaskSketcherSolverAdvanced(); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); @@ -90,7 +90,6 @@ TaskSketcherSolverAdvanced::TaskSketcherSolverAdvanced(ViewProviderSketch *sketc TaskSketcherSolverAdvanced::~TaskSketcherSolverAdvanced() { - delete ui; } void TaskSketcherSolverAdvanced::updateDefaultMethodParameters(void) diff --git a/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.h b/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.h index d55473d8b4..2e2dcfde1b 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherSolverAdvanced.h @@ -76,7 +76,7 @@ protected: private: QWidget* proxy; - Ui_TaskSketcherSolverAdvanced* ui; + std::unique_ptr ui; }; } //namespace SketcherGui diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp index 95b5b458de..72a70c40d9 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp @@ -986,6 +986,7 @@ bool ViewProviderSketch::mouseButtonPressed(int Button, bool pressed, const SbVe << "Sketcher_CreateRectangle" << "Sketcher_CreateHexagon" << "Sketcher_CreateFillet" + << "Sketcher_CreatePointFillet" << "Sketcher_Trimming" << "Sketcher_Extend" << "Sketcher_External" @@ -6493,6 +6494,16 @@ void ViewProviderSketch::UpdateSolverInformation() .arg(tr("(click to select)")) .arg(appendRedundantMsg(getSketchObject()->getLastRedundant()))); } + + QString partiallyRedundantString; + + if(hasPartiallyRedundant) { + partiallyRedundantString = QString::fromLatin1("
%1%2
%3

") + .arg(tr("Sketch contains partially redundant constraints ")) + .arg(tr("(click to select)")) + .arg(appendPartiallyRedundantMsg(getSketchObject()->getLastPartiallyRedundant())); + } + if (getSketchObject()->getLastSolverStatus() == 0) { if (dofs == 0) { // color the sketch as fully constrained if it has geometry (other than the axes) @@ -6500,31 +6511,19 @@ void ViewProviderSketch::UpdateSolverInformation() edit->FullyConstrained = true; if (!hasRedundancies) { - signalSetUp(QString::fromLatin1("%1").arg(tr("Fully constrained sketch"))); + signalSetUp(QString::fromLatin1("%1 %2").arg(tr("Fully constrained sketch")).arg(partiallyRedundantString)); } } else if (!hasRedundancies) { QString infoString; if (dofs == 1) - infoString = tr("Under-constrained sketch with 1 degree of freedom. %1") - .arg(hasPartiallyRedundant? - QString::fromLatin1("
%1%2
%3

") - .arg(tr("Sketch contains partially redundant constraints ")) - .arg(tr("(click to select)")) - .arg(appendPartiallyRedundantMsg(getSketchObject()->getLastPartiallyRedundant())) - : QString()); + signalSetUp(tr("Under-constrained sketch with 1 degree of freedom. %1") + .arg(partiallyRedundantString)); else - infoString = tr("Under-constrained sketch with %1 degrees of freedom. %2") + signalSetUp(tr("Under-constrained sketch with %1 degrees of freedom. %2") .arg(dofs) - .arg(hasPartiallyRedundant? - QString::fromLatin1("
%1%2
%3

") - .arg(tr("Sketch contains partially redundant constraints ")) - .arg(tr("(click to select)")) - .arg(appendPartiallyRedundantMsg(getSketchObject()->getLastPartiallyRedundant())) - : QString()); - - signalSetUp(infoString); + .arg(partiallyRedundantString)); } signalSolved(QString::fromLatin1("%1").arg(tr("Solved in %1 sec").arg(getSketchObject()->getLastSolveTime()))); diff --git a/src/Mod/Sketcher/Gui/Workbench.cpp b/src/Mod/Sketcher/Gui/Workbench.cpp index 104784ee24..748c423323 100644 --- a/src/Mod/Sketcher/Gui/Workbench.cpp +++ b/src/Mod/Sketcher/Gui/Workbench.cpp @@ -232,6 +232,23 @@ inline void SketcherAddWorkspaceRegularPolygon(Gui::ToolBarIte geom << "Sketcher_CompCreateRegularPolygon"; } + +template +void SketcherAddWorkspaceFillets(T& geom); + +template <> +inline void SketcherAddWorkspaceFillets(Gui::MenuItem& geom) +{ + geom << "Sketcher_CreateFillet" + << "Sketcher_CreatePointFillet"; +} + +template <> +inline void SketcherAddWorkspaceFillets(Gui::ToolBarItem& geom) +{ + geom << "Sketcher_CompCreateFillets"; +} + template inline void SketcherAddWorkbenchGeometries(T& geom) { @@ -243,9 +260,9 @@ inline void SketcherAddWorkbenchGeometries(T& geom) << "Sketcher_CreateRectangle"; SketcherAddWorkspaceRegularPolygon(geom); geom << "Sketcher_CreateSlot" - << "Separator" - << "Sketcher_CreateFillet" - << "Sketcher_Trimming" + << "Separator"; + SketcherAddWorkspaceFillets(geom); + geom << "Sketcher_Trimming" << "Sketcher_Extend" << "Sketcher_External" << "Sketcher_CarbonCopy" diff --git a/src/Mod/Sketcher/SketcherTests/TestSketchFillet.py b/src/Mod/Sketcher/SketcherTests/TestSketchFillet.py new file mode 100644 index 0000000000..2c076613dc --- /dev/null +++ b/src/Mod/Sketcher/SketcherTests/TestSketchFillet.py @@ -0,0 +1,292 @@ +# (c) Emmanuel O'Brien 2021 LGPL * +# * +# This file is part of the FreeCAD CAx development system. * +# * +# 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. * +# * +# 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 Library General Public License for more details. * +# * +# You should have received a copy of the GNU Library General Public * +# License along with FreeCAD; if not, write to the Free Software * +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# USA * +#************************************************************************** + + +import FreeCAD, math, os, sys, unittest, Part, Sketcher +from .TestSketcherSolver import CreateRectangleSketch +App = FreeCAD + +def VShape(SketchFeature): + # Simple inverted V shape + SketchFeature.addGeometry(Part.LineSegment(App.Vector(0,0,0),App.Vector(1,1,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,1,-1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,1,0,2,1)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(1,1,0),App.Vector(2,0,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,1,0,2)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',1,1,1,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',1,1,1,2,-1)) + SketchFeature.fillet(0, 2, 0.25, True, True) + +def BoxCircle(SketchFeature): + # Square with a circle centered at the upper right corner + top_edge = int(SketchFeature.GeometryCount) + right_edge = top_edge + 1 + CreateRectangleSketch(SketchFeature, [0, 0], [2, 2]) + top_midpoint = App.Vector(1, 2, 0) + right_midpoint = App.Vector(2, 1, 0) + circle = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.Circle(App.Vector(0,0,0), App.Vector(0,0,1), 1),False) + SketchFeature.addConstraint(Sketcher.Constraint('Radius',circle,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',top_edge,2,circle,3)) + # Since the circle center is coincident with the corner, there are three coincident points + # and thus the simpler fillet() call would get confused. Instead we'll need to point at the two + # lines and their midpoints + SketchFeature.fillet(top_edge, right_edge, top_midpoint, right_midpoint, 0.25, True, True) + +def PointOnObject(SketchFeature): + # Square with the upper right corner touching the edge of a circle + top_edge = int(SketchFeature.GeometryCount) + right_edge = top_edge + 1 + CreateRectangleSketch(SketchFeature, [0, 0], [2, 2]) + circle = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.Circle(App.Vector(12,3,0), App.Vector(0,0,1), 1),False) + SketchFeature.addConstraint(Sketcher.Constraint('Radius',circle,1)) + SketchFeature.addConstraint(Sketcher.Constraint('PointOnObject',top_edge,2,circle)) + SketchFeature.fillet(top_edge, 2, 0.25, True, True) + +class TestSketchFillet(unittest.TestCase): + def setUp(self): + self.Doc = FreeCAD.newDocument("TestSketchFillet") + + def testBasicFillet(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'BasicFillet') + CreateRectangleSketch(SketchFeature, [0, 0], [2, 2]) + SketchFeature.fillet(0,2, 0.25, True, True) + self.assertAlmostEqual(SketchFeature.Geometry[4].Radius, 0.25) + + # Fillets can be made even between unconnected lines + def testUnconnected(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'Unconnected') + # Inverted open V + SketchFeature.addGeometry(Part.LineSegment(App.Vector(0,0,0),App.Vector(1,1,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,1,-1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,1,0,2,1)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(2.1,1,0),App.Vector(3,0,0))) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',1,1, 1,2, 0.9)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',1,1, 1,2, -1)) + + SketchFeature.addGeometry(Part.LineSegment(App.Vector(3,3,0),App.Vector(5,3,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Equal',0,2)) + self.Doc.recompute() + self.assertAlmostEqual(SketchFeature.Geometry[2].length(), math.sqrt(2)) + + SketchFeature.fillet(0,1, App.Vector(0.5,0.5,0), App.Vector(2.55,0.5,0), 0.25, True, True) + # Make sure a fillet was created + self.assertAlmostEqual(SketchFeature.Geometry[3].Radius, 0.25) + + self.Doc.recompute() + # Third line's length shouldn't have changed + self.assertAlmostEqual(SketchFeature.Geometry[2].length(), math.sqrt(2)) + # First line should be shorter + self.assertNotAlmostEqual(SketchFeature.Geometry[0].length(), math.sqrt(2)) + + # Curved lines can also be filleted + def testCurve(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'Curve') + SketchFeature.addGeometry(Part.LineSegment(App.Vector(0,0,0),App.Vector(1,1,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,1,-1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,1,0,2,1)) + arc = Part.ArcOfCircle(Part.Circle(App.Vector(3,0,0),App.Vector(0,0,1),3),0,-1) + SketchFeature.addGeometry(arc) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',1,3, -1,1, -3)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',1,3, -1,1, 0)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,1, 0,2)) + self.Doc.recompute() + + SketchFeature.fillet(0,1, App.Vector(0.5,0.5,0), App.Vector(0.8,0.3,0), 0.25, True, True) + self.assertAlmostEqual(SketchFeature.Geometry[2].Radius, 0.25) + + def testUnconnectedCurve(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'UnconnectedCurve') + SketchFeature.addGeometry(Part.LineSegment(App.Vector(0,0,0),App.Vector(1,1,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,1,-1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,1,0,2,1)) + arc = Part.ArcOfCircle(Part.Circle(App.Vector(3,1,0),App.Vector(0,0,1), 1.75), -3.14, -2.17) + SketchFeature.addGeometry(arc) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',-1,1, 1,3, 3.0)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',-1,1, 1,3, 1.0)) + SketchFeature.addConstraint(Sketcher.Constraint('Distance',0,2, 1,1, 0.25)) + self.Doc.recompute() + + #SketchFeature.fillet(0,1, App.Vector(0.75,0.75,0), App.Vector(1.22,0.66,0), 0.25, True, True) + # Make sure the fillet happened + #self.Doc.recompute() + #self.assertAlmostEqual(SketchFeature.Geometry[2].Radius, 0.25) + + # The following tests are mostly about verifying that transferFilletConstraints + # does the right thing with pre-existing constraints when a fillet is created. + + # Make sure the original corner is preserved when filleting + def testOriginalCorner(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'OriginalCorner') + VShape(SketchFeature) + self.Doc.recompute() + # If fillet() ever gets refactored such that the corner gets added after + # the arc, then getPoint(3,1) might break. A more general approach would + # be to iterate over the geometry list and find the bare vertex. + self.assertAlmostEqual(App.Vector(1,1,0), SketchFeature.getPoint(3,1)) + + # Make sure coincident constraints get moved to the old corner location + def testCoincident(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'Coincident') + BoxCircle(SketchFeature) + self.Doc.recompute() + # Make sure the circle center is still at the old corner + self.assertAlmostEqual(App.Vector(2,2,0).distanceToPoint(SketchFeature.getPoint(4,3)), 0.0) + + # Point-to-point horizontal and vertical constraints should also move to the old corner + def testHorizontalVertical(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'HorizontalVertical') + # Inverted V + SketchFeature.addGeometry(Part.LineSegment(App.Vector(0,0,0),App.Vector(1,1,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,1,-1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,1,0,2,1)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(1,1,0),App.Vector(2,0,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,1,0,2)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',1,1,1,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',1,1,1,2,-1)) + + SketchFeature.addGeometry(Part.Point(App.Vector(2,1))) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',0,2, 2,1)) + SketchFeature.addGeometry(Part.Point(App.Vector(1,2))) + SketchFeature.addConstraint(Sketcher.Constraint('Vertical',1,1, 3,1)) + + SketchFeature.fillet(0, 2, 0.25, True, True) + + # Verify the constraint moved to the original corner + found_horizontal = False + found_vertical = False + for c in SketchFeature.Constraints: + if c.Type == 'Horizontal' and c.First == 5 and c.FirstPos == 1 and \ + c.Second == 2 and c.SecondPos == 1: + found_horizontal = True + elif c.Type == 'Vertical' and c.First == 5 and c.FirstPos == 1 and \ + c.Second == 3 and c.SecondPos == 1: + found_vertical = True + + self.assertTrue(found_horizontal) + self.assertTrue(found_vertical) + + # Distance constraints to the old corner point should be preserved + def testDistance(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'Distance') + # We'll end up implicitly testing line length constraints as well since that's + # what CreateRectangleSketch uses to enforce side length. If the side length doesn't + # switch to a point-to-point distance constraint with the original corner as expected, + # the point won't end up at its expected destination. + CreateRectangleSketch(SketchFeature, [0, 0], [2, 2]) + + point = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.Point(App.Vector(3,2,0))) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,2, point,1, 1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,2, point,1, 0)) + + self.Doc.recompute() + self.assertTrue(SketchFeature.FullyConstrained) + SketchFeature.fillet(0, 2, 0.25, True, True) + SketchFeature.addConstraint(Sketcher.Constraint('Radius',5,0.25)) + self.Doc.recompute() + # If any constraints disappeared then we won't be fully constrained + self.assertTrue(SketchFeature.FullyConstrained) + # Make sure the point is to the right of the original corner as expected + self.assertAlmostEqual(SketchFeature.getPoint(point,1), App.Vector(3, 2, 0)) + + # Make sure point on object constraints get moved to the old corner location + def testPointOnObject(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'PointOnObject') + PointOnObject(SketchFeature) + self.Doc.recompute() + # Make sure the circle center is one radius away from the old corner + self.assertAlmostEqual(App.Vector(2,2,0).distanceToPoint(SketchFeature.getPoint(4,3)), 1.0) + + # Make sure colinearity doesn't get dropped + def testTangent(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'Tangent') + + # Setup the geometry + first_line = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(0,0,0),App.Vector(1,1,0))) + # Anchor at the origin + SketchFeature.addConstraint(Sketcher.Constraint('Coincident', 0, 1, -1, 1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',first_line,2,-1,1,-1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',first_line,2,-1,1,-1)) + + second_line = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(1,1,0),App.Vector(2,0,0))) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',second_line,2,-1,1,-2)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',first_line,2,second_line,1)) + tangent_line = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(2,2,0), App.Vector(3,3,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Tangent', tangent_line, first_line)) + SketchFeature.addConstraint(Sketcher.Constraint('Distance', tangent_line, 1.41421356237)) + SketchFeature.fillet(first_line, 2, 0.25, True, True) + + self.Doc.recompute() + # Move the tangent line and see if it's aimed right + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',tangent_line,1,-1,1,-4)) + self.Doc.recompute() + + # The first endpoint should now be at 4,4 + self.assertAlmostEqual(App.Vector(4,4,0).distanceToPoint(SketchFeature.getPoint(tangent_line, 1)), 0.0) + + # We expect the other end of the tangent line to be at 5,5, but I think 3,3 also satisfies + # the colinearity constraint + try: + self.assertAlmostEqual(App.Vector(3,3,0).distanceToPoint(SketchFeature.getPoint(tangent_line, 2)), 0.0) + except AssertionError: + self.assertAlmostEqual(App.Vector(5,5,0).distanceToPoint(SketchFeature.getPoint(tangent_line, 2)), 0.0) + + def testSymmetric(self): + SketchFeature = self.Doc.addObject('Sketcher::SketchObject', 'Symmetric') + # Inverted V + SketchFeature.addGeometry(Part.LineSegment(App.Vector(0,0,0),App.Vector(1,1,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,1,-1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,1,0,2,1)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(1,1,0),App.Vector(2,0,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,1,0,2)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',1,1,1,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',1,1,1,2,-1)) + + # Mirror point + SketchFeature.addGeometry(Part.Point(App.Vector(3,2,0))) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',-1,1, 2,1, 3)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',-1,1, 2,1, 2)) + + # Point that will mirror the apex of the V + SketchFeature.addGeometry(Part.Point(App.Vector(4,2,0))) + + SketchFeature.addConstraint(Sketcher.Constraint('Symmetric',0,2, 3,1, 2,1)) + self.Doc.recompute() + + SketchFeature.fillet(0, 2, 0.25, True, True) + self.Doc.recompute() + self.assertAlmostEqual(App.Vector(5,3,0).distanceToPoint(SketchFeature.getPoint(3, 1)), 0.0) + + def tearDown(self): + # comment out to omit closing document for debugging + FreeCAD.closeDocument("TestSketchFillet") + pass diff --git a/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py b/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py new file mode 100644 index 0000000000..90a0e42cf0 --- /dev/null +++ b/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py @@ -0,0 +1,216 @@ +# (c) Juergen Riegel (FreeCAD@juergen-riegel.net) 2011 LGPL * +# * +# This file is part of the FreeCAD CAx development system. * +# * +# 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. * +# * +# 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 Library General Public License for more details. * +# * +# You should have received a copy of the GNU Library General Public * +# License along with FreeCAD; if not, write to the Free Software * +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# USA * +#************************************************************************** + + +import FreeCAD, os, sys, unittest, Part, Sketcher +App = FreeCAD + +def CreateRectangleSketch(SketchFeature, corner, lengths): + hmin, hmax = corner[0], corner[0] + lengths[0] + vmin, vmax = corner[1], corner[1] + lengths[1] + + # add the geometry and grab the count offset + i = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmin,vmax),FreeCAD.Vector(hmax,vmax,0))) + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmax,vmax,0),FreeCAD.Vector(hmax,vmin,0))) + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmax,vmin,0),FreeCAD.Vector(hmin,vmin,0))) + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmin,vmin,0),FreeCAD.Vector(hmin,vmax,0))) + + # add the rectangular constraints + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+0,2,i+1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+1,2,i+2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+2,2,i+3,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+3,2,i+0,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',i+0)) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',i+2)) + SketchFeature.addConstraint(Sketcher.Constraint('Vertical',i+1)) + SketchFeature.addConstraint(Sketcher.Constraint('Vertical',i+3)) + + # Fix the bottom left corner of the rectangle + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',i+2,2,corner[0])) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',i+2,2,corner[1])) + + # add dimensions + if lengths[0] == lengths[1]: + SketchFeature.addConstraint(Sketcher.Constraint('Equal',i+2,i+3)) + SketchFeature.addConstraint(Sketcher.Constraint('Distance',i+0,hmax-hmin)) + else: + SketchFeature.addConstraint(Sketcher.Constraint('Distance',i+1,vmax-vmin)) + SketchFeature.addConstraint(Sketcher.Constraint('Distance',i+0,hmax-hmin)) + +def CreateCircleSketch(SketchFeature, center, radius): + i = int(SketchFeature.GeometryCount) + SketchFeature.addGeometry(Part.Circle(App.Vector(*center), App.Vector(0,0,1), radius),False) + SketchFeature.addConstraint(Sketcher.Constraint('Radius',i,radius)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',i,3,center[0])) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',i,3,center[1])) + +def CreateBoxSketchSet(SketchFeature): + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(-99.230339,36.960674,0),FreeCAD.Vector(69.432587,36.960674,0))) + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(69.432587,36.960674,0),FreeCAD.Vector(69.432587,-53.196629,0))) + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(69.432587,-53.196629,0),FreeCAD.Vector(-99.230339,-53.196629,0))) + SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(-99.230339,-53.196629,0),FreeCAD.Vector(-99.230339,36.960674,0))) + # add the constraints + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,2,1,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,2,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',2,2,3,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',3,2,0,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',0)) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',2)) + SketchFeature.addConstraint(Sketcher.Constraint('Vertical',1)) + SketchFeature.addConstraint(Sketcher.Constraint('Vertical',3)) + # add dimensions + SketchFeature.addConstraint(Sketcher.Constraint('Distance',1,81.370787)) + SketchFeature.addConstraint(Sketcher.Constraint('Distance',0,187.573036)) + +def CreateSlotPlateSet(SketchFeature): + SketchFeature.addGeometry(Part.LineSegment(App.Vector(60.029362,-30.279360,0),App.Vector(-120.376335,-30.279360,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',0)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(-120.376335,-30.279360,0),App.Vector(-70.193062,38.113884,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,2,1,1)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(-70.193062,38.113884,0),App.Vector(60.241116,37.478645,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,2,2,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',2)) + SketchFeature.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(60.039921,3.811391,0),App.Vector(0,0,1),35.127132),-1.403763,1.419522)) + SketchFeature.addConstraint(Sketcher.Constraint('Tangent',2,2,3,2)) + SketchFeature.addConstraint(Sketcher.Constraint('Tangent',0,1,3,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Angle',0,2,1,1,0.947837)) + SketchFeature.addConstraint(Sketcher.Constraint('Distance',0,184.127425)) + SketchFeature.setDatum(7,200.000000) + SketchFeature.addConstraint(Sketcher.Constraint('Radius',3,38.424808)) + SketchFeature.setDatum(8,40.000000) + SketchFeature.setDatum(6,0.872665) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,2,0.0)) + SketchFeature.setDatum(9,0.000000) + SketchFeature.movePoint(0,2,App.Vector(-0.007829,-33.376450,0)) + SketchFeature.movePoint(0,2,App.Vector(-0.738149,-10.493386,0)) + SketchFeature.movePoint(0,2,App.Vector(-0.007829,2.165328,0)) + SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,2,2.165328)) + SketchFeature.setDatum(10,0.000000) + +def CreateSlotPlateInnerSet(SketchFeature): + SketchFeature.addGeometry(Part.Circle(App.Vector(195.055893,39.562252,0),App.Vector(0,0,1),29.846098)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(150.319031,13.449363,0),App.Vector(36.700474,13.139774,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',5)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(36.700474,13.139774,0),App.Vector(77.566010,63.292927,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',5,2,6,1)) + SketchFeature.addGeometry(Part.LineSegment(App.Vector(77.566010,63.292927,0),App.Vector(148.151917,63.602505,0))) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',6,2,7,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',7)) + SketchFeature.addConstraint(Sketcher.Constraint('Parallel',1,6)) + SketchFeature.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(192.422913,38.216347,0),App.Vector(0,0,1),45.315174),2.635158,3.602228)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',7,2,8,1)) + SketchFeature.addConstraint(Sketcher.Constraint('Coincident',8,2,5,1)) + + + +#--------------------------------------------------------------------------- +# define the test cases to test the FreeCAD Sketcher module +#--------------------------------------------------------------------------- + + +class TestSketcherSolver(unittest.TestCase): + def setUp(self): + self.Doc = FreeCAD.newDocument("SketchSolverTest") + + def testBoxCase(self): + self.Box = self.Doc.addObject('Sketcher::SketchObject','SketchBox') + CreateBoxSketchSet(self.Box) + self.Doc.recompute() + # moving a point of the sketch + self.Box.movePoint(0,2,App.Vector(88.342697,28.174158,0)) + # fully constrain + self.Box.addConstraint(Sketcher.Constraint('DistanceX',1,2,90.0)) + self.Box.addConstraint(Sketcher.Constraint('DistanceY',1,2,-50.0)) + self.Doc.recompute() + + def testSlotCase(self): + self.Slot = self.Doc.addObject('Sketcher::SketchObject','SketchSlot') + CreateSlotPlateSet(self.Slot) + self.Doc.recompute() + # test if all edges created + self.failUnless(len(self.Slot.Shape.Edges) == 4) + CreateSlotPlateInnerSet(self.Slot) + self.Doc.recompute() + self.failUnless(len(self.Slot.Shape.Edges) == 9) + + def testIssue3245(self): + self.Doc2 = FreeCAD.newDocument("Issue3245") + self.Doc2.addObject('Sketcher::SketchObject','Sketch') + self.Doc2.Sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(0.000000,0.000000,0.000000,1.000000)) + self.Doc2.Sketch.MapMode = "Deactivated" + self.Doc2.Sketch.addGeometry(Part.LineSegment(App.Vector(-1.195999,56.041161,0),App.Vector(60.654316,56.382877,0)),False) + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('PointOnObject',0,1,-2)) + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('Horizontal',0)) + self.Doc2.Sketch.addGeometry(Part.LineSegment(App.Vector(0.512583,32.121155,0),App.Vector(60.654316,31.779440,0)),False) + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('Horizontal',1)) + self.Doc2.Sketch.addGeometry(Part.LineSegment(App.Vector(0.170867,13.326859,0),App.Vector(61.679455,13.326859,0)),False) + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('PointOnObject',2,1,-2)) + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('Horizontal',2)) + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('PointOnObject',1,1,-2)) + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,60.654316)) + self.Doc2.Sketch.setExpression('Constraints[6]', u'60') + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('DistanceX',1,1,1,2,60.654316)) + self.Doc2.Sketch.setExpression('Constraints[7]', u'65') + self.Doc2.Sketch.addConstraint(Sketcher.Constraint('DistanceX',2,1,2,2,61.679455)) + self.Doc2.Sketch.setExpression('Constraints[8]', u'70') + self.Doc2.recompute() + self.Doc2.Sketch.delGeometry(2) + values = d = {key: value for (key, value) in self.Doc2.Sketch.ExpressionEngine} + self.failUnless(values['Constraints[4]'] == u'60') + self.failUnless(values['Constraints[5]'] == u'65') + FreeCAD.closeDocument("Issue3245") + + def testIssue3245_2(self): + self.Doc2 = FreeCAD.newDocument("Issue3245") + ActiveSketch = self.Doc2.addObject('Sketcher::SketchObject','Sketch') + ActiveSketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(0.000000,0.000000,0.000000,1.000000)) + ActiveSketch.MapMode = "Deactivated" + geoList = [] + geoList.append(Part.LineSegment(App.Vector(-23.574591,42.399727,0),App.Vector(81.949776,42.399727,0))) + geoList.append(Part.LineSegment(App.Vector(81.949776,42.399727,0),App.Vector(81.949776,-19.256901,0))) + geoList.append(Part.LineSegment(App.Vector(81.949776,-19.256901,0),App.Vector(-23.574591,-19.256901,0))) + geoList.append(Part.LineSegment(App.Vector(-23.574591,-19.256901,0),App.Vector(-23.574591,42.399727,0))) + ActiveSketch.addGeometry(geoList,False) + conList = [] + conList.append(Sketcher.Constraint('Coincident',0,2,1,1)) + conList.append(Sketcher.Constraint('Coincident',1,2,2,1)) + conList.append(Sketcher.Constraint('Coincident',2,2,3,1)) + conList.append(Sketcher.Constraint('Coincident',3,2,0,1)) + conList.append(Sketcher.Constraint('Horizontal',0)) + conList.append(Sketcher.Constraint('Horizontal',2)) + conList.append(Sketcher.Constraint('Vertical',1)) + conList.append(Sketcher.Constraint('Vertical',3)) + ActiveSketch.addConstraint(conList) + ActiveSketch.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,105.524367)) + ActiveSketch.setExpression('Constraints[8]', u'10 + 10') + ActiveSketch.addConstraint(Sketcher.Constraint('DistanceY',3,1,3,2,61.656628)) + ActiveSketch.setDatum(9,App.Units.Quantity('5.000000 mm')) + ActiveSketch.delConstraint(8) + values = d = {key: value for (key, value) in self.Doc2.Sketch.ExpressionEngine} + self.Doc2.recompute() + self.failUnless(len(values) == 0) + FreeCAD.closeDocument("Issue3245") + + def tearDown(self): + #closing doc + FreeCAD.closeDocument("SketchSolverTest") + #print ("omit closing document for debugging") diff --git a/src/Mod/Sketcher/SketcherTests/__init__.py b/src/Mod/Sketcher/SketcherTests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/Sketcher/TestSketcherApp.py b/src/Mod/Sketcher/TestSketcherApp.py index d822aa4514..6a06b1520d 100644 --- a/src/Mod/Sketcher/TestSketcherApp.py +++ b/src/Mod/Sketcher/TestSketcherApp.py @@ -1,4 +1,5 @@ # (c) Juergen Riegel (FreeCAD@juergen-riegel.net) 2011 LGPL * +# (c) Emmanuel O'Brien 2021 LGPL * # * # This file is part of the FreeCAD CAx development system. * # * @@ -19,198 +20,12 @@ # USA * #************************************************************************** +# Broken-out test modules +from SketcherTests.TestSketcherSolver import TestSketcherSolver +from SketcherTests.TestSketchFillet import TestSketchFillet -import FreeCAD, os, sys, unittest, Part, Sketcher -App = FreeCAD - -def CreateRectangleSketch(SketchFeature, corner, lengths): - hmin, hmax = corner[0], corner[0] + lengths[0] - vmin, vmax = corner[1], corner[1] + lengths[1] - - # add the geometry and grab the count offset - i = int(SketchFeature.GeometryCount) - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmin,vmax),FreeCAD.Vector(hmax,vmax,0))) - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmax,vmax,0),FreeCAD.Vector(hmax,vmin,0))) - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmax,vmin,0),FreeCAD.Vector(hmin,vmin,0))) - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(hmin,vmin,0),FreeCAD.Vector(hmin,vmax,0))) - - # add the rectangular constraints - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+0,2,i+1,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+1,2,i+2,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+2,2,i+3,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',i+3,2,i+0,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',i+0)) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',i+2)) - SketchFeature.addConstraint(Sketcher.Constraint('Vertical',i+1)) - SketchFeature.addConstraint(Sketcher.Constraint('Vertical',i+3)) - - # Fix the bottom left corner of the rectangle - SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',i+2,2,corner[0])) - SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',i+2,2,corner[1])) - - # add dimensions - if lengths[0] == lengths[1]: - SketchFeature.addConstraint(Sketcher.Constraint('Equal',i+2,i+3)) - SketchFeature.addConstraint(Sketcher.Constraint('Distance',i+0,hmax-hmin)) - else: - SketchFeature.addConstraint(Sketcher.Constraint('Distance',i+1,vmax-vmin)) - SketchFeature.addConstraint(Sketcher.Constraint('Distance',i+0,hmax-hmin)) - -def CreateCircleSketch(SketchFeature, center, radius): - i = int(SketchFeature.GeometryCount) - SketchFeature.addGeometry(Part.Circle(App.Vector(*center), App.Vector(0,0,1), radius),False) - SketchFeature.addConstraint(Sketcher.Constraint('Radius',i,radius)) - SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',i,3,center[0])) - SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',i,3,center[1])) - -def CreateBoxSketchSet(SketchFeature): - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(-99.230339,36.960674,0),FreeCAD.Vector(69.432587,36.960674,0))) - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(69.432587,36.960674,0),FreeCAD.Vector(69.432587,-53.196629,0))) - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(69.432587,-53.196629,0),FreeCAD.Vector(-99.230339,-53.196629,0))) - SketchFeature.addGeometry(Part.LineSegment(FreeCAD.Vector(-99.230339,-53.196629,0),FreeCAD.Vector(-99.230339,36.960674,0))) - # add the constraints - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,2,1,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,2,2,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',2,2,3,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',3,2,0,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',0)) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',2)) - SketchFeature.addConstraint(Sketcher.Constraint('Vertical',1)) - SketchFeature.addConstraint(Sketcher.Constraint('Vertical',3)) - # add dimensions - SketchFeature.addConstraint(Sketcher.Constraint('Distance',1,81.370787)) - SketchFeature.addConstraint(Sketcher.Constraint('Distance',0,187.573036)) - -def CreateSlotPlateSet(SketchFeature): - SketchFeature.addGeometry(Part.LineSegment(App.Vector(60.029362,-30.279360,0),App.Vector(-120.376335,-30.279360,0))) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',0)) - SketchFeature.addGeometry(Part.LineSegment(App.Vector(-120.376335,-30.279360,0),App.Vector(-70.193062,38.113884,0))) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',0,2,1,1)) - SketchFeature.addGeometry(Part.LineSegment(App.Vector(-70.193062,38.113884,0),App.Vector(60.241116,37.478645,0))) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',1,2,2,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',2)) - SketchFeature.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(60.039921,3.811391,0),App.Vector(0,0,1),35.127132),-1.403763,1.419522)) - SketchFeature.addConstraint(Sketcher.Constraint('Tangent',2,2,3,2)) - SketchFeature.addConstraint(Sketcher.Constraint('Tangent',0,1,3,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Angle',0,2,1,1,0.947837)) - SketchFeature.addConstraint(Sketcher.Constraint('Distance',0,184.127425)) - SketchFeature.setDatum(7,200.000000) - SketchFeature.addConstraint(Sketcher.Constraint('Radius',3,38.424808)) - SketchFeature.setDatum(8,40.000000) - SketchFeature.setDatum(6,0.872665) - SketchFeature.addConstraint(Sketcher.Constraint('DistanceX',0,2,0.0)) - SketchFeature.setDatum(9,0.000000) - SketchFeature.movePoint(0,2,App.Vector(-0.007829,-33.376450,0)) - SketchFeature.movePoint(0,2,App.Vector(-0.738149,-10.493386,0)) - SketchFeature.movePoint(0,2,App.Vector(-0.007829,2.165328,0)) - SketchFeature.addConstraint(Sketcher.Constraint('DistanceY',0,2,2.165328)) - SketchFeature.setDatum(10,0.000000) - -def CreateSlotPlateInnerSet(SketchFeature): - SketchFeature.addGeometry(Part.Circle(App.Vector(195.055893,39.562252,0),App.Vector(0,0,1),29.846098)) - SketchFeature.addGeometry(Part.LineSegment(App.Vector(150.319031,13.449363,0),App.Vector(36.700474,13.139774,0))) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',5)) - SketchFeature.addGeometry(Part.LineSegment(App.Vector(36.700474,13.139774,0),App.Vector(77.566010,63.292927,0))) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',5,2,6,1)) - SketchFeature.addGeometry(Part.LineSegment(App.Vector(77.566010,63.292927,0),App.Vector(148.151917,63.602505,0))) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',6,2,7,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Horizontal',7)) - SketchFeature.addConstraint(Sketcher.Constraint('Parallel',1,6)) - SketchFeature.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(192.422913,38.216347,0),App.Vector(0,0,1),45.315174),2.635158,3.602228)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',7,2,8,1)) - SketchFeature.addConstraint(Sketcher.Constraint('Coincident',8,2,5,1)) - - - -#--------------------------------------------------------------------------- -# define the test cases to test the FreeCAD Sketcher module -#--------------------------------------------------------------------------- - - -class SketcherSolverTestCases(unittest.TestCase): - def setUp(self): - self.Doc = FreeCAD.newDocument("SketchSolverTest") - - def testBoxCase(self): - self.Box = self.Doc.addObject('Sketcher::SketchObject','SketchBox') - CreateBoxSketchSet(self.Box) - self.Doc.recompute() - # moving a point of the sketch - self.Box.movePoint(0,2,App.Vector(88.342697,28.174158,0)) - # fully constrain - self.Box.addConstraint(Sketcher.Constraint('DistanceX',1,2,90.0)) - self.Box.addConstraint(Sketcher.Constraint('DistanceY',1,2,-50.0)) - self.Doc.recompute() - - def testSlotCase(self): - self.Slot = self.Doc.addObject('Sketcher::SketchObject','SketchSlot') - CreateSlotPlateSet(self.Slot) - self.Doc.recompute() - # test if all edges created - self.failUnless(len(self.Slot.Shape.Edges) == 4) - CreateSlotPlateInnerSet(self.Slot) - self.Doc.recompute() - self.failUnless(len(self.Slot.Shape.Edges) == 9) - - def testIssue3245(self): - self.Doc2 = FreeCAD.newDocument("Issue3245") - self.Doc2.addObject('Sketcher::SketchObject','Sketch') - self.Doc2.Sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(0.000000,0.000000,0.000000,1.000000)) - self.Doc2.Sketch.MapMode = "Deactivated" - self.Doc2.Sketch.addGeometry(Part.LineSegment(App.Vector(-1.195999,56.041161,0),App.Vector(60.654316,56.382877,0)),False) - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('PointOnObject',0,1,-2)) - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('Horizontal',0)) - self.Doc2.Sketch.addGeometry(Part.LineSegment(App.Vector(0.512583,32.121155,0),App.Vector(60.654316,31.779440,0)),False) - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('Horizontal',1)) - self.Doc2.Sketch.addGeometry(Part.LineSegment(App.Vector(0.170867,13.326859,0),App.Vector(61.679455,13.326859,0)),False) - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('PointOnObject',2,1,-2)) - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('Horizontal',2)) - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('PointOnObject',1,1,-2)) - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,60.654316)) - self.Doc2.Sketch.setExpression('Constraints[6]', u'60') - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('DistanceX',1,1,1,2,60.654316)) - self.Doc2.Sketch.setExpression('Constraints[7]', u'65') - self.Doc2.Sketch.addConstraint(Sketcher.Constraint('DistanceX',2,1,2,2,61.679455)) - self.Doc2.Sketch.setExpression('Constraints[8]', u'70') - self.Doc2.recompute() - self.Doc2.Sketch.delGeometry(2) - values = d = {key: value for (key, value) in self.Doc2.Sketch.ExpressionEngine} - self.failUnless(values['Constraints[4]'] == u'60') - self.failUnless(values['Constraints[5]'] == u'65') - FreeCAD.closeDocument("Issue3245") - - def testIssue3245_2(self): - self.Doc2 = FreeCAD.newDocument("Issue3245") - ActiveSketch = self.Doc2.addObject('Sketcher::SketchObject','Sketch') - ActiveSketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(0.000000,0.000000,0.000000,1.000000)) - ActiveSketch.MapMode = "Deactivated" - geoList = [] - geoList.append(Part.LineSegment(App.Vector(-23.574591,42.399727,0),App.Vector(81.949776,42.399727,0))) - geoList.append(Part.LineSegment(App.Vector(81.949776,42.399727,0),App.Vector(81.949776,-19.256901,0))) - geoList.append(Part.LineSegment(App.Vector(81.949776,-19.256901,0),App.Vector(-23.574591,-19.256901,0))) - geoList.append(Part.LineSegment(App.Vector(-23.574591,-19.256901,0),App.Vector(-23.574591,42.399727,0))) - ActiveSketch.addGeometry(geoList,False) - conList = [] - conList.append(Sketcher.Constraint('Coincident',0,2,1,1)) - conList.append(Sketcher.Constraint('Coincident',1,2,2,1)) - conList.append(Sketcher.Constraint('Coincident',2,2,3,1)) - conList.append(Sketcher.Constraint('Coincident',3,2,0,1)) - conList.append(Sketcher.Constraint('Horizontal',0)) - conList.append(Sketcher.Constraint('Horizontal',2)) - conList.append(Sketcher.Constraint('Vertical',1)) - conList.append(Sketcher.Constraint('Vertical',3)) - ActiveSketch.addConstraint(conList) - ActiveSketch.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,105.524367)) - ActiveSketch.setExpression('Constraints[8]', u'10 + 10') - ActiveSketch.addConstraint(Sketcher.Constraint('DistanceY',3,1,3,2,61.656628)) - ActiveSketch.setDatum(9,App.Units.Quantity('5.000000 mm')) - ActiveSketch.delConstraint(8) - values = d = {key: value for (key, value) in self.Doc2.Sketch.ExpressionEngine} - self.Doc2.recompute() - self.failUnless(len(values) == 0) - FreeCAD.closeDocument("Issue3245") - - def tearDown(self): - #closing doc - FreeCAD.closeDocument("SketchSolverTest") - #print ("omit closing document for debugging") +# Path and PartDesign tests use these functions that used to live here +# but moved to SketcherTests/TestSketcherSolver.py +from SketcherTests.TestSketcherSolver import CreateCircleSketch +from SketcherTests.TestSketcherSolver import CreateRectangleSketch +from SketcherTests.TestSketcherSolver import CreateSlotPlateSet diff --git a/src/Mod/Spreadsheet/App/Cell.cpp b/src/Mod/Spreadsheet/App/Cell.cpp index 3af0131c24..749f5835ee 100644 --- a/src/Mod/Spreadsheet/App/Cell.cpp +++ b/src/Mod/Spreadsheet/App/Cell.cpp @@ -38,9 +38,11 @@ #include #include #include +#include #include #include "Sheet.h" #include +#include FC_LOG_LEVEL_INIT("Spreadsheet",true,true) @@ -279,62 +281,98 @@ void Cell::afterRestore() { void Cell::setContent(const char * value) { PropertySheet::AtomicPropertyChange signaller(*owner); - App::Expression * expr = 0; + ExpressionPtr newExpr; clearException(); - if (value != 0) { - if(owner->sheet()->isRestoring()) { - expression.reset(new App::StringExpression(owner->sheet(),value)); + if (value) { + if (owner->sheet()->isRestoring()) { + expression.reset(new App::StringExpression(owner->sheet(), value)); setUsed(EXPRESSION_SET, true); return; } if (*value == '=') { try { - expr = App::ExpressionParser::parse(owner->sheet(), value + 1); + newExpr = ExpressionPtr(App::ExpressionParser::parse(owner->sheet(), value + 1)); } catch (Base::Exception & e) { - expr = new App::StringExpression(owner->sheet(), value); + newExpr = std::make_unique(owner->sheet(), value); setParseException(e.what()); } } else if (*value == '\'') { - expr = new App::StringExpression(owner->sheet(), value + 1); + newExpr = std::make_unique(owner->sheet(), value + 1); } else if (*value != '\0') { + // check if value is just a number char * end; errno = 0; - double float_value = strtod(value, &end); - if (!*end && errno == 0) { - expr = new App::NumberExpression(owner->sheet(), Quantity(float_value)); + const double float_value = strtod(value, &end); + if (errno == 0) { + const bool isEndEmpty = *end == '\0' || strspn(end, " \t\n\r") == strlen(end); + if (isEndEmpty) { + newExpr = std::make_unique(owner->sheet(), Quantity(float_value)); + } } - else { + + // if not a float, check if it is a quantity or compatible fraction + const bool isStartingWithNumber = value != end; + if (!newExpr && isStartingWithNumber) { try { - expr = ExpressionParser::parse(owner->sheet(), value); - if (expr) - delete expr->eval(); - } - catch (Base::Exception &) { - expr = new App::StringExpression(owner->sheet(), value); + ExpressionPtr parsedExpr(App::ExpressionParser::parse(owner->sheet(), value)); + + if (const auto fraction = freecad_dynamic_cast(parsedExpr.get())) { + if (fraction->getOperator() == OperatorExpression::UNIT) { + const auto left = freecad_dynamic_cast(fraction->getLeft()); + const auto right = freecad_dynamic_cast(fraction->getRight()); + if (left && right) { + newExpr = std::move(parsedExpr); + } + } + else if (fraction->getOperator() == OperatorExpression::DIV) { + // only the following types of fractions are ok: + // 1/2, 1m/2, 1/2s, 1m/2s, 1/m + + // check for numbers in (de)nominator + const bool isNumberNom = freecad_dynamic_cast(fraction->getLeft()); + const bool isNumberDenom = freecad_dynamic_cast(fraction->getRight()); + + // check for numbers with units in (de)nominator + const auto opNom = freecad_dynamic_cast(fraction->getLeft()); + const auto opDenom = freecad_dynamic_cast(fraction->getRight()); + const bool isQuantityNom = opNom && opNom->getOperator() == OperatorExpression::UNIT; + const bool isQuantityDenom = opDenom && opDenom->getOperator() == OperatorExpression::UNIT; + + // check for units in denomainator + const auto uDenom = freecad_dynamic_cast(fraction->getRight()); + const bool isUnitDenom = uDenom && uDenom->getTypeId() == UnitExpression::getClassTypeId(); + + const bool isNomValid = isNumberNom || isQuantityNom; + const bool isDenomValid = isNumberDenom || isQuantityDenom || isUnitDenom; + if (isNomValid && isDenomValid) { + newExpr = std::move(parsedExpr); + } + } + } + else if (const auto number = freecad_dynamic_cast(parsedExpr.get())) { + // NumbersExpressions can accept more than can be parsed with strtod. + // Example: 12.34 and 12,34 are both valid NumberExpressions + newExpr = std::move(parsedExpr); + } } + catch (...) {} } } - } - try { - setExpression(App::ExpressionPtr(expr)); - signaller.tryInvoke(); - } - catch (Base::Exception &e) { - if (value) { - std::string _value = value; - if (_value[0] != '=') { - _value.insert (0, 1, '='); - } - - setExpression(App::ExpressionPtr(new App::StringExpression(owner->sheet(), _value))); - setParseException(e.what()); + if (!newExpr && *value != '\0') { + newExpr = std::make_unique(owner->sheet(), value); } + + // trying to add an empty string will make newExpr = nullptr } + + // set expression, or delete the current expression by setting nullptr if empty string was entered + setExpression(std::move(newExpr)); + signaller.tryInvoke(); } /** @@ -503,15 +541,22 @@ void Cell::setAlias(const std::string &n) owner->revAliasProp.erase(alias); - alias = n; - // Update owner - if (alias != "") { + if (!n.empty()) { owner->aliasProp[address] = n; owner->revAliasProp[n] = address; } - else + else { owner->aliasProp.erase(address); + } + + if (!alias.empty()) { + // The property may have been added in Sheet::updateAlias + auto * docObj = static_cast(owner->getContainer()); + docObj->removeDynamicProperty(alias.c_str()); + } + + alias = n; setUsed(ALIAS_SET, !alias.empty()); setDirty(); @@ -1020,4 +1065,3 @@ std::string Cell::getFormattedQuantity(void) return result; } - diff --git a/src/Mod/Spreadsheet/TestSpreadsheet.py b/src/Mod/Spreadsheet/TestSpreadsheet.py index adda58466d..157c57d9a7 100644 --- a/src/Mod/Spreadsheet/TestSpreadsheet.py +++ b/src/Mod/Spreadsheet/TestSpreadsheet.py @@ -668,6 +668,23 @@ class SpreadsheetCases(unittest.TestCase): self.assertEqual(sheet.A17, 0.5) self.assertEqual(sheet.A18, 0.5) + def testQuantitiesAndFractionsAsNumbers(self): + """ Test quantities and simple fractions as numbers """ + sheet = self.doc.addObject('Spreadsheet::Sheet','Spreadsheet') + sheet.set('A1', '1mm') + sheet.set('A2', '1/2') + sheet.set('A3', '4mm/2') + sheet.set('A4', '2/mm') + sheet.set('A5', '4/2mm') + sheet.set('A6', '6mm/3s') + self.doc.recompute() + self.assertEqual(sheet.A1, Units.Quantity('1 mm')) + self.assertEqual(sheet.A2, 0.5) + self.assertEqual(sheet.A3, Units.Quantity('2 mm')) + self.assertEqual(sheet.A4, Units.Quantity('2 1/mm')) + self.assertEqual(sheet.A5, Units.Quantity('2 1/mm')) + self.assertEqual(sheet.A6, Units.Quantity('2 mm/s')) + def testRemoveRows(self): """ Removing rows -- check renaming of internal cells """ sheet = self.doc.addObject('Spreadsheet::Sheet','Spreadsheet') @@ -1034,6 +1051,16 @@ class SpreadsheetCases(unittest.TestCase): self.doc.recompute() self.assertEqual(sheet.get('C1'), Units.Quantity('3 mm')) + def testIssue4156(self): + """ Regression test for issue 4156; necessarily use of leading '=' to enter an expression, creates inconsistent behavior depending on the spreadsheet state""" + sheet = self.doc.addObject('Spreadsheet::Sheet','Spreadsheet') + sheet.set('A3', 'A1') + sheet.set('A1', '1000') + self.doc.recompute() + sheet.set('A3', '') + sheet.set('A3', 'A1') + self.assertEqual(sheet.getContents('A3'), 'A1') + def testInsertRowsAlias(self): """ Regression test for issue 4429; insert rows to sheet with aliases""" sheet = self.doc.addObject('Spreadsheet::Sheet','Spreadsheet') @@ -1102,6 +1129,20 @@ class SpreadsheetCases(unittest.TestCase): sheet.removeColumns('B', 1) sheet.setAlias('C3','test') + def testUndoAliasCreationReuseName(self): + """ Test deleted aliases by undo remains in database""" + sheet = self.doc.addObject('Spreadsheet::Sheet','Spreadsheet') + + self.doc.UndoMode = 1 + self.doc.openTransaction("create alias") + sheet.setAlias('B2', 'test') + self.doc.commitTransaction() + self.doc.recompute() + + self.doc.undo() + self.doc.recompute() + sheet.setAlias('C3','test') + def tearDown(self): #closing doc FreeCAD.closeDocument(self.doc.Name) diff --git a/src/Mod/Start/StartPage/EnableDownload.py b/src/Mod/Start/StartPage/EnableDownload.py index 4baca89549..f7f117969e 100644 --- a/src/Mod/Start/StartPage/EnableDownload.py +++ b/src/Mod/Start/StartPage/EnableDownload.py @@ -20,6 +20,6 @@ #* * #*************************************************************************** -import FreeCAD,FreeCADGui +import FreeCAD rf=FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Start") rf.SetBool("AllowDownload",True) diff --git a/src/Mod/Start/StartPage/LoadMRU.py b/src/Mod/Start/StartPage/LoadMRU.py index 4c7889daa3..7b7e14c7d6 100644 --- a/src/Mod/Start/StartPage/LoadMRU.py +++ b/src/Mod/Start/StartPage/LoadMRU.py @@ -20,7 +20,7 @@ #* * #*************************************************************************** -import FreeCADGui,sys +import FreeCADGui # MRU will be given before this script is run rf=FreeCAD.ParamGet("User parameter:BaseApp/Preferences/RecentFiles") FreeCADGui.loadFile(rf.GetString("MRU"+str(MRU))) diff --git a/src/Mod/Start/StartPage/StartPage.html b/src/Mod/Start/StartPage/StartPage.html index 36a3bf2aa0..9227550771 100644 --- a/src/Mod/Start/StartPage/StartPage.html +++ b/src/Mod/Start/StartPage/StartPage.html @@ -1,9 +1,10 @@ - + + T_TITLE - - + + @@ -12,7 +13,7 @@
VERSIONSTRING - + T_VTOOLTIP
@@ -57,25 +58,25 @@

T_GENERALDOCUMENTATION

- + T_USERHUB T_USERHUB

T_DESCR_USERHUB

- + T_POWERHUB T_POWERHUB

T_DESCR_POWERHUB

- + T_DEVHUB T_DEVHUB

T_DESCR_DEVHUB

- + T_MANUAL T_MANUAL

T_DESCR_MANUAL

diff --git a/src/Mod/Start/StartPage/StartPage.js b/src/Mod/Start/StartPage/StartPage.js index ca70689613..ad906f89e9 100644 --- a/src/Mod/Start/StartPage/StartPage.js +++ b/src/Mod/Start/StartPage/StartPage.js @@ -31,7 +31,7 @@ function load() { if (allowDownloads == 1) { // load latest commits - ddiv = document.getElementById("commits"); + var ddiv = document.getElementById("commits"); ddiv.innerHTML = "Connecting..."; var tobj=new JSONscriptRequest('https://api.github.com/repos/FreeCAD/FreeCAD/commits?callback=printCommits'); tobj.buildScriptTag(); // Build the script tag @@ -61,7 +61,7 @@ function printCommits(data) { // json callback for git commits - ddiv = document.getElementById('commits'); + var ddiv = document.getElementById('commits'); ddiv.innerHTML = "Received"; var html = ['
    ']; for (var i = 0; i < 25; i++) { @@ -76,7 +76,7 @@ function printAddons(data) { // json callback for addons list - ddiv = document.getElementById('addons'); + var ddiv = document.getElementById('addons'); ddiv.innerHTML = "Received"; var html = ['
      ']; var blacklist = ['addons_installer.FCMacro','FreeCAD-Addon-Details.md','README.md']; @@ -98,7 +98,7 @@ function printForum(data) { // json callback for forum posts - ddiv = document.getElementById('forum'); + var ddiv = document.getElementById('forum'); ddiv.innerHTML = "Received"; var html = ['
        ']; for (var i = 0; i < 25; i++) { diff --git a/src/Mod/Start/StartPage/StartPage.py b/src/Mod/Start/StartPage/StartPage.py index 5fc176f778..20589363bf 100644 --- a/src/Mod/Start/StartPage/StartPage.py +++ b/src/Mod/Start/StartPage/StartPage.py @@ -25,7 +25,7 @@ # the html code of the start page. It is built only once per FreeCAD session for now... import six -import sys,os,FreeCAD,FreeCADGui,tempfile,time,zipfile,urllib,re +import sys,os,FreeCAD,FreeCADGui,tempfile,time,zipfile,re from . import TranslationTexts from PySide import QtCore,QtGui @@ -107,7 +107,7 @@ def getInfo(filename): try: import gnome.ui import gnomevfs - except: + except Exception: # alternative method import hashlib fhash = hashlib.md5(("file://"+path).encode("utf8")).hexdigest() @@ -140,7 +140,7 @@ def getInfo(filename): if filename.lower().endswith(".fcstd"): try: zfile=zipfile.ZipFile(filename) - except: + except Exception: print("Cannot read file: ",filename) return None files=zfile.namelist() @@ -148,19 +148,19 @@ def getInfo(filename): if files[0] == "Document.xml": doc = str(zfile.read(files[0])) doc = doc.replace("\n"," ") - r = re.findall("Property name=\"CreatedBy.*?String value=\"(.*?)\"\/>",doc) + r = re.findall("Property name=\"CreatedBy.*?String value=\"(.*?)\"/>",doc) if r: author = r[0] # remove email if present in author field if "<" in author: author = author.split("<")[0].strip() - r = re.findall("Property name=\"Company.*?String value=\"(.*?)\"\/>",doc) + r = re.findall("Property name=\"Company.*?String value=\"(.*?)\"/>",doc) if r: company = r[0] - r = re.findall("Property name=\"License.*?String value=\"(.*?)\"\/>",doc) + r = re.findall("Property name=\"License.*?String value=\"(.*?)\"/>",doc) if r: lic = r[0] - r = re.findall("Property name=\"Comment.*?String value=\"(.*?)\"\/>",doc) + r = re.findall("Property name=\"Comment.*?String value=\"(.*?)\"/>",doc) if r: descr = r[0] if "thumbnails/Thumbnail.png" in files: @@ -247,16 +247,16 @@ def buildCard(filename,method,arg=None): if finfo[5]: infostring += "\n\n" + encode(finfo[5]) if size: - result += '' result += '
      • ' - result += '' + result += '' + result += ''+encode(basename)+'' result += '
        ' result += '

        '+encode(basename)+'

        ' result += '

        '+encode(author)+'

        ' result += '

        '+size+'

        ' result += '
        ' - result += '
      • ' result += '' + result += '' return result @@ -298,6 +298,10 @@ def handle(): HTML = HTML.replace("CSS",CSS) HTML = encode(HTML) + # set the language + + HTML = HTML.replace("BCP47_LANGUAGE",QtCore.QLocale().bcp47Name()) + # get the stylesheet if we are using one if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Start").GetBool("UseStyleSheet",False): @@ -366,22 +370,21 @@ def handle(): # build SECTION_RECENTFILES - SECTION_RECENTFILES = encode("") rf = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/RecentFiles") rfcount = rf.GetInt("RecentFiles",0) SECTION_RECENTFILES = encode("

        "+TranslationTexts.T_RECENTFILES+"

        ") SECTION_RECENTFILES += "
          " - SECTION_RECENTFILES += '' SECTION_RECENTFILES += '
        • ' + SECTION_RECENTFILES += '' if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Start").GetBool("NewFileGradient",False): - SECTION_RECENTFILES += '' + SECTION_RECENTFILES += ''+encode(TranslationTexts.T_CREATENEW)+'' else: - SECTION_RECENTFILES += '' + SECTION_RECENTFILES += ''+encode(TranslationTexts.T_CREATENEW)+'' SECTION_RECENTFILES += '
          ' SECTION_RECENTFILES += '

          '+encode(TranslationTexts.T_CREATENEW)+'

          ' SECTION_RECENTFILES += '
          ' - SECTION_RECENTFILES += '
        • ' SECTION_RECENTFILES += '' + SECTION_RECENTFILES += '' for i in range(rfcount): filename = rf.GetString("MRU%d" % (i)) SECTION_RECENTFILES += encode(buildCard(filename,method="LoadMRU.py?MRU=",arg=str(i))) @@ -425,14 +428,12 @@ def handle(): # build IMAGE_SRC paths - HTML = HTML.replace("IMAGE_SRC_USERHUB",'file:///'+os.path.join(resources_dir, 'images/userhub.png')) - HTML = HTML.replace("IMAGE_SRC_POWERHUB",'file:///'+os.path.join(resources_dir, 'images/poweruserhub.png')) - HTML = HTML.replace("IMAGE_SRC_DEVHUB",'file:///'+os.path.join(resources_dir, 'images/developerhub.png')) - HTML = HTML.replace("IMAGE_SRC_MANUAL",'file:///'+os.path.join(resources_dir, 'images/manual.png')) - HTML = HTML.replace("IMAGE_SRC_SETTINGS",'file:///'+os.path.join(resources_dir, 'images/settings.png')) - imagepath= 'file:///'+os.path.join(resources_dir, 'images/installed.png') - imagepath = imagepath.replace('\\','/') # replace Windows backslash with slash to make the path javascript compatible - HTML = HTML.replace("IMAGE_SRC_INSTALLED",imagepath) + HTML = HTML.replace("IMAGE_SRC_USERHUB",'file:///'+os.path.join(resources_dir, 'images/userhub.png').replace('\\','/')) + HTML = HTML.replace("IMAGE_SRC_POWERHUB",'file:///'+os.path.join(resources_dir, 'images/poweruserhub.png').replace('\\','/')) + HTML = HTML.replace("IMAGE_SRC_DEVHUB",'file:///'+os.path.join(resources_dir, 'images/developerhub.png').replace('\\','/')) + HTML = HTML.replace("IMAGE_SRC_MANUAL",'file:///'+os.path.join(resources_dir, 'images/manual.png').replace('\\','/')) + HTML = HTML.replace("IMAGE_SRC_SETTINGS",'file:///'+os.path.join(resources_dir, 'images/settings.png').replace('\\','/')) + HTML = HTML.replace("IMAGE_SRC_INSTALLED",'file:///'+os.path.join(resources_dir, 'images/installed.png').replace('\\','/')) # build UL_WORKBENCHES @@ -482,7 +483,7 @@ def handle(): xpm = w.Icon if "XPM" in xpm: xpm = xpm.replace("\n ","\n") # some XPMs have some indent that QT doesn't like - r = [s[:-1].strip('"') for s in re.findall("(?s)\{(.*?)\};",xpm)[0].split("\n")[1:]] + r = [s[:-1].strip('"') for s in re.findall("(?s){(.*?)};",xpm)[0].split("\n")[1:]] p = QtGui.QPixmap(r) p = p.scaled(24,24) img = tempfile.mkstemp(dir=tempfolder,suffix='.png')[1] @@ -493,7 +494,7 @@ def handle(): img = os.path.join(resources_dir,"images/freecad.png") iconbank[wb] = img UL_WORKBENCHES += '
        • ' - UL_WORKBENCHES += ' ' + UL_WORKBENCHES += ''+wn+' ' UL_WORKBENCHES += ''+wn.replace("ReverseEngineering","ReverseEng")+'' UL_WORKBENCHES += '
        • ' UL_WORKBENCHES += '
        ' @@ -503,19 +504,19 @@ def handle(): try: import dxfLibrary - except: + except Exception: pass else: wblist.append("dxf-library") try: import RebarTools - except: + except Exception: pass else: wblist.append("reinforcement") try: import CADExchangerIO - except: + except Exception: pass else: wblist.append("cadexchanger") diff --git a/src/Mod/Start/TestStart/TestStartPage.py b/src/Mod/Start/TestStart/TestStartPage.py index 9e0740a658..7cc1da5761 100644 --- a/src/Mod/Start/TestStart/TestStartPage.py +++ b/src/Mod/Start/TestStart/TestStartPage.py @@ -24,7 +24,7 @@ import unittest import FreeCAD import Start from StartPage import StartPage -from html.parser import HTMLParser +import re class TestStartPage(unittest.TestCase): """Basic validation of the generated Start page.""" @@ -35,6 +35,7 @@ class TestStartPage(unittest.TestCase): def setUp(self): pass + def test_all_css_placeholders_removed(self): """Check to see if all of the CSS placeholders have been replaced.""" placeholders = ["BACKGROUND","BGTCOLOR","FONTFAMILY","FONTSIZE","LINKCOLOR", @@ -44,6 +45,7 @@ class TestStartPage(unittest.TestCase): for placeholder in placeholders: self.assertNotIn (placeholder, page, "{} was not removed from the CSS".format(placeholder)) + def test_all_js_placeholders_removed(self): """Check to see if all of the JavaScript placeholders have been replaced.""" placeholders = ["IMAGE_SRC_INSTALLED"] @@ -51,6 +53,7 @@ class TestStartPage(unittest.TestCase): for placeholder in placeholders: self.assertNotIn (placeholder, page, "{} was not removed from the JS".format(placeholder)) + def test_all_html_placeholders_removed(self): """Check to see if all of the HTML placeholders have been replaced.""" placeholders = ["T_TITLE","VERSIONSTRING","T_DOCUMENTS","T_HELP","T_ACTIVITY", @@ -69,4 +72,97 @@ class TestStartPage(unittest.TestCase): page = StartPage.handle() for placeholder in placeholders: self.assertNotIn (placeholder, page, "{} was not removed from the HTML".format(placeholder)) - \ No newline at end of file + + + def test_files_do_not_contain_backslashes(self): + # This would be caught by the W3C validator if we didn't sanitize the filenames before sending them. + page = StartPage.handle() + fileRE = re.compile(r'"file:///(.*?)"') + results = fileRE.findall(string=page) + + badFilenames = [] + for result in results: + if result.find("\\") != -1: + badFilenames.append(result) + + if len(badFilenames) > 0: + self.fail("The following filenames contain backslashes, which is prohibited in HTML: {}".format(badFilenames)) + + + def test_html_validates(self): + # Send the generated html to the W3C validator for analysis (removing potentially-sensitive data first) + import urllib.request + import os + import json + page = self.sanitize(StartPage.handle()) # Remove potentially sensitive data + + # For debugging, if you want to ensure that the sanitization worked correctly: + # from pathlib import Path + # home = str(Path.home()) + # f=open(home+"/test.html", "w") + # f.write(page) + # f.close() + + validation_url = "https://validator.w3.org/nu/?out=json" + data = page.encode('utf-8') # data should be bytes + req = urllib.request.Request(validation_url, data) + req.add_header("Content-type","text/html; charset=utf-8") + errorCount = 0 + warningCount = 0 + infoCount = 0 + validationResultString = "" + try: + with urllib.request.urlopen (req) as response: + text = response.read() + + responseJSON = json.loads(text) + + for message in responseJSON["messages"]: + if "type" in message: + if message["type"] == "info": + if "subtype" in message: + if message["subtype"] == "warning": + warningCount += 1 + validationResultString += "WARNING: {}\n".format(ascii(message["message"])) + else: + infoCount += 1 + validationResultString += "INFO: {}\n".format(ascii(message["message"])) + elif message["type"] == "error": + errorCount += 1 + validationResultString += "ERROR: {}\n".format(ascii(message["message"])) + elif message["type"] == "non-document-error": + FreeCAD.Console.PrintWarning("W3C validator returned a non-document error:\n {}".format(message)) + return + + except urllib.error.HTTPError as e: + FreeCAD.Console.PrintWarning("W3C validator returned response code {}".format(e.code)) + + except urllib.error.URLError: + FreeCAD.Console.PrintWarning("Could not communicate with W3C validator") + + if errorCount > 0 or warningCount > 0: + StartPage.exportTestFile() + FreeCAD.Console.PrintWarning("HTML validation failed: Start page source written to your home directory for analysis.") + self.fail("W3C Validator analysis shows the Start page has {} errors and {} warnings:\n\n{}".format(errorCount, warningCount, validationResultString)) + elif infoCount > 0: + FreeCAD.Console.PrintWarning("The Start page is valid HTML, but the W3C sent back {} informative messages:\n{}.".format(infoCount,validationResultString)) + + def sanitize (self, html): + + # Anonymize all local filenames + fileRE = re.compile(r'"file:///.*?"') + html = fileRE.sub(repl=r'"file:///A/B/C"', string=html) + + # Anonymize titles, which are used for mouseover text and might contain document information + titleRE = re.compile(r'title="[\s\S]*?"') # Some titles have newlines in them + html = titleRE.sub(repl=r'title="Y"', string=html) + + # Anonymize the document names, which we display in

        tags + h4RE = re.compile(r'

        .*?

        ') + html = h4RE.sub(repl=r'

        Z

        ', string=html) + + # Remove any simple single-line paragraphs, which might contain document author information, file size information, etc. + pRE = re.compile(r'

        [^<]*?

        ') + html = pRE.sub(repl=r'

        X

        ', string=html) + + return html diff --git a/src/Mod/Surface/Gui/Command.cpp b/src/Mod/Surface/Gui/Command.cpp index abc31b9b23..705a6a822f 100644 --- a/src/Mod/Surface/Gui/Command.cpp +++ b/src/Mod/Surface/Gui/Command.cpp @@ -171,7 +171,7 @@ CmdSurfaceGeomFillSurface::CmdSurfaceGeomFillSurface() sToolTipText = QT_TR_NOOP("Creates a surface from two, three or four boundary edges."); sWhatsThis = "Surface_GeomFillSurface"; sStatusTip = sToolTipText; - sPixmap = "Surface_BSplineSurface"; + sPixmap = "Surface_GeomFillSurface"; } bool CmdSurfaceGeomFillSurface::isActive(void) @@ -237,7 +237,7 @@ CmdSurfaceExtendFace::CmdSurfaceExtendFace() "with its local U and V parameters."); sWhatsThis = "Surface_ExtendFace"; sStatusTip = sToolTipText; - sPixmap = "Surface_Extend"; + sPixmap = "Surface_ExtendFace"; } void CmdSurfaceExtendFace::activated(int) diff --git a/src/Mod/Surface/Gui/Resources/Surface.qrc b/src/Mod/Surface/Gui/Resources/Surface.qrc index c09283aa91..7af306fa62 100644 --- a/src/Mod/Surface/Gui/Resources/Surface.qrc +++ b/src/Mod/Surface/Gui/Resources/Surface.qrc @@ -4,8 +4,9 @@ icons/Surface_BSplineSurface.svg icons/Surface_CurveOnMesh.svg icons/Surface_Cut.svg - icons/Surface_Extend.svg + icons/Surface_ExtendFace.svg icons/Surface_Filling.svg + icons/Surface_GeomFillSurface.svg icons/Surface_Sections.svg icons/Surface_Sewing.svg icons/Surface_Surface.svg diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_BSplineSurface.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_BSplineSurface.svg index 3e9b34bee4..a2644e009c 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_BSplineSurface.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_BSplineSurface.svg @@ -7,2106 +7,111 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" - id="svg3364" + id="svg2985" height="64px" width="64px"> Surface_BSplineSurface + id="title889">Surface_BSplineSurface + id="defs2987"> + id="linearGradient4387"> - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + id="radialGradient3692" + xlink:href="#linearGradient3377" /> + id="linearGradient3377"> + id="stop3379" /> + id="stop3381" /> + id="linearGradient3377-3"> + id="stop3379-8" /> + id="stop3381-3" /> + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)" + gradientUnits="userSpaceOnUse" + id="radialGradient6412" + xlink:href="#linearGradient3377-3" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient3036"> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + id="metadata2990"> @@ -2114,127 +119,75 @@ Surface_BSplineSurface - 2017-04-17 - Nate Miller + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_BSplineSurface.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - A purple curved surface that has a thick, red, highlighted edge. This edge has three vertices indicated as circles as if it was a multipoint spline. It is based on the 'Surface' icon. + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - [vocx] + - surface - curve - spline - points + - - - - - - - - - + d="M 13.816245,52.162367 C 28.338105,17.330258 44.796216,62.805512 56.413699,28.940961" + style="fill:none;stroke:#fce94f;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> - - - - - - - - - - - - + id="path3820-1-9-6" + d="M 5.4725393,24.503343 13.365603,45.716252 C 30.88904,17.984227 44.167745,56.981787 53.473496,28.274182 L 39.51487,7.9294358 C 31.707324,28.541126 16.157527,5.2639846 5.4725393,24.503343 Z" + style="display:inline;fill:none;fill-opacity:1;stroke:#729fcf;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + + + diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_BezierSurface.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_BezierSurface.svg index d6abc7b517..d5179839bc 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_BezierSurface.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_BezierSurface.svg @@ -6,2307 +6,112 @@ xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" - width="64px" + version="1.1" + id="svg2985" height="64px" - id="svg3364" - version="1.1"> + width="64px"> Surface_BezierSurface + id="title889">Surface_BezierSurface + id="defs2987"> + id="linearGradient4387"> - - - - - - + + + + - + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + id="radialGradient3692" + xlink:href="#linearGradient3377" /> + id="linearGradient3377"> + id="stop3379" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="stop3381" /> + id="linearGradient3377-3"> + id="stop3379-8" /> + id="stop3381-3" /> + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)" + gradientUnits="userSpaceOnUse" + id="radialGradient6412" + xlink:href="#linearGradient3377-3" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient3036"> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + id="metadata2990"> @@ -2314,109 +119,91 @@ Surface_BezierSurface - - 2017-04-17 - Nate Miller + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_BezierSurface.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - A purple curved surface, with one thick, red highlighted edge. The endpoints of this edge are marked with squares, which are tied to handles indicating control points for a bezier curve. It is based on the 'Surface' icon. + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - [vocx] + surface - curve - bezier - handles + - - - - - - - - - + style="display:inline;fill:url(#linearGradient2095);fill-opacity:1;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 3.1519208,24.413282 13.801284,52.472481 C 28.323144,17.640372 44.781255,63.115626 56.398738,29.251075 L 38.972508,3.1269928 C 30.809307,30.407148 17.749067,-0.16084869 3.1519208,24.413282 Z" + id="path3820-1-9" /> + style="fill:none;stroke:#172a04;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 13.801284,52.472481 C 28.323144,17.640372 44.781255,63.115626 56.398738,29.251075" + id="path3764" /> + id="path1219" + d="M 13.816245,52.162367 C 28.338105,17.330258 44.796216,62.805512 56.413699,28.940961" + style="fill:none;stroke:#fce94f;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + id="path3867" + d="M 48.620445,57.157022 55.854309,32.153751" + style="fill:#ce5c00;stroke:#ce5c00;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + id="path3820-1-9-6" + d="M 5.4725393,24.503343 13.365603,45.716252 C 31.320659,18.292526 44.167745,56.981787 53.473496,28.274182 L 39.51487,7.9294358 C 31.707324,28.541126 16.157527,5.2639846 5.4725393,24.503343 Z" + style="display:inline;fill:none;fill-opacity:1;stroke:#729fcf;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + + style="fill:#fcaf3e;fill-opacity:1;stroke:#ce5c00;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#8ae234;fill-opacity:1;fill-rule:nonzero;stroke:#172a04;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + style="fill:#8ae234;fill-opacity:1;fill-rule:nonzero;stroke:#172a04;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + style="fill:#fcaf3e;fill-opacity:1;stroke:#ce5c00;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_CurveOnMesh.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_CurveOnMesh.svg index c6bf9bf8ba..d2611f592f 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_CurveOnMesh.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_CurveOnMesh.svg @@ -7,2106 +7,111 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" - id="svg3364" + id="svg2985" height="64px" width="64px"> Surface_CurveOnMesh + id="title866">Surface_CurveOnMesh + id="defs2987"> + id="linearGradient4387"> - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + id="radialGradient3692" + xlink:href="#linearGradient3377" /> + id="linearGradient3377"> + id="stop3379" /> + id="stop3381" /> + id="linearGradient3377-3"> + id="stop3379-8" /> + id="stop3381-3" /> + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)" + gradientUnits="userSpaceOnUse" + id="radialGradient6412" + xlink:href="#linearGradient3377-3" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient3036"> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + id="metadata2990"> @@ -2114,95 +119,86 @@ Surface_CurveOnMesh - 2020-09-30 - [vocx] + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_CurveOnMesh.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - A purple curved surface that has a thick, red, highlighted curve on top of, it in the middle of the shape. The surface has mesh lines. It is based on the 'Surface' icon. + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - - mesh - curve - spline - middle + surface + - - - - - - - - - + d="M 3.1519208,24.413282 17.899691,60.320494 c 14.52186,-34.832109 31.154371,7.93994 42.771854,-25.924611 L 38.972508,3.1269928 C 30.809307,30.407148 17.749067,-0.16084869 3.1519208,24.413282 Z" + style="display:inline;fill:url(#linearGradient2095);fill-opacity:1;stroke:#172a04;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + id="path864" + d="M 13.603223,48.657681 Z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="display:inline;fill:none;fill-opacity:1;stroke:#8ae234;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 5.3492197,24.626663 18.113409,55.643482 C 32.677175,26.678261 49.470489,63.641047 58.406281,34.501823 L 39.63819,7.4361573 C 30.967406,29.589343 16.774125,4.5857267 5.3492197,24.626663 Z" + id="path3820-1-9-6" /> + style="fill:none;stroke:#4e9a06;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 14.551524,40.360731 c 4.206197,-6.239691 10.886783,-4.698556 12.49142,-4.817808" + id="path891" /> + id="path859" + d="M 9.0688148,17.178428 24.67764,49.965681" + style="fill:#0b1521;stroke:#172a04;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> - - - - + id="path861" + d="M 22.497637,14.213624 40.286466,47.088077" + style="fill:none;stroke:#172a04;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + + + + + + diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_Cut.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_Cut.svg index cee2cf4fce..8a16b39b90 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_Cut.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_Cut.svg @@ -7,3683 +7,111 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" - id="svg3364" + id="svg2985" height="64px" width="64px"> Surface_Cut + id="title889">Surface_Cut + id="defs2987"> + id="linearGradient4387"> - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + id="radialGradient3692" + xlink:href="#linearGradient3377" /> + id="linearGradient3377"> + id="stop3379" /> + id="stop3381" /> + id="linearGradient3377-3"> + id="stop3379-8" /> + id="stop3381-3" /> + id="radialGradient6412" + xlink:href="#linearGradient3377-3" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient3036"> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + x1="47" + y1="9" + x2="7" + y2="28" + gradientUnits="userSpaceOnUse" /> + + + + + id="metadata2990"> @@ -3691,86 +119,69 @@ Surface_Cut - - 2017-04-17 - Nate Miller + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_Cut.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - A purple curved surface. A semi-transparent plane cuts the curve in two. The position of the cut is marked with a red thick edge on the curve. It is based on the 'Surface' icon. + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - [vocx] + surface - plane - cut + - - - - - - - - + style="display:inline;fill:url(#linearGradient2095);fill-opacity:1;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 3.1519208,24.413282 17.899691,60.320494 c 14.52186,-34.832109 31.154371,7.93994 42.771854,-25.924611 L 38.972508,3.1269928 C 30.809307,30.407148 17.749067,-0.16084869 3.1519208,24.413282 Z" + id="path3820-1-9" /> + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 13.603223,48.657681 Z" + id="path864" /> + id="path3820-1-9-6" + d="M 5.3492197,24.626663 18.113409,55.643482 C 32.677175,26.678261 49.470489,63.641047 58.406281,34.501823 L 39.63819,7.4361573 C 30.967406,29.589343 16.774125,4.5857267 5.3492197,24.626663 Z" + style="display:inline;fill:none;fill-opacity:1;stroke:#729fcf;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:0.7175;fill:#ef2929;stroke:#280000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 57.281971,22.709091 57.096986,3.6995887 3.097977,11.384814 2.9746572,44.456724 9.7698774,41.423451 C 17.231743,18.328355 39.666029,41.309915 50.005085,20.047261 l 3.975198,3.989905 z" + id="path866" /> + id="path866-8" + d="M 57.281971,22.709091 57.096992,3.6995881 3.097977,11.384814 2.9746572,44.456724 9.7698772,41.423451 C 17.223185,18.328171 39.853014,41.194251 50.390456,19.754377 l 3.296943,4.452353 z" + style="opacity:1;fill:none;stroke:#280000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + id="path857-1" + d="M 50.227282,19.009631 C 40.095257,43.882552 19.635873,17.890993 9.5920156,41.158467" + style="fill:none;stroke:#0b1521;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#fce94f;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 50.140081,19.053231 C 40.008056,43.926152 19.548672,17.934593 9.5048155,41.202067" + id="path857" /> diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_Extend.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_Extend.svg deleted file mode 100644 index fb03b99117..0000000000 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_Extend.svg +++ /dev/null @@ -1,5767 +0,0 @@ - - - Surface_Extend - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Surface_Extend - 2020-09-30 - - - [vocx] - - - - - CC-BY-SA 4.0 - - - - - FreeCAD - - - http://www.freecadweb.org/wiki/index.php?title=Artwork - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_Extend.svg - A purple curved surface that contains a smaller surface. The smaller surface has Its four edges in thick, red highlighted. Gray lines extend from the smaller, inner surface to the exterior one. It is based on the 'Surface' icon. - - - - - - - - - surface - curve - extrapolation - highlights - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_ExtendFace.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_ExtendFace.svg new file mode 100644 index 0000000000..d7af2c7f2d --- /dev/null +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_ExtendFace.svg @@ -0,0 +1,211 @@ + + + Surface_ExtendFace + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Surface_ExtendFace + + + [bitacovir] + + + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + + + + + + surface + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_Filling.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_Filling.svg index 791cf3cff3..5daf7464f8 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_Filling.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_Filling.svg @@ -6,1795 +6,89 @@ xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" - version="1.1" - id="svg3364" + width="64px" height="64px" - width="64px"> + id="svg2985" + version="1.1"> Surface_Filling + id="title889">Surface_Filling + id="defs2987"> - - - - - - + id="linearGradient4387"> + id="stop4389" /> + id="stop4391" /> - - - - - - - - - - - - - - - - - + id="linearGradient6321"> + id="stop6323" /> - + id="stop6325" /> - - - - - - - - - + gradientTransform="translate(-0.23443224,0.23443198)" /> + id="linearGradient3377"> + style="stop-color:#faff2b;stop-opacity:1;" /> + style="stop-color:#ffaa00;stop-opacity:1;" /> + id="linearGradient3377-3"> + style="stop-color:#faff2b;stop-opacity:1;" /> + style="stop-color:#ffaa00;stop-opacity:1;" /> + gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)" + cx="45.883327" + cy="28.869568" + fx="45.883327" + fy="28.869568" + r="19.467436" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient3036"> + + + - - - - - - - - - - + xlink:href="#linearGradient1189" + gradientTransform="matrix(0.96812402,0,0,0.96755864,-0.72057496,-2.6783592)" /> + + + + + id="metadata2990"> @@ -1913,100 +119,61 @@ Surface_Filling - 2017-04-17 - Nate Miller + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - http://www.freecadweb.org/wiki/index.php?title=Artwork - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_Filling.svg - A purple curved surface. Its four edges are thick and highlighted in red. It is based on the 'Surface' icon. + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - [vocx] + - surface - curve - edges - highlights + - - - - - - - - + id="path3820-1-9" + d="M 3.1519208,24.413282 17.899691,60.320494 c 14.52186,-34.832109 31.154371,7.93994 42.771854,-25.924611 L 38.972508,3.1269928 C 30.809307,30.407148 17.749067,-0.16084869 3.1519208,24.413282 Z" + style="display:inline;fill:url(#linearGradient2095);fill-opacity:1;stroke:#302b00;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="display:inline;fill:none;fill-opacity:1;stroke:#fce94f;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 6.0274776,24.811642 18.175069,54.286966 C 33.540412,25.445065 49.03887,62.222871 57.666363,34.995102 L 39.76151,8.7926732 C 30.227488,30.267601 17.452383,4.7707061 6.0274776,24.811642 Z" + id="path3820-1-9-6" /> + style="fill:none;stroke:#302b00;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 8.3549048,24.972224 18.621264,49.266191 C 31.716699,27.213603 50.17526,55.741537 55.247193,35.269413 L 40.202199,12.856071 C 30.336022,29.759966 16.753608,10.080699 8.3549048,24.972224 Z" + id="path857" /> + style="fill:none;stroke:#0b1521;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 50.052882,19.009632 C 39.920857,43.882553 19.461473,17.890994 9.4176161,41.158468" + id="path857-1" /> - - - - - + id="path857-3" + d="M 49.965681,19.053232 C 39.833656,43.926153 19.374272,17.934594 9.3304161,41.202068" + style="fill:none;stroke:#fce94f;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_GeomFillSurface.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_GeomFillSurface.svg new file mode 100644 index 0000000000..2c6e085a7b --- /dev/null +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_GeomFillSurface.svg @@ -0,0 +1,171 @@ + + + Surface_GeomFillSurface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Surface_GeomFillSurface + + + [bitacovir] + + + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + + + + + + surface + + + + + + + + + + + + diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_Sections.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_Sections.svg index 95c5529f4a..f84f5b73fe 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_Sections.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_Sections.svg @@ -6,2754 +6,150 @@ xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" - width="64px" + version="1.1" + id="svg2985" height="64px" - id="svg3364" - version="1.1"> + width="64px"> Surface_Sections + id="title889">Surface_Sections + id="defs2987"> + id="linearGradient873"> + id="stop869" /> - - + id="stop871" /> + id="linearGradient863"> + + + + + + + + - + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + id="radialGradient3692" + xlink:href="#linearGradient3377" /> + id="linearGradient3377"> + id="stop3379" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="stop3381" /> + id="linearGradient3377-3"> + id="stop3379-8" /> + id="stop3381-3" /> + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)" + gradientUnits="userSpaceOnUse" + id="radialGradient6412" + xlink:href="#linearGradient3377-3" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient3036"> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + x1="47" + y1="9" + x2="7" + y2="28" + gradientUnits="userSpaceOnUse" /> + + + + + + + id="metadata2990"> @@ -2761,139 +157,73 @@ Surface_Sections - - 2020-09-29 - [vocx] + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_Sections.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + + + surface - curve - sections - edges - A purple surface made with several highlighted red edges which represent transversal sections of the surface. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_Sewing.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_Sewing.svg index cf8e3b2e59..7411c1dcc6 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_Sewing.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_Sewing.svg @@ -7,1806 +7,111 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" - id="svg3364" + id="svg2985" height="64px" width="64px"> Surface_Sewing + id="title889">Surface_Sewing + id="defs2987"> + id="linearGradient4387"> - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + r="19.467436" + fy="28.869568" + fx="45.883327" + cy="28.869568" + cx="45.883327" + id="radialGradient3692" + xlink:href="#linearGradient3377" /> + id="linearGradient3377"> + id="stop3379" /> + id="stop3381" /> + id="linearGradient3377-3"> + id="stop3379-8" /> + id="stop3381-3" /> + id="radialGradient6412" + xlink:href="#linearGradient3377-3" /> + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient1189"> + + + + id="metadata2990"> @@ -1814,99 +119,73 @@ Surface_Sewing - - 2017-04-17 - Nate Miller + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_Sewing.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - A purple curved surface, with black edges, and one edge passing through the middle, dividing the curve in two parts. Six straight lines cross the middle edge, indicating stitches. It is based on the 'Surface' icon. + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - [vocx] + surface - curve - edges + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_Surface.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_Surface.svg index a0f51cfc3f..b3217de204 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_Surface.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_Surface.svg @@ -7,1697 +7,111 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" - id="svg3364" + id="svg2985" height="64px" width="64px"> Surface_Surface + id="title889">Surface_Surface + id="defs2987"> + id="linearGradient4387"> - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + id="linearGradient3377"> + id="stop3379" /> + id="stop3381" /> + id="linearGradient3377-3"> + id="stop3379-8" /> + id="stop3381-3" /> + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient1189"> + + + + id="metadata2990"> @@ -1705,75 +119,53 @@ Surface_Surface - 2017-04-17 - Nate Miller + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_Surface.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - A purple curved surface, with a linear gradient, and some highlights in light and dark purple. - + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - [vocx] + surface - curve - wave + - - - - - - - - - - - - - + + + + + diff --git a/src/Mod/Surface/Gui/Resources/icons/Surface_Workbench.svg b/src/Mod/Surface/Gui/Resources/icons/Surface_Workbench.svg index 35053ff75c..44e6791ec6 100644 --- a/src/Mod/Surface/Gui/Resources/icons/Surface_Workbench.svg +++ b/src/Mod/Surface/Gui/Resources/icons/Surface_Workbench.svg @@ -6,1698 +6,112 @@ xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" - version="1.1" - id="svg3364" + width="64px" height="64px" - width="64px"> + id="svg2985" + version="1.1"> Surface_Workbench + id="title889">Surface_Workbench + id="defs2987"> - - - - - - + id="linearGradient4387"> + id="stop4389" /> + id="stop4391" /> - - - - - - - - - - - - - - - - - + id="linearGradient6321"> + id="stop6323" /> - - - - - - - - - + id="stop6325" /> + xlink:href="#linearGradient3377" + id="radialGradient3692" + cx="45.883327" + cy="28.869568" + fx="45.883327" + fy="28.869568" + r="19.467436" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-0.23443224,0.23443198)" /> + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + x2="7" + y1="9" + x1="47" + id="linearGradient2095" + xlink:href="#linearGradient1189" + gradientTransform="matrix(0.96812402,0,0,0.96755864,-0.72057496,-2.6783592)" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient1189"> + + + + id="metadata2990"> @@ -1705,75 +119,53 @@ Surface_Workbench - 2017-04-17 - Nate Miller + [bitacovir] - - - CC-BY-SA 4.0 - - + Part_Shape_from_Mesh + 2020/10/03 + http://www.freecadweb.org/wiki/index.php?title=Artwork FreeCAD - FreeCAD/src/Mod/Surface/Gui/Resources/icons/Surface_Workbench.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - A purple curved surface, with a linear gradient, and some highlights in light and dark purple. It is based on the 'Surface' icon. - + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html - [vocx] + surface - curve - wave + - - - - - - - - - - - - - + + + + + diff --git a/src/Mod/Surface/Gui/TaskFilling.cpp b/src/Mod/Surface/Gui/TaskFilling.cpp index e9ec391cef..3b07bee830 100644 --- a/src/Mod/Surface/Gui/TaskFilling.cpp +++ b/src/Mod/Surface/Gui/TaskFilling.cpp @@ -91,13 +91,7 @@ bool ViewProviderFilling::setEdit(int ModNum) void ViewProviderFilling::unsetEdit(int ModNum) { - if (ModNum == ViewProvider::Default) { - // when pressing ESC make sure to close the dialog - QTimer::singleShot(0, &Gui::Control(), SLOT(closeDialog())); - } - else { - PartGui::ViewProviderSpline::unsetEdit(ModNum); - } + PartGui::ViewProviderSpline::unsetEdit(ModNum); } QIcon ViewProviderFilling::getIcon(void) const @@ -272,6 +266,7 @@ FillingPanel::FillingPanel(ViewProviderFilling* vp, Surface::Filling* obj) // Create context menu QAction* action = new QAction(tr("Remove"), this); action->setShortcut(QString::fromLatin1("Del")); + action->setShortcutContext(Qt::WidgetShortcut); ui->listBoundary->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onDeleteEdge())); ui->listBoundary->setContextMenuPolicy(Qt::ActionsContextMenu); @@ -901,6 +896,11 @@ void TaskFilling::open() widget3->open(); } +void TaskFilling::closed() +{ + widget1->reject(); +} + bool TaskFilling::accept() { bool ok = widget1->accept(); diff --git a/src/Mod/Surface/Gui/TaskFilling.h b/src/Mod/Surface/Gui/TaskFilling.h index 5d5d825444..c069187adc 100644 --- a/src/Mod/Surface/Gui/TaskFilling.h +++ b/src/Mod/Surface/Gui/TaskFilling.h @@ -116,6 +116,7 @@ public: public: void open(); + void closed(); bool accept(); bool reject(); diff --git a/src/Mod/Surface/Gui/TaskFillingEdge.cpp b/src/Mod/Surface/Gui/TaskFillingEdge.cpp index 7ea642fbf4..f0bb1f71d2 100644 --- a/src/Mod/Surface/Gui/TaskFillingEdge.cpp +++ b/src/Mod/Surface/Gui/TaskFillingEdge.cpp @@ -128,6 +128,7 @@ FillingEdgePanel::FillingEdgePanel(ViewProviderFilling* vp, Surface::Filling* ob // Create context menu QAction* action = new QAction(tr("Remove"), this); action->setShortcut(QString::fromLatin1("Del")); + action->setShortcutContext(Qt::WidgetShortcut); ui->listUnbound->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onDeleteUnboundEdge())); ui->listUnbound->setContextMenuPolicy(Qt::ActionsContextMenu); diff --git a/src/Mod/Surface/Gui/TaskFillingVertex.cpp b/src/Mod/Surface/Gui/TaskFillingVertex.cpp index b39d0fd334..4bfa3a6be6 100644 --- a/src/Mod/Surface/Gui/TaskFillingVertex.cpp +++ b/src/Mod/Surface/Gui/TaskFillingVertex.cpp @@ -127,6 +127,7 @@ FillingVertexPanel::FillingVertexPanel(ViewProviderFilling* vp, Surface::Filling // Create context menu QAction* action = new QAction(tr("Remove"), this); action->setShortcut(QString::fromLatin1("Del")); + action->setShortcutContext(Qt::WidgetShortcut); ui->listFreeVertex->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onDeleteVertex())); ui->listFreeVertex->setContextMenuPolicy(Qt::ActionsContextMenu); diff --git a/src/Mod/TechDraw/App/AppTechDrawPy.cpp b/src/Mod/TechDraw/App/AppTechDrawPy.cpp index 7afe7011f9..ed07a2a5fe 100644 --- a/src/Mod/TechDraw/App/AppTechDrawPy.cpp +++ b/src/Mod/TechDraw/App/AppTechDrawPy.cpp @@ -29,8 +29,10 @@ #include #include #include +#include #include #include +#include #include #endif @@ -51,7 +53,9 @@ #include #include #include +#include #include +#include #include #include @@ -71,6 +75,8 @@ #include "DrawProjGroup.h" #include "DrawProjGroupItem.h" #include "DrawDimHelper.h" +#include "HatchLine.h" +#include "DrawGeomHatch.h" namespace TechDraw { //module level static C++ functions go here @@ -79,7 +85,9 @@ namespace TechDraw { using Part::TopoShape; using Part::TopoShapePy; using Part::TopoShapeEdgePy; +using Part::TopoShapeFacePy; using Part::TopoShapeWirePy; +using Part::TopoShapeCompoundPy; using Import::ImpExpDxfWrite; namespace TechDraw { @@ -122,7 +130,9 @@ public: add_varargs_method("makeDistanceDim3d",&Module::makeDistanceDim3d, "makeDistanceDim(DrawViewPart, dimType, 3dFromPoint, 3dToPoint) -- draw a Length dimension between fromPoint to toPoint. FromPoint and toPoint are unscaled 3d model points. dimType is one of ['Distance', 'DistanceX', 'DistanceY'." ); - + add_varargs_method("makeGeomHatch",&Module::makeGeomHatch, + "makeGeomHatch(face, [patScale], [patName], [patFile]) -- draw a geom hatch on a given face, using optionally the given scale (default 1) and a given pattern name (ex. Diamond) and .pat file (the default pattern name and/or .pat files set in preferences are used if none are given). Returns a Part compound shape." + ); initialize("This is a module for making drawings"); // register with Python } virtual ~Module() {} @@ -797,8 +807,8 @@ private: Py::Object makeDistanceDim(const Py::Tuple& args) { - //points come in unscaled,but makeDistDim unscales them so we need to prescale here. - //makeDistDim was built for extent dims which work from scaled geometry + //points come in unscaled,but makeDistDim unscales them so we need to prescale here. + //makeDistDim was built for extent dims which work from scaled geometry PyObject* pDvp; PyObject* pDimType; PyObject* pFrom; @@ -835,7 +845,7 @@ private: if (PyObject_TypeCheck(pTo, &(Base::VectorPy::Type))) { to = static_cast(pTo)->value(); } - DrawViewDimension* dvd = + DrawViewDimension* dvd = DrawDimHelper::makeDistDim(dvp, dimType, DrawUtil::invertY(from), @@ -885,7 +895,7 @@ private: //3d points are not scaled from = DrawUtil::invertY(dvp->projectPoint(from)); to = DrawUtil::invertY(dvp->projectPoint(to)); - //DrawViewDimension* = + //DrawViewDimension* = DrawDimHelper::makeDistDim(dvp, dimType, from, @@ -893,6 +903,88 @@ private: return Py::None(); } + + + Py::Object makeGeomHatch(const Py::Tuple& args) + { + PyObject* pFace; + double scale = 1.0; + char* pPatName = ""; + char* pPatFile = ""; + TechDraw::DrawViewPart* source = nullptr; + TopoDS_Face face; + + if (!PyArg_ParseTuple(args.ptr(), "O|detet", &pFace, &scale, "utf-8", &pPatName, "utf-8", &pPatFile)) { + throw Py::TypeError("expected (face, [scale], [patName], [patFile])"); + } + std::string patName = std::string(pPatName); + std::string patFile = std::string(pPatFile); + if (PyObject_TypeCheck(pFace, &(TopoShapeFacePy::Type))) { + const TopoDS_Shape& sh = static_cast(pFace)->getTopoShapePtr()->getShape(); + face = TopoDS::Face(sh); + } + else { + throw Py::TypeError("first argument must be a Part.Face instance"); + } + if (patName.empty()) { + patName = TechDraw::DrawGeomHatch::prefGeomHatchName(); + } + if (patFile.empty()) { + patFile = TechDraw::DrawGeomHatch::prefGeomHatchFile(); + } + Base::FileInfo fi(patFile); + if (!fi.isReadable()) { + Base::Console().Error(".pat File: %s is not readable\n",patFile.c_str()); + return Py::None(); + } + std::vector specs = TechDraw::DrawGeomHatch::getDecodedSpecsFromFile(patFile, patName); + std::vector lineSets; + for (auto& hl: specs) { + TechDraw::LineSet ls; + ls.setPATLineSpec(hl); + lineSets.push_back(ls); + } + std::vector lsresult = TechDraw::DrawGeomHatch::getTrimmedLines(source, lineSets, face, scale); + if (!lsresult.empty()) { + /* below code returns a list of edges, but probably slower to handle + Py::List result; + try { + for (auto& lsr:lsresult) { + std::vector edgeList = lsr.getEdges(); + for (auto& edge:edgeList) { + PyObject* pyedge = new TopoShapeEdgePy(new TopoShape(edge)); + result.append(Py::asObject(pyedge)); + } + } + } + catch (Base::Exception &e) { + throw Py::Exception(Base::BaseExceptionFreeCADError, e.what()); + } + return result; + */ + BRep_Builder builder; + TopoDS_Compound comp; + builder.MakeCompound(comp); + try { + for (auto& lsr:lsresult) { + std::vector edgeList = lsr.getEdges(); + for (auto& edge:edgeList) { + if (!edge.IsNull()) { + builder.Add(comp, edge); + } + } + } + } + catch (Base::Exception &e) { + throw Py::Exception(Base::BaseExceptionFreeCADError, e.what()); + } + PyObject* pycomp = new TopoShapeCompoundPy(new TopoShape(comp)); + return Py::asObject(pycomp); + } + return Py::None(); + } + + }; PyObject* initModule() diff --git a/src/Mod/TechDraw/App/DrawViewDimension.cpp b/src/Mod/TechDraw/App/DrawViewDimension.cpp index f848373005..d0f25e58ee 100644 --- a/src/Mod/TechDraw/App/DrawViewDimension.cpp +++ b/src/Mod/TechDraw/App/DrawViewDimension.cpp @@ -110,8 +110,9 @@ DrawViewDimension::DrawViewDimension(void) ADD_PROPERTY_TYPE(References3D, (0,0), "", (App::Prop_None), "3D Geometry References"); References3D.setScope(App::LinkScope::Global); - ADD_PROPERTY_TYPE(FormatSpec, (getDefaultFormatSpec()), "Format", App::Prop_Output,"Dimension Format"); - ADD_PROPERTY_TYPE(FormatSpecTolerance, (getDefaultFormatSpec(true)), "Format", App::Prop_Output, "Dimension tolerance format"); + ADD_PROPERTY_TYPE(FormatSpec, (getDefaultFormatSpec()), "Format", App::Prop_Output,"Dimension format"); + ADD_PROPERTY_TYPE(FormatSpecOverTolerance, (getDefaultFormatSpec(true)), "Format", App::Prop_Output, "Dimension overtolerance format"); + ADD_PROPERTY_TYPE(FormatSpecUnderTolerance, (getDefaultFormatSpec(true)), "Format", App::Prop_Output, "Dimension undertolerance format"); ADD_PROPERTY_TYPE(Arbitrary,(false), "Format", App::Prop_Output, "Value overridden by user"); ADD_PROPERTY_TYPE(ArbitraryTolerances, (false), "Format", App::Prop_Output, "Tolerance values overridden by user"); @@ -142,6 +143,7 @@ DrawViewDimension::DrawViewDimension(void) // by default EqualTolerance is true, thus make UnderTolerance read-only UnderTolerance.setStatus(App::Property::ReadOnly, true); + FormatSpecUnderTolerance.setStatus(App::Property::ReadOnly, true); measurement = new Measure::Measurement(); //TODO: should have better initial datumLabel position than (0,0) in the DVP?? something closer to the object being measured? @@ -212,11 +214,19 @@ void DrawViewDimension::onChanged(const App::Property* prop) UnderTolerance.setValue(0.0); OverTolerance.setReadOnly(true); UnderTolerance.setReadOnly(true); + FormatSpecOverTolerance.setReadOnly(true); + FormatSpecUnderTolerance.setReadOnly(true); + ArbitraryTolerances.setValue(false); + ArbitraryTolerances.setReadOnly(true); } else { OverTolerance.setReadOnly(false); - if (!EqualTolerance.getValue()) + FormatSpecOverTolerance.setReadOnly(false); + ArbitraryTolerances.setReadOnly(false); + if (!EqualTolerance.getValue()) { UnderTolerance.setReadOnly(false); + FormatSpecUnderTolerance.setReadOnly(false); + } } requestPaint(); } @@ -232,11 +242,15 @@ void DrawViewDimension::onChanged(const App::Property* prop) UnderTolerance.setValue(-1.0 * OverTolerance.getValue()); UnderTolerance.setUnit(OverTolerance.getUnit()); UnderTolerance.setReadOnly(true); + FormatSpecUnderTolerance.setValue(FormatSpecOverTolerance.getValue()); + FormatSpecUnderTolerance.setReadOnly(true); } else { OverTolerance.setConstraints(&ToleranceConstraint); - if (!TheoreticalExact.getValue()) + if (!TheoreticalExact.getValue()) { UnderTolerance.setReadOnly(false); + FormatSpecUnderTolerance.setReadOnly(false); + } } requestPaint(); } @@ -248,8 +262,21 @@ void DrawViewDimension::onChanged(const App::Property* prop) } requestPaint(); } + else if (prop == &FormatSpecOverTolerance) { + if (!ArbitraryTolerances.getValue()) { + FormatSpecUnderTolerance.setValue(FormatSpecOverTolerance.getValue()); + } + requestPaint(); + } + else if (prop == &FormatSpecUnderTolerance) { + if (!ArbitraryTolerances.getValue()) { + FormatSpecOverTolerance.setValue(FormatSpecUnderTolerance.getValue()); + } + requestPaint(); + } else if ( (prop == &FormatSpec) || (prop == &Arbitrary) || + (prop == &ArbitraryTolerances) || (prop == &MeasureType) || (prop == &UnderTolerance) || (prop == &Inverted) ) { @@ -322,6 +349,9 @@ short DrawViewDimension::mustExecute() const Type.isTouched() || FormatSpec.isTouched() || Arbitrary.isTouched() || + FormatSpecOverTolerance.isTouched() || + FormatSpecUnderTolerance.isTouched() || + ArbitraryTolerances.isTouched() || MeasureType.isTouched() || TheoreticalExact.isTouched() || EqualTolerance.isTouched() || @@ -850,7 +880,7 @@ std::string DrawViewDimension::formatValue(qreal value, QString qFormatSpec, int std::string DrawViewDimension::getFormattedToleranceValue(int partial) { - QString FormatSpec = QString::fromUtf8(FormatSpecTolerance.getStrValue().data()); + QString FormatSpec = QString::fromUtf8(FormatSpecOverTolerance.getStrValue().data()); QString ToleranceString; if (ArbitraryTolerances.getValue()) @@ -863,25 +893,26 @@ std::string DrawViewDimension::getFormattedToleranceValue(int partial) std::pair DrawViewDimension::getFormattedToleranceValues(int partial) { - QString FormatSpec = QString::fromUtf8(FormatSpecTolerance.getStrValue().data()); + QString underFormatSpec = QString::fromUtf8(FormatSpecUnderTolerance.getStrValue().data()); + QString overFormatSpec = QString::fromUtf8(FormatSpecOverTolerance.getStrValue().data()); std::pair tolerances; QString underTolerance, overTolerance; if (ArbitraryTolerances.getValue()) { - underTolerance = FormatSpec; - overTolerance = FormatSpec; + underTolerance = underFormatSpec; + overTolerance = overFormatSpec; } else { if (DrawUtil::fpCompare(UnderTolerance.getValue(), 0.0)) { underTolerance = QString::fromUtf8(formatValue(UnderTolerance.getValue(), QString::fromUtf8("%.0f"), partial).c_str()); } else { - underTolerance = QString::fromUtf8(formatValue(UnderTolerance.getValue(), FormatSpec, partial).c_str()); + underTolerance = QString::fromUtf8(formatValue(UnderTolerance.getValue(), underFormatSpec, partial).c_str()); } if (DrawUtil::fpCompare(OverTolerance.getValue(), 0.0)) { overTolerance = QString::fromUtf8(formatValue(OverTolerance.getValue(), QString::fromUtf8("%.0f"), partial).c_str()); } else { - overTolerance = QString::fromUtf8(formatValue(OverTolerance.getValue(), FormatSpec, partial).c_str()); + overTolerance = QString::fromUtf8(formatValue(OverTolerance.getValue(), overFormatSpec, partial).c_str()); } } @@ -895,10 +926,38 @@ std::string DrawViewDimension::getFormattedDimensionValue(int partial) { QString qFormatSpec = QString::fromUtf8(FormatSpec.getStrValue().data()); - if (Arbitrary.getValue()) { + if ( (Arbitrary.getValue() && !EqualTolerance.getValue()) + || (Arbitrary.getValue() && TheoreticalExact.getValue()) ) { return FormatSpec.getStrValue(); } + // if there is an equal over-/undertolerance and not theoretically exact, add the tolerance to dimension + if (EqualTolerance.getValue() && !TheoreticalExact.getValue() && + (!DrawUtil::fpCompare(OverTolerance.getValue(), 0.0) || ArbitraryTolerances.getValue())) { + QString labelText = QString::fromUtf8(formatValue(getDimValue(), qFormatSpec, 1).c_str()); //just the number pref/spec/suf + QString unitText = QString::fromUtf8(formatValue(getDimValue(), qFormatSpec, 2).c_str()); //just the unit + QString tolerance = QString::fromStdString(getFormattedToleranceValue(1).c_str()); + QString result; + if (Arbitrary.getValue()) { + labelText = QString::fromStdString(FormatSpec.getStrValue()); + unitText = QString(); + } + // tolerance might start with a plus sign that we don't want, so cut it off + if (tolerance.at(0) == QChar::fromLatin1('+')) + tolerance.remove(0, 1); + if ((Type.isValue("Angle")) || (Type.isValue("Angle3Pt"))) { + result = labelText + unitText + QString::fromUtf8(" \xC2\xB1 ") + tolerance; + } else { + // add the tolerance to the dimension using the ± sign + result = labelText + QString::fromUtf8(" \xC2\xB1 ") + tolerance; + } + if (partial == 2) { + result = unitText; + } + + return result.toStdString(); + } + return formatValue(getDimValue(), qFormatSpec, partial); } @@ -1347,8 +1406,9 @@ bool DrawViewDimension::has3DReferences(void) const bool DrawViewDimension::hasOverUnderTolerance(void) const { bool result = false; - if (!DrawUtil::fpCompare(OverTolerance.getValue(), 0.0) || - !DrawUtil::fpCompare(UnderTolerance.getValue(), 0.0) ) { + if (ArbitraryTolerances.getValue() || + !DrawUtil::fpCompare(OverTolerance.getValue(), 0.0) || + !DrawUtil::fpCompare(UnderTolerance.getValue(), 0.0)) { result = true; } return result; diff --git a/src/Mod/TechDraw/App/DrawViewDimension.h b/src/Mod/TechDraw/App/DrawViewDimension.h index 8876bb5356..c4efe316ec 100644 --- a/src/Mod/TechDraw/App/DrawViewDimension.h +++ b/src/Mod/TechDraw/App/DrawViewDimension.h @@ -102,7 +102,8 @@ public: App::PropertyBool TheoreticalExact; App::PropertyBool Inverted; App::PropertyString FormatSpec; - App::PropertyString FormatSpecTolerance; + App::PropertyString FormatSpecOverTolerance; + App::PropertyString FormatSpecUnderTolerance; App::PropertyBool Arbitrary; App::PropertyBool ArbitraryTolerances; App::PropertyBool EqualTolerance; diff --git a/src/Mod/TechDraw/App/QDomNodeModel.cpp b/src/Mod/TechDraw/App/QDomNodeModel.cpp index 1ecfa48d04..25a704a174 100644 --- a/src/Mod/TechDraw/App/QDomNodeModel.cpp +++ b/src/Mod/TechDraw/App/QDomNodeModel.cpp @@ -195,9 +195,10 @@ QXmlName QDomNodeModel::name ( const QXmlNodeModelIndex & ni ) const return QXmlName(m_Pool, n.nodeName(), QString(), QString()); } -QVector QDomNodeModel::namespaceBindings(const QXmlNodeModelIndex & ni ) const +QVector QDomNodeModel::namespaceBindings(const QXmlNodeModelIndex & ni) const { QDomNode n = toDomNode(ni); + bool xmlNamespaceWasDefined = false; QVector res; while (!n.isNull()) @@ -219,14 +220,27 @@ QVector QDomNodeModel::namespaceBindings(const QXmlNodeModelIndex & ni for (x = 0; x < res.size(); ++x) if (res.at(x).prefix(m_Pool) == p) break; - if (x >= res.size()) + if (x >= res.size()) { res.append(QXmlName(m_Pool, QString::fromUtf8("xmlns"), attrs.item(i).nodeValue(), p)); - } + if (p == QString::fromLatin1("xml")) + xmlNamespaceWasDefined = true; + } + } } n = n.parentNode(); } + // Per the XML standard: + // "The prefix xml is by definition bound to the namespace name http://www.w3.org/XML/1998/namespace. It MAY, but + // need not, be declared, and MUST NOT be bound to any other namespace name. Other prefixes MUST NOT be bound to + // this namespace name, and it MUST NOT be declared as the default namespace." + // + // If the document does not specifically include this namespace, add it now: + if (!xmlNamespaceWasDefined) { + res.append(QXmlName(m_Pool, QString::fromUtf8("xmlns"), QString::fromLatin1("http://www.w3.org/XML/1998/namespace"), QString::fromLatin1("xml"))); + } + return res; } diff --git a/src/Mod/TechDraw/App/ShapeExtractor.cpp b/src/Mod/TechDraw/App/ShapeExtractor.cpp index e2d070683d..292f146a14 100644 --- a/src/Mod/TechDraw/App/ShapeExtractor.cpp +++ b/src/Mod/TechDraw/App/ShapeExtractor.cpp @@ -163,32 +163,44 @@ std::vector ShapeExtractor::getXShapes(const App::Link* xLink) return xSourceShapes; } + bool needsTransform = false; std::vector children = xLink->getLinkedChildren(); - Base::Placement linkPlm; + Base::Placement linkPlm; // default constructor is an identity placement, i.e. no rotation nor translation if (xLink->hasPlacement()) { linkPlm = xLink->getLinkPlacementProperty()->getValue(); + needsTransform = true; + } + Base::Matrix4D linkScale; // default constructor is an identity matrix, possibly scale it with link's scale + if(xLink->getScaleProperty() || xLink->getScaleVectorProperty()) { + linkScale.scale(xLink->getScaleVector()); + needsTransform = true; } + Base::Matrix4D netTransform; if (!children.empty()) { for (auto& l:children) { -//What to do with LinkGroup??? -// if (l->getTypeId().isDerivedFrom(App::LinkGroup::getClassTypeId())) { -// Base::Console().Message("SE::getXShapes - found a LinkGroup\n"); -// } + bool childNeedsTransform = false; Base::Placement childPlm; + Base::Matrix4D childScale; if (l->getTypeId().isDerivedFrom(App::LinkElement::getClassTypeId())) { App::LinkElement* cLinkElem = static_cast(l); if (cLinkElem->hasPlacement()) { childPlm = cLinkElem->getLinkPlacementProperty()->getValue(); + childNeedsTransform = true; + } + if(cLinkElem->getScaleProperty() || cLinkElem->getScaleVectorProperty()) { + childScale.scale(cLinkElem->getScaleVector()); + childNeedsTransform = true; } } auto shape = Part::Feature::getShape(l); if(!shape.IsNull()) { - Base::Placement netPlm = linkPlm; - netPlm *= childPlm; - if (xLink->hasPlacement()) { + if (needsTransform || childNeedsTransform) { + // Multiplication is associative, but the braces show the idea of combining the two transforms: + // ( link placement and scale ) combined to ( child placement and scale ) + netTransform = (linkPlm.toMatrix() * linkScale) * (childPlm.toMatrix() * childScale); Part::TopoShape ts(shape); - ts.setPlacement(netPlm); + ts.transformGeometry(netTransform); shape = ts.getShape(); } if (shape.ShapeType() > TopAbs_COMPSOLID) { //simple shape @@ -209,9 +221,11 @@ std::vector ShapeExtractor::getXShapes(const App::Link* xLink) if (link != nullptr) { auto shape = Part::Feature::getShape(link); if(!shape.IsNull()) { - if (xLink->hasPlacement()) { + if (needsTransform) { + // Transform is just link placement and scale, no child objects + netTransform = linkPlm.toMatrix() * linkScale; Part::TopoShape ts(shape); - ts.setPlacement(linkPlm); + ts.transformGeometry(netTransform); shape = ts.getShape(); } diff --git a/src/Mod/TechDraw/Gui/Command.cpp b/src/Mod/TechDraw/Gui/Command.cpp index 687fd905ee..ea9bacd75e 100644 --- a/src/Mod/TechDraw/Gui/Command.cpp +++ b/src/Mod/TechDraw/Gui/Command.cpp @@ -183,11 +183,13 @@ CmdTechDrawPageTemplate::CmdTechDrawPageTemplate() void CmdTechDrawPageTemplate::activated(int iMsg) { Q_UNUSED(iMsg); + QString work_dir = Gui::FileDialog::getWorkingDirectory(); QString templateDir = Preferences::defaultTemplateDir(); QString templateFileName = Gui::FileDialog::getOpenFileName(Gui::getMainWindow(), QString::fromUtf8(QT_TR_NOOP("Select a Template File")), templateDir, QString::fromUtf8(QT_TR_NOOP("Template (*.svg *.dxf)"))); + Gui::FileDialog::setWorkingDirectory(work_dir); // Don't overwrite WD with templateDir if (templateFileName.isEmpty()) { return; @@ -316,6 +318,7 @@ void CmdTechDrawView::activated(int iMsg) resolve, single); for (auto& sel: selection) { + bool is_linked = false; auto obj = sel.getObject(); if (obj->isDerivedFrom(TechDraw::DrawPage::getClassTypeId()) ) { continue; @@ -323,6 +326,27 @@ void CmdTechDrawView::activated(int iMsg) if ( obj->isDerivedFrom(App::LinkElement::getClassTypeId()) || obj->isDerivedFrom(App::LinkGroup::getClassTypeId()) || obj->isDerivedFrom(App::Link::getClassTypeId()) ) { + is_linked = true; + } + // If parent of the obj is a link to another document, we possibly need to treat non-link obj as linked, too + // 1st, is obj in another document? + if (obj->getDocument() != this->getDocument()) { + std::set parents = obj->getInListEx(true); + for (auto &parent: parents) { + // Only consider parents in the current document, i.e. possible links in this View's document + if (parent->getDocument() != this->getDocument()) { + continue; + } + // 2nd, do we really have a link to obj? + if (parent->isDerivedFrom(App::LinkElement::getClassTypeId()) || + parent->isDerivedFrom(App::LinkGroup::getClassTypeId()) || + parent->isDerivedFrom(App::Link::getClassTypeId())) { + // We have a link chain from this document to obj, and obj is in another document -> it's an XLink target + is_linked = true; + } + } + } + if (is_linked) { xShapes.push_back(obj); continue; } @@ -572,6 +596,7 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) resolve, single); for (auto& sel: selection) { + bool is_linked = false; auto obj = sel.getObject(); if (obj->isDerivedFrom(TechDraw::DrawPage::getClassTypeId()) ) { continue; @@ -579,6 +604,27 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) if ( obj->isDerivedFrom(App::LinkElement::getClassTypeId()) || obj->isDerivedFrom(App::LinkGroup::getClassTypeId()) || obj->isDerivedFrom(App::Link::getClassTypeId()) ) { + is_linked = true; + } + // If parent of the obj is a link to another document, we possibly need to treat non-link obj as linked, too + // 1st, is obj in another document? + if (obj->getDocument() != this->getDocument()) { + std::set parents = obj->getInListEx(true); + for (auto &parent: parents) { + // Only consider parents in the current document, i.e. possible links in this View's document + if (parent->getDocument() != this->getDocument()) { + continue; + } + // 2nd, do we really have a link to obj? + if (parent->isDerivedFrom(App::LinkElement::getClassTypeId()) || + parent->isDerivedFrom(App::LinkGroup::getClassTypeId()) || + parent->isDerivedFrom(App::Link::getClassTypeId())) { + // We have a link chain from this document to obj, and obj is in another document -> it's an XLink target + is_linked = true; + } + } + } + if (is_linked) { xShapes.push_back(obj); continue; } diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui index 105a38b0a7..8838d9990d 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui @@ -569,6 +569,9 @@ Show Center Marks + + true + ShowCenterMarks diff --git a/src/Mod/TechDraw/Gui/QGIPrimPath.cpp b/src/Mod/TechDraw/Gui/QGIPrimPath.cpp index 3355a13a0f..59933a6a93 100644 --- a/src/Mod/TechDraw/Gui/QGIPrimPath.cpp +++ b/src/Mod/TechDraw/Gui/QGIPrimPath.cpp @@ -234,6 +234,7 @@ void QGIPrimPath::setNormalColor(QColor c) { m_colNormal = c; m_colOverride = true; + m_colCurrent = m_colNormal; } void QGIPrimPath::setCapStyle(Qt::PenCapStyle c) @@ -316,6 +317,7 @@ void QGIPrimPath::resetFill() { void QGIPrimPath::setFillColor(QColor c) { m_colNormalFill = c; + m_fillColorCurrent = m_colNormalFill; // m_colDefFill = c; } diff --git a/src/Mod/TechDraw/Gui/QGIViewDimension.cpp b/src/Mod/TechDraw/Gui/QGIViewDimension.cpp index b16d4c7763..2bef981208 100644 --- a/src/Mod/TechDraw/Gui/QGIViewDimension.cpp +++ b/src/Mod/TechDraw/Gui/QGIViewDimension.cpp @@ -337,14 +337,10 @@ void QGIDatumLabel::setToleranceString() if( dim == nullptr ) { return; // don't show if both are zero or if EqualTolerance is true - } else if (!dim->hasOverUnderTolerance() || dim->EqualTolerance.getValue()) { + } else if (!dim->hasOverUnderTolerance() || dim->EqualTolerance.getValue() || dim->TheoreticalExact.getValue()) { m_tolTextOver->hide(); m_tolTextUnder->hide(); - return; - } else if (dim->TheoreticalExact.getValue()) { - m_tolTextOver->hide(); - m_tolTextUnder->hide(); - // we must explicitly empy the text other wise the frame drawn for + // we must explicitly empty the text otherwise the frame drawn for // TheoreticalExact would be as wide as necessary for the text m_tolTextOver->setPlainText(QString()); m_tolTextUnder->setPlainText(QString()); @@ -353,11 +349,6 @@ void QGIDatumLabel::setToleranceString() m_tolTextOver->show(); m_tolTextUnder->show(); - QString tolSuffix; - if ((dim->Type.isValue("Angle")) || (dim->Type.isValue("Angle3Pt"))) { - tolSuffix = QString::fromUtf8(dim->getFormattedDimensionValue(2).c_str()); //just the unit - } - std::pair labelTexts, unitTexts; if (dim->ArbitraryTolerances.getValue()) { @@ -637,25 +628,23 @@ void QGIViewDimension::updateDim() QString labelText; QString unitText; - if (dim->Arbitrary.getValue()) { + if ( (dim->Arbitrary.getValue() && !dim->EqualTolerance.getValue()) + || (dim->Arbitrary.getValue() && dim->TheoreticalExact.getValue()) ) { labelText = QString::fromUtf8(dim->getFormattedDimensionValue(1).c_str()); //just the number pref/spec/suf } else { if (dim->isMultiValueSchema()) { labelText = QString::fromUtf8(dim->getFormattedDimensionValue(0).c_str()); //don't format multis } else { labelText = QString::fromUtf8(dim->getFormattedDimensionValue(1).c_str()); //just the number pref/spec/suf - unitText = QString::fromUtf8(dim->getFormattedDimensionValue(2).c_str()); //just the unit - } - // if there is an equal over-/undertolerance and not theoretically exact, add the tolerance to dimension - if (dim->EqualTolerance.getValue() && !DrawUtil::fpCompare(dim->OverTolerance.getValue(), 0.0) - && !dim->TheoreticalExact.getValue()) { - std::pair ToleranceText, ToleranceUnit; - QString tolerance = QString::fromStdString(dim->getFormattedToleranceValue(1).c_str()); - // tolerance might start with a plus sign that we don't want, so cut it off - if (tolerance.at(0) == QChar::fromLatin1('+')) - tolerance.remove(0, 1); - // add the tolerance to the dimension using the ± sign - labelText = labelText + QString::fromUtf8(" \xC2\xB1 ") + tolerance; + if (dim->EqualTolerance.getValue()) { + if (dim->ArbitraryTolerances.getValue()) { + unitText = QString(); + } else { + unitText = QString::fromUtf8(dim->getFormattedToleranceValue(2).c_str()); //just the unit + } + } else { + unitText = QString::fromUtf8(dim->getFormattedDimensionValue(2).c_str()); //just the unit + } } } QFont font = datumLabel->getFont(); diff --git a/src/Mod/TechDraw/Gui/SymbolChooser.h b/src/Mod/TechDraw/Gui/SymbolChooser.h index 3f4bdec4b6..81d26c23d1 100644 --- a/src/Mod/TechDraw/Gui/SymbolChooser.h +++ b/src/Mod/TechDraw/Gui/SymbolChooser.h @@ -54,7 +54,7 @@ protected: void loadSymbolNames(QString pathToSymbols); private: - Ui_SymbolChooser* ui; + std::unique_ptr ui; QString m_symbolDir; QString m_symbolPath; QString m_source; diff --git a/src/Mod/TechDraw/Gui/TaskDimension.cpp b/src/Mod/TechDraw/Gui/TaskDimension.cpp index ad8967bb31..83d8690255 100644 --- a/src/Mod/TechDraw/Gui/TaskDimension.cpp +++ b/src/Mod/TechDraw/Gui/TaskDimension.cpp @@ -54,7 +54,6 @@ using namespace TechDrawGui; TaskDimension::TaskDimension(QGIViewDimension *parent, ViewProviderDimension *dimensionVP) : ui(new Ui_TaskDimension) { - int i = 0; m_parent = parent; m_dimensionVP = dimensionVP; @@ -68,6 +67,8 @@ TaskDimension::TaskDimension(QGIViewDimension *parent, ViewProviderDimension *di ui->cbEqualTolerance->setDisabled(true); ui->qsbOvertolerance->setDisabled(true); ui->qsbUndertolerance->setDisabled(true); + ui->leFormatSpecifierOverTolerance->setDisabled(true); + ui->leFormatSpecifierUnderTolerance->setDisabled(true); } ui->cbEqualTolerance->setChecked(parent->dvDimension->EqualTolerance.getValue()); connect(ui->cbEqualTolerance, SIGNAL(stateChanged(int)), this, SLOT(onEqualToleranceChanged())); @@ -88,8 +89,10 @@ TaskDimension::TaskDimension(QGIViewDimension *parent, ViewProviderDimension *di connect(ui->qsbOvertolerance, SIGNAL(valueChanged(double)), this, SLOT(onOvertoleranceChanged())); connect(ui->qsbUndertolerance, SIGNAL(valueChanged(double)), this, SLOT(onUndertoleranceChanged())); // undertolerance is disabled when EqualTolerance is true - if (ui->cbEqualTolerance->isChecked()) + if (ui->cbEqualTolerance->isChecked()) { ui->qsbUndertolerance->setDisabled(true); + ui->leFormatSpecifierUnderTolerance->setDisabled(true); + } // Formatting std::string StringValue = parent->dvDimension->FormatSpec.getValue(); @@ -98,10 +101,14 @@ TaskDimension::TaskDimension(QGIViewDimension *parent, ViewProviderDimension *di connect(ui->leFormatSpecifier, SIGNAL(textChanged(QString)), this, SLOT(onFormatSpecifierChanged())); ui->cbArbitrary->setChecked(parent->dvDimension->Arbitrary.getValue()); connect(ui->cbArbitrary, SIGNAL(stateChanged(int)), this, SLOT(onArbitraryChanged())); - StringValue = parent->dvDimension->FormatSpecTolerance.getValue(); + StringValue = parent->dvDimension->FormatSpecOverTolerance.getValue(); qs = QString::fromUtf8(StringValue.data(), StringValue.size()); - ui->leToleranceFormatSpecifier->setText(qs); - connect(ui->leToleranceFormatSpecifier, SIGNAL(textChanged(QString)), this, SLOT(onToleranceFormatSpecifierChanged())); + ui->leFormatSpecifierOverTolerance->setText(qs); + StringValue = parent->dvDimension->FormatSpecUnderTolerance.getValue(); + qs = QString::fromUtf8(StringValue.data(), StringValue.size()); + ui->leFormatSpecifierUnderTolerance->setText(qs); + connect(ui->leFormatSpecifierOverTolerance, SIGNAL(textChanged(QString)), this, SLOT(onFormatSpecifierOverToleranceChanged())); + connect(ui->leFormatSpecifierUnderTolerance, SIGNAL(textChanged(QString)), this, SLOT(onFormatSpecifierUnderToleranceChanged())); ui->cbArbitraryTolerances->setChecked(parent->dvDimension->ArbitraryTolerances.getValue()); connect(ui->cbArbitraryTolerances, SIGNAL(stateChanged(int)), this, SLOT(onArbitraryTolerancesChanged())); @@ -133,7 +140,8 @@ bool TaskDimension::accept() m_parent->dvDimension->FormatSpec.setValue(ui->leFormatSpecifier->text().toUtf8().constData()); m_parent->dvDimension->Arbitrary.setValue(ui->cbArbitrary->isChecked()); - m_parent->dvDimension->FormatSpecTolerance.setValue(ui->leToleranceFormatSpecifier->text().toUtf8().constData()); + m_parent->dvDimension->FormatSpecOverTolerance.setValue(ui->leFormatSpecifierOverTolerance->text().toUtf8().constData()); + m_parent->dvDimension->FormatSpecUnderTolerance.setValue(ui->leFormatSpecifierUnderTolerance->text().toUtf8().constData()); m_parent->dvDimension->ArbitraryTolerances.setValue(ui->cbArbitraryTolerances->isChecked()); m_dimensionVP->FlipArrowheads.setValue(ui->cbArrowheads->isChecked()); @@ -170,12 +178,20 @@ void TaskDimension::onTheoreticallyExactChanged() ui->cbEqualTolerance->setDisabled(true); ui->qsbOvertolerance->setDisabled(true); ui->qsbUndertolerance->setDisabled(true); + ui->leFormatSpecifierOverTolerance->setDisabled(true); + ui->leFormatSpecifierUnderTolerance->setDisabled(true); + ui->cbArbitraryTolerances->setDisabled(true); + ui->cbArbitraryTolerances->setChecked(false); } else { ui->cbEqualTolerance->setDisabled(false); ui->qsbOvertolerance->setDisabled(false); - if (!ui->cbEqualTolerance->isChecked()) + ui->leFormatSpecifierOverTolerance->setDisabled(false); + ui->cbArbitraryTolerances->setDisabled(false); + if (!ui->cbEqualTolerance->isChecked()) { ui->qsbUndertolerance->setDisabled(false); + ui->leFormatSpecifierUnderTolerance->setDisabled(false); + } } recomputeFeature(); } @@ -193,13 +209,15 @@ void TaskDimension::onEqualToleranceChanged() ui->qsbUndertolerance->setValue(-1.0 * ui->qsbOvertolerance->value().getValue()); ui->qsbUndertolerance->setUnit(ui->qsbOvertolerance->value().getUnit()); ui->qsbUndertolerance->setDisabled(true); + ui->leFormatSpecifierUnderTolerance->setDisabled(true); } else { ui->qsbOvertolerance->setMinimum(-DBL_MAX); - if (!ui->cbTheoreticallyExact->isChecked()) + if (!ui->cbTheoreticallyExact->isChecked()) { ui->qsbUndertolerance->setDisabled(false); + ui->leFormatSpecifierUnderTolerance->setDisabled(false); + } } - ui->qsbUndertolerance->setDisabled(ui->cbEqualTolerance->isChecked()); recomputeFeature(); } @@ -232,9 +250,23 @@ void TaskDimension::onArbitraryChanged() recomputeFeature(); } -void TaskDimension::onToleranceFormatSpecifierChanged() +void TaskDimension::onFormatSpecifierOverToleranceChanged() { - m_parent->dvDimension->FormatSpecTolerance.setValue(ui->leToleranceFormatSpecifier->text().toUtf8().constData()); + m_parent->dvDimension->FormatSpecOverTolerance.setValue(ui->leFormatSpecifierOverTolerance->text().toUtf8().constData()); + if (!ui->cbArbitraryTolerances->isChecked()) { + ui->leFormatSpecifierUnderTolerance->setText(ui->leFormatSpecifierOverTolerance->text()); + m_parent->dvDimension->FormatSpecUnderTolerance.setValue(ui->leFormatSpecifierUnderTolerance->text().toUtf8().constData()); + } + recomputeFeature(); +} + +void TaskDimension::onFormatSpecifierUnderToleranceChanged() +{ + m_parent->dvDimension->FormatSpecUnderTolerance.setValue(ui->leFormatSpecifierUnderTolerance->text().toUtf8().constData()); + if (!ui->cbArbitraryTolerances->isChecked()) { + ui->leFormatSpecifierOverTolerance->setText(ui->leFormatSpecifierUnderTolerance->text()); + m_parent->dvDimension->FormatSpecOverTolerance.setValue(ui->leFormatSpecifierOverTolerance->text().toUtf8().constData()); + } recomputeFeature(); } diff --git a/src/Mod/TechDraw/Gui/TaskDimension.h b/src/Mod/TechDraw/Gui/TaskDimension.h index 6a4095460e..97d2a48633 100644 --- a/src/Mod/TechDraw/Gui/TaskDimension.h +++ b/src/Mod/TechDraw/Gui/TaskDimension.h @@ -56,7 +56,8 @@ private Q_SLOTS: void onUndertoleranceChanged(); void onFormatSpecifierChanged(); void onArbitraryChanged(); - void onToleranceFormatSpecifierChanged(); + void onFormatSpecifierOverToleranceChanged(); + void onFormatSpecifierUnderToleranceChanged(); void onArbitraryTolerancesChanged(); void onFlipArrowheadsChanged(); void onColorChanged(); diff --git a/src/Mod/TechDraw/Gui/TaskDimension.ui b/src/Mod/TechDraw/Gui/TaskDimension.ui index 4af2f251c7..f6a54c1dd7 100644 --- a/src/Mod/TechDraw/Gui/TaskDimension.ui +++ b/src/Mod/TechDraw/Gui/TaskDimension.ui @@ -145,18 +145,32 @@ be used instead if the dimension value - Tolerance Format Specifier: + OverTolerance Format Specifier: - + - Text to be displayed + Specifies the overtolerance format in printf() style, or arbitrary text + + + UnderTolerance Format Specifier: + + + + + + + Specifies the undertolerance format in printf() style, or arbitrary text + + + + If checked the content of 'Format Spec' will diff --git a/src/Mod/TechDraw/Gui/TaskLineDecor.cpp b/src/Mod/TechDraw/Gui/TaskLineDecor.cpp index 789be010c8..4758f4f62f 100644 --- a/src/Mod/TechDraw/Gui/TaskLineDecor.cpp +++ b/src/Mod/TechDraw/Gui/TaskLineDecor.cpp @@ -79,7 +79,6 @@ TaskLineDecor::TaskLineDecor(TechDraw::DrawViewPart* partFeat, TaskLineDecor::~TaskLineDecor() { - delete ui; } void TaskLineDecor::initUi() diff --git a/src/Mod/TechDraw/Gui/TaskLineDecor.h b/src/Mod/TechDraw/Gui/TaskLineDecor.h index c5205a754f..d64b8fe3ac 100644 --- a/src/Mod/TechDraw/Gui/TaskLineDecor.h +++ b/src/Mod/TechDraw/Gui/TaskLineDecor.h @@ -70,7 +70,7 @@ protected: void getDefaults(void); private: - Ui_TaskLineDecor* ui; + std::unique_ptr ui; TechDraw::DrawViewPart* m_partFeat; std::vector m_edges; int m_style; diff --git a/src/Mod/TechDraw/Templates/Arch_A_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_A_Landscape.svg new file mode 100644 index 0000000000..6659dfce8b --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_A_Landscape.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch A + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_A_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_A_Portrait.svg new file mode 100644 index 0000000000..253821f556 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_A_Portrait.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch A + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_B_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_B_Landscape.svg new file mode 100644 index 0000000000..caa5a6faca --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_B_Landscape.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch B + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_B_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_B_Portrait.svg new file mode 100644 index 0000000000..912e8be8b3 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_B_Portrait.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch B + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_C_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_C_Landscape.svg new file mode 100644 index 0000000000..ae6791964e --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_C_Landscape.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch C + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_C_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_C_Portrait.svg new file mode 100644 index 0000000000..e0dd6cae46 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_C_Portrait.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch C + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_D_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_D_Landscape.svg new file mode 100644 index 0000000000..d3660f8a4d --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_D_Landscape.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch D + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_D_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_D_Portrait.svg new file mode 100644 index 0000000000..d4087adbb3 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_D_Portrait.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch D + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_E1_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_E1_Landscape.svg new file mode 100644 index 0000000000..b9358b6ab0 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E1_Landscape.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E1 + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_E1_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_E1_Portrait.svg new file mode 100644 index 0000000000..6e3967ae15 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E1_Portrait.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E1 + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_E2_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_E2_Landscape.svg new file mode 100644 index 0000000000..4711cf77c9 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E2_Landscape.svg @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E2 + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_E2_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_E2_Portrait.svg new file mode 100644 index 0000000000..dab40ec5f6 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E2_Portrait.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E2 + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_E3_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_E3_Landscape.svg new file mode 100644 index 0000000000..71184d1dd9 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E3_Landscape.svg @@ -0,0 +1,358 @@ + + + _________ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E3 + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_E3_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_E3_Portrait.svg new file mode 100644 index 0000000000..55af04f93e --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E3_Portrait.svg @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E3 + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/TechDraw/Templates/Arch_E_Landscape.svg b/src/Mod/TechDraw/Templates/Arch_E_Landscape.svg new file mode 100644 index 0000000000..3e535aa440 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E_Landscape.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/TechDraw/Templates/Arch_E_Portrait.svg b/src/Mod/TechDraw/Templates/Arch_E_Portrait.svg new file mode 100644 index 0000000000..93e7ffae38 --- /dev/null +++ b/src/Mod/TechDraw/Templates/Arch_E_Portrait.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _________ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AUTHOR NAME + CREATION DATE + SUPERVISOR NAME + CHECK DATE + Arch E + SCALE + WEIGHT + NUMBER + SHEET + TITLE + SUBTITLE + COMPANY NAME + COPYRIGHT + _________ + _________ + _________ + _________ + _________ + _________ + _________ + _________ + diff --git a/src/Mod/Test/Document.py b/src/Mod/Test/Document.py index 5abee2e9e3..538f8212f1 100644 --- a/src/Mod/Test/Document.py +++ b/src/Mod/Test/Document.py @@ -171,7 +171,7 @@ class DocumentBasicCases(unittest.TestCase): FreeCAD.Console.PrintLog(" exception thrown, OK\n") else: self.fail("no exception thrown") - self.failUnless(sorted(L1.getEnumerationsOfProperty('Enum')) == sorted(['Zero', 'One', 'Two', 'Three', 'Four'])) + self.failUnless(sorted(L1.getEnumerationsOfProperty('Enum')) == sorted(['Zero', 'One', 'Two', 'Three', 'Four'])) #self.failUnless(L1.IntegerList == [4711] ) #f = L1.FloatList @@ -245,7 +245,7 @@ class DocumentBasicCases(unittest.TestCase): #we should have all methods we need to handle extensions try: self.failUnless(not grp.hasExtension("App::GroupExtensionPython")) - grp.addExtension("App::GroupExtensionPython", self) + grp.addExtension("App::GroupExtensionPython") self.failUnless(grp.hasExtension("App::GroupExtension")) self.failUnless(grp.hasExtension("App::GroupExtensionPython")) grp.addObject(obj) @@ -260,8 +260,9 @@ class DocumentBasicCases(unittest.TestCase): return False; callback = SpecialGroup() - grp2 = self.Doc.addObject("App::DocumentObject", "Extension_3") - grp2.addExtension("App::GroupExtensionPython", callback) + grp2 = self.Doc.addObject("App::FeaturePython", "Extension_3") + grp2.addExtension("App::GroupExtensionPython") + grp2.Proxy = callback try: self.failUnless(grp2.hasExtension("App::GroupExtension")) @@ -281,7 +282,7 @@ class DocumentBasicCases(unittest.TestCase): class MyExtension(): def __init__(self, obj): - obj.addExtension("App::GroupExtensionPython", self) + obj.addExtension("App::GroupExtensionPython") obj = self.Doc.addObject("App::DocumentObject", "myObj") MyExtension(obj) @@ -293,7 +294,7 @@ class DocumentBasicCases(unittest.TestCase): def testExtensionGroup(self): obj = self.Doc.addObject("App::DocumentObject", "Obj") grp = self.Doc.addObject("App::FeaturePython", "Extension_2") - grp.addExtension("App::GroupExtensionPython", None) + grp.addExtension("App::GroupExtensionPython") grp.Group = [obj] self.assertTrue(obj in grp.Group) @@ -301,11 +302,11 @@ class DocumentBasicCases(unittest.TestCase): class Layer(): def __init__(self, obj): - obj.addExtension("App::GroupExtensionPython", self) + obj.addExtension("App::GroupExtensionPython") class LayerViewProvider(): def __init__(self, obj): - obj.addExtension("Gui::ViewProviderGroupExtensionPython", self) + obj.addExtension("Gui::ViewProviderGroupExtensionPython") obj.Proxy = self obj = self.Doc.addObject("App::FeaturePython","Layer") @@ -385,7 +386,7 @@ class DocumentBasicCases(unittest.TestCase): # class must be defined in global scope to allow it to be reloaded on document open class SaveRestoreSpecialGroup(): def __init__(self, obj): - obj.addExtension("App::GroupExtensionPython", self) + obj.addExtension("App::GroupExtensionPython") obj.Proxy = self def allowObject(self, obj): @@ -394,7 +395,7 @@ class SaveRestoreSpecialGroup(): # class must be defined in global scope to allow it to be reloaded on document open class SaveRestoreSpecialGroupViewProvider(): def __init__(self, obj): - obj.addExtension("Gui::ViewProviderGroupExtensionPython", self) + obj.addExtension("Gui::ViewProviderGroupExtensionPython") obj.Proxy = self def testFunction(self): @@ -468,7 +469,7 @@ class DocumentSaveRestoreCases(unittest.TestCase): grp1 = Doc.addObject("App::DocumentObject", "Extension_1") grp2 = Doc.addObject("App::FeaturePython", "Extension_2") - grp1.addExtension("App::GroupExtensionPython", None) + grp1.addExtension("App::GroupExtensionPython") SaveRestoreSpecialGroup(grp2) if FreeCAD.GuiUp: SaveRestoreSpecialGroupViewProvider(grp2.ViewObject) @@ -480,16 +481,12 @@ class DocumentSaveRestoreCases(unittest.TestCase): self.failUnless(Doc.Extension_1.hasExtension("App::GroupExtension")) self.failUnless(Doc.Extension_2.hasExtension("App::GroupExtension")) - self.failUnless(Doc.Extension_1.ExtensionProxy is None) - self.failUnless(Doc.Extension_2.ExtensionProxy is not None) self.failUnless(Doc.Extension_2.Group[0] is Doc.Obj) self.failUnless(hasattr(Doc.Extension_2.Proxy, 'allowObject')) - self.failUnless(hasattr(Doc.Extension_2.ExtensionProxy, 'allowObject')) if FreeCAD.GuiUp: self.failUnless(Doc.Extension_2.ViewObject.hasExtension("Gui::ViewProviderGroupExtensionPython")) self.failUnless(hasattr(Doc.Extension_2.ViewObject.Proxy, 'testFunction')) - self.failUnless(hasattr(Doc.Extension_2.ViewObject.ExtensionProxy, 'testFunction')) FreeCAD.closeDocument("SaveRestoreExtensions") @@ -1803,7 +1800,7 @@ class DocumentObserverCases(unittest.TestCase): self.failUnless(self.Obs.parameter2.pop() == 'Prop') self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) - pyobj.addExtension("App::GroupExtensionPython", None) + pyobj.addExtension("App::GroupExtensionPython") self.failUnless(self.Obs.signal.pop() == 'ObjDynExt') self.failUnless(self.Obs.parameter.pop() is pyobj) self.failUnless(self.Obs.parameter2.pop() == 'App::GroupExtensionPython') @@ -1945,7 +1942,7 @@ class DocumentObserverCases(unittest.TestCase): self.failUnless(self.GuiObs.parameter.pop(0) is obj.ViewObject) self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) - obj.ViewObject.addExtension("Gui::ViewProviderGroupExtensionPython", None) + obj.ViewObject.addExtension("Gui::ViewProviderGroupExtensionPython") self.failUnless(self.Obs.signal.pop() == 'ObjDynExt') self.failUnless(self.Obs.parameter.pop() is obj.ViewObject) self.failUnless(self.Obs.parameter2.pop() == 'Gui::ViewProviderGroupExtensionPython')