diff --git a/.github/workflows/CI_master.yml b/.github/workflows/CI_master.yml index 55b2db64d7..c36304c27b 100644 --- a/.github/workflows/CI_master.yml +++ b/.github/workflows/CI_master.yml @@ -63,8 +63,11 @@ jobs: with: artifactBasename: Lint-${{ github.run_id }} changedFiles: ${{ needs.Prepare.outputs.changedFiles }} + changedLines: ${{ needs.Prepare.outputs.changedLines }} changedCppFiles: ${{ needs.Prepare.outputs.changedCppFiles }} + changedCppLines: ${{ needs.Prepare.outputs.changedCppLines }} changedPythonFiles: ${{ needs.Prepare.outputs.changedPythonFiles }} + changedPythonLines: ${{ needs.Prepare.outputs.changedPythonLines }} WrapUp: needs: [ diff --git a/.github/workflows/codeql_cpp.yml b/.github/workflows/codeql_cpp.yml index 56c1b614ef..6f9045de64 100644 --- a/.github/workflows/codeql_cpp.yml +++ b/.github/workflows/codeql_cpp.yml @@ -114,10 +114,10 @@ jobs: # tools: https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.20.7/codeql-bundle-linux64.tar.gz # Add exclusions - # config: | - # query-filters: - # - exclude: - # id: py/file-not-closed + config: | + paths-ignore: + - src/3rdParty/** + - '**/ui_*.h' # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above diff --git a/.github/workflows/issue-metrics.yml b/.github/workflows/issue-metrics.yml index eb6914e8c6..42e7d8d49a 100644 --- a/.github/workflows/issue-metrics.yml +++ b/.github/workflows/issue-metrics.yml @@ -35,7 +35,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool - uses: github/issue-metrics@119b5237f41e78241b9b9cae254e544b52a359a0 # v3.20.1 + uses: github/issue-metrics@346541fd0068df64c02607a4c7f55438dc2881e2 # v3.21.0 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:FreeCAD/FreeCAD is:issue created:${{ env.last_month }}' diff --git a/.github/workflows/sub_lint.yml b/.github/workflows/sub_lint.yml index 9b020394db..ac31ef17e1 100644 --- a/.github/workflows/sub_lint.yml +++ b/.github/workflows/sub_lint.yml @@ -34,12 +34,21 @@ on: changedFiles: type: string required: true + changedLines: + type: string + required: false changedCppFiles: type: string required: true + changedCppLines: + type: string + required: false changedPythonFiles: type: string required: true + changedPythonLines: + type: string + required: false checkLineendings: default: false type: boolean @@ -317,6 +326,7 @@ jobs: run: | python3 tools/lint/clang_tidy.py \ --files "${{ inputs.changedCppFiles }}" \ + --line-filter '${{ inputs.changedCppLines }}' \ --clang-style "${{ inputs.clangStyle }}" \ --log-dir "${{ env.logdir }}" \ --report-file "${{ env.reportdir }}${{ env.reportfilename }}" diff --git a/.github/workflows/sub_prepare.yml b/.github/workflows/sub_prepare.yml index cec57f0c17..45c97a16b8 100644 --- a/.github/workflows/sub_prepare.yml +++ b/.github/workflows/sub_prepare.yml @@ -46,10 +46,16 @@ on: value: ${{ jobs.Prepare.outputs.reportFile }} changedFiles: value: ${{ jobs.Prepare.outputs.changedFiles }} + changedLines: + value: ${{ jobs.Prepare.outputs.changedLines }} changedPythonFiles: value: ${{ jobs.Prepare.outputs.changedPythonFiles }} + changedPythonLines: + value: ${{ jobs.Prepare.outputs.changedPythonLines }} changedCppFiles: value: ${{ jobs.Prepare.outputs.changedCppFiles }} + changedCppLines: + value: ${{ jobs.Prepare.outputs.changedCppLines }} jobs: @@ -67,8 +73,11 @@ jobs: outputs: reportFile: ${{ steps.Init.outputs.reportFile }} changedFiles: ${{ steps.Output.outputs.changedFiles }} + changedLines: ${{ steps.Output.outputs.changedLines }} changedPythonFiles: ${{ steps.Output.outputs.changedPythonFiles }} + changedPythonLines: ${{ steps.Output.outputs.changedPythonLines }} changedCppFiles: ${{ steps.Output.outputs.changedCppFiles }} + changedCppLines: ${{ steps.Output.outputs.changedCppLines }} steps: - name: Harden the runner (Audit all outbound calls) @@ -84,6 +93,10 @@ jobs: commitCnt=0 touch ${{ env.logdir }}changedFiles.lst ${{ env.logdir }}changedCppFiles.lst ${{ env.logdir }}changedPythonFiles.lst echo "reportFile=${{ env.reportfilename }}" >> $GITHUB_OUTPUT + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: true - name: Determine base and head SHA in case of PR if: env.isPR == 'true' run: | @@ -133,11 +146,22 @@ jobs: commitCnt=$(jq -re '.ahead_by' ${{ env.logdir }}compare.json) echo "Changeset is composed of $commitCnt commit(s)" | tee -a ${{env.reportdir}}${{ env.reportfilename }} - name: Get files modified in changeset #TODO check what happens with deleted file in the subsequent process - if: env.isPR == 'true' || env.isPush == 'true' + if: env.isPR == 'true' + env: + API_URL: ${{ github.api_url }} + TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + REF: ${{ github.ref_name }} + PR: ${{ github.event.number }} run: | - jq '.files[] | if .status != "removed" then .filename else empty end' ${{ env.logdir }}compare.json > ${{ env.logdir }}changedFiles.lst - grep -E '\.(py|py3)"' ${{ env.logdir }}changedFiles.lst > ${{ env.logdir }}changedPythonFiles.lst || true - grep -E '\.(c|c\+\+|cc|cpp|cu|cuh|cxx|h|h\+\+|hh|hpp|hxx)"' ${{ env.logdir }}changedFiles.lst > ${{ env.logdir }}changedCppFiles.lst || true + # could reduce this to a single + python3 tools/lint/changed_lines.py --api-url ${API_URL} --token ${TOKEN} --repo ${REPO} --ref=${REF} --pr=${PR} > ${{ env.logdir }}changedLines.lst + cat ${{ env.logdir }}changedLines.lst | jq '.[].name' > ${{ env.logdir }}changedFiles.lst + python3 tools/lint/changed_lines.py --api-url ${API_URL} --token ${TOKEN} --repo ${REPO} --ref=${REF} --pr=${PR} --file-filter '.py, .pyi' > ${{ env.logdir }}changedPythonLines.lst + cat ${{ env.logdir }}changedPythonLines.lst | jq '.[].name' > ${{ env.logdir }}changedPythonFiles.lst + python3 tools/lint/changed_lines.py --api-url ${API_URL} --token ${TOKEN} --repo ${REPO} --ref=${REF} --pr=${PR} --file-filter '.c, .cc, .cu, .cuh, .c++, .cpp, .cxx, .h, .hh, .h++, .hpp, .hxx' > ${{ env.logdir }}changedCppLines.lst + cat ${{ env.logdir }}changedCppLines.lst | jq '.[].name' > ${{ env.logdir }}changedCppFiles.lst + # Write the report echo "::group::Modified files in changeset (removed files are ignored) :" ; cat ${{ env.logdir }}changedFiles.lst ; echo "::endgroup::" echo "
Modified files (removed files are ignored):" >> ${{env.reportdir}}${{ env.reportfilename }} @@ -148,8 +172,11 @@ jobs: id: Output run: | echo "changedFiles=$(cat ${{ env.logdir }}changedFiles.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "changedLines=$(cat ${{ env.logdir }}changedLines.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT echo "changedPythonFiles=$(cat ${{ env.logdir }}changedPythonFiles.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "changedPythonLines=$(cat ${{ env.logdir }}changedPythonLines.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT echo "changedCppFiles=$(cat ${{ env.logdir }}changedCppFiles.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "changedCppLines=$(cat ${{ env.logdir }}changedCppLines.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT - name: Upload logs if: always() diff --git a/src/App/GroupExtension.pyi b/src/App/GroupExtension.pyi index cef2617867..8d50bc416b 100644 --- a/src/App/GroupExtension.pyi +++ b/src/App/GroupExtension.pyi @@ -70,3 +70,9 @@ class GroupExtension(DocumentObjectExtension): @param recursive if true check also if the obj is child of some sub group (default is false). """ ... + + def allowObject(self, obj: Any) -> bool: + """ + Returns true if obj is allowed in the group extension. + """ + ... diff --git a/src/App/GroupExtensionPyImp.cpp b/src/App/GroupExtensionPyImp.cpp index a13fbd7f65..fc281215d8 100644 --- a/src/App/GroupExtensionPyImp.cpp +++ b/src/App/GroupExtensionPyImp.cpp @@ -318,3 +318,30 @@ int GroupExtensionPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj* { return 0; } + +// def allowObject(self, obj: Any) -> bool: +PyObject* GroupExtensionPy::allowObject(PyObject* args) +{ + PyObject* object; + if (!PyArg_ParseTuple(args, "O!", &(DocumentObjectPy::Type), &object)) { + return nullptr; + } + + auto* docObj = static_cast(object); + if (!docObj->getDocumentObjectPtr() + || !docObj->getDocumentObjectPtr()->isAttachedToDocument()) { + PyErr_SetString(Base::PyExc_FC_GeneralError, "Cannot check an invalid object"); + return nullptr; + } + if (docObj->getDocumentObjectPtr()->getDocument() + != getGroupExtensionPtr()->getExtendedObject()->getDocument()) { + PyErr_SetString(Base::PyExc_FC_GeneralError, + "Cannot check an object from another document from this group"); + return nullptr; + } + + GroupExtension* grp = getGroupExtensionPtr(); + + bool allowed = grp->allowObject(docObj->getDocumentObjectPtr()); + return PyBool_FromLong(allowed ? 1 : 0); +} diff --git a/src/Base/Matrix.cpp b/src/Base/Matrix.cpp index aa0623b2b7..8a9f5019c0 100644 --- a/src/Base/Matrix.cpp +++ b/src/Base/Matrix.cpp @@ -35,30 +35,30 @@ using namespace Base; // clang-format off Matrix4D::Matrix4D() - : dMtrx4D {{1., 0., 0., 0.}, - {0., 1., 0., 0.}, - {0., 0., 1., 0.}, - {0., 0., 0., 1.}} + : dMtrx4D {{{1., 0., 0., 0.}, + {0., 1., 0., 0.}, + {0., 0., 1., 0.}, + {0., 0., 0., 1.}}} {} Matrix4D::Matrix4D(float a11, float a12, float a13, float a14, float a21, float a22, float a23, float a24, float a31, float a32, float a33, float a34, float a41, float a42, float a43, float a44) - : dMtrx4D {{a11, a12, a13, a14}, - {a21, a22, a23, a24}, - {a31, a32, a33, a34}, - {a41, a42, a43, a44}} + : dMtrx4D {{{a11, a12, a13, a14}, + {a21, a22, a23, a24}, + {a31, a32, a33, a34}, + {a41, a42, a43, a44}}} {} Matrix4D::Matrix4D(double a11, double a12, double a13, double a14, double a21, double a22, double a23, double a24, double a31, double a32, double a33, double a34, double a41, double a42, double a43, double a44) - : dMtrx4D {{a11, a12, a13, a14}, - {a21, a22, a23, a24}, - {a31, a32, a33, a34}, - {a41, a42, a43, a44}} + : dMtrx4D {{{a11, a12, a13, a14}, + {a21, a22, a23, a24}, + {a31, a32, a33, a34}, + {a41, a42, a43, a44}}} {} // clang-format on @@ -277,13 +277,9 @@ void Matrix4D::rotLine(const Vector3d& vec, double fAngle) double fsin {}; // set all entries to "0" - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { - clMA.dMtrx4D[iz][is] = 0; - clMB.dMtrx4D[iz][is] = 0; - clMC.dMtrx4D[iz][is] = 0; - } - } + clMA.nullify(); + clMB.nullify(); + clMC.nullify(); // ** normalize the rotation axis clRotAxis.Normalize(); @@ -313,10 +309,9 @@ void Matrix4D::rotLine(const Vector3d& vec, double fAngle) clMC.dMtrx4D[2][0] = -fsin * clRotAxis.y; clMC.dMtrx4D[2][1] = fsin * clRotAxis.x; - for (short iz = 0; iz < 3; iz++) { - for (short is = 0; is < 3; is++) { - clMRot.dMtrx4D[iz][is] = - clMA.dMtrx4D[iz][is] + clMB.dMtrx4D[iz][is] + clMC.dMtrx4D[iz][is]; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + clMRot.dMtrx4D[i][j] = clMA.dMtrx4D[i][j] + clMB.dMtrx4D[i][j] + clMC.dMtrx4D[i][j]; } } @@ -522,15 +517,15 @@ void Matrix4D::inverse() /**** Herausnehmen und Inversion der TranslationsMatrix aus der TransformationMatrix ****/ - for (short iz = 0; iz < 3; iz++) { - clInvTrlMat.dMtrx4D[iz][3] = -dMtrx4D[iz][3]; + for (int i = 0; i < 3; i++) { + clInvTrlMat.dMtrx4D[i][3] = -dMtrx4D[i][3]; } /**** Herausnehmen und Inversion der RotationsMatrix aus der TransformationMatrix ****/ - for (short iz = 0; iz < 3; iz++) { - for (short is = 0; is < 3; is++) { - clInvRotMat.dMtrx4D[iz][is] = dMtrx4D[is][iz]; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + clInvRotMat.dMtrx4D[i][j] = dMtrx4D[j][i]; } } @@ -624,7 +619,7 @@ void Matrix4D::inverseOrthogonal() { Base::Vector3d vec(dMtrx4D[0][3], dMtrx4D[1][3], dMtrx4D[2][3]); transpose(); - vec = this->operator*(vec); + multVec(vec, vec); dMtrx4D[0][3] = -vec.x; dMtrx4D[3][0] = 0; dMtrx4D[1][3] = -vec.y; @@ -651,36 +646,36 @@ void Matrix4D::inverseGauss() void Matrix4D::getMatrix(double dMtrx[16]) const { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { - dMtrx[4 * iz + is] = dMtrx4D[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx[4 * i + j] = dMtrx4D[i][j]; } } } void Matrix4D::setMatrix(const double dMtrx[16]) { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { - dMtrx4D[iz][is] = dMtrx[4 * iz + is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] = dMtrx[4 * i + j]; } } } void Matrix4D::getGLMatrix(double dMtrx[16]) const { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { - dMtrx[iz + 4 * is] = dMtrx4D[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx[i + 4 * j] = dMtrx4D[i][j]; } } } void Matrix4D::setGLMatrix(const double dMtrx[16]) { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { - dMtrx4D[iz][is] = dMtrx[iz + 4 * is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] = dMtrx[i + 4 * j]; } } } @@ -693,7 +688,7 @@ unsigned long Matrix4D::getMemSpace() void Matrix4D::Print() const { // NOLINTBEGIN - for (short i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { printf("%9.3f %9.3f %9.3f %9.3f\n", dMtrx4D[i][0], dMtrx4D[i][1], @@ -705,15 +700,12 @@ void Matrix4D::Print() const void Matrix4D::transpose() { - double dNew[4][4]; - - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - dNew[j][i] = dMtrx4D[i][j]; - } - } - - memcpy(dMtrx4D, dNew, sizeof(dMtrx4D)); + std::swap(dMtrx4D[0][1], dMtrx4D[1][0]); + std::swap(dMtrx4D[0][2], dMtrx4D[2][0]); + std::swap(dMtrx4D[0][3], dMtrx4D[3][0]); + std::swap(dMtrx4D[1][2], dMtrx4D[2][1]); + std::swap(dMtrx4D[1][3], dMtrx4D[3][1]); + std::swap(dMtrx4D[2][3], dMtrx4D[3][2]); } @@ -788,8 +780,8 @@ std::string Matrix4D::analyse() const trp.transpose(); trp = trp * sub; bool ortho = true; - for (unsigned short i = 0; i < 4 && ortho; i++) { - for (unsigned short j = 0; j < 4 && ortho; j++) { + for (unsigned int i = 0; i < 4 && ortho; i++) { + for (unsigned int j = 0; j < 4 && ortho; j++) { if (i != j) { if (fabs(trp[i][j]) > eps) { ortho = false; diff --git a/src/Base/Matrix.h b/src/Base/Matrix.h index b177abb0fe..3e0ba98794 100644 --- a/src/Base/Matrix.h +++ b/src/Base/Matrix.h @@ -106,13 +106,13 @@ public: /// Comparison inline bool operator==(const Matrix4D& mat) const; /// Index operator - inline double* operator[](unsigned short usNdx); + inline std::array& operator[](unsigned int usNdx); /// Index operator - inline const double* operator[](unsigned short usNdx) const; + inline const std::array& operator[](unsigned int usNdx) const; /// Get vector of row - inline Vector3d getRow(unsigned short usNdx) const; + inline Vector3d getRow(unsigned int usNdx) const; /// Get vector of column - inline Vector3d getCol(unsigned short usNdx) const; + inline Vector3d getCol(unsigned int usNdx) const; /// Get vector of diagonal inline Vector3d diagonal() const; /// Get trace of the 3x3 matrix @@ -120,9 +120,9 @@ public: /// Get trace of the 4x4 matrix inline double trace() const; /// Set row to vector - inline void setRow(unsigned short usNdx, const Vector3d& vec); + inline void setRow(unsigned int usNdx, const Vector3d& vec); /// Set column to vector - inline void setCol(unsigned short usNdx, const Vector3d& vec); + inline void setCol(unsigned int usNdx, const Vector3d& vec); /// Set diagonal to vector inline void setDiagonal(const Vector3d& vec); /// Compute the determinant of the matrix @@ -234,27 +234,21 @@ public: void fromString(const std::string& str); private: - double dMtrx4D[4][4]; + using Array2d = std::array, 4>; + Array2d dMtrx4D; }; inline Matrix4D Matrix4D::operator+(const Matrix4D& mat) const { - Matrix4D clMat; - - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = dMtrx4D[iz][is] + mat[iz][is]; - } - } - - return clMat; + Matrix4D newMat(*this); + return newMat += mat; } inline Matrix4D& Matrix4D::operator+=(const Matrix4D& mat) { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] += mat[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] += mat[i][j]; } } @@ -263,22 +257,15 @@ inline Matrix4D& Matrix4D::operator+=(const Matrix4D& mat) inline Matrix4D Matrix4D::operator-(const Matrix4D& mat) const { - Matrix4D clMat; - - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = dMtrx4D[iz][is] - mat[iz][is]; - } - } - - return clMat; + Matrix4D newMat(*this); + return newMat -= mat; } inline Matrix4D& Matrix4D::operator-=(const Matrix4D& mat) { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] -= mat[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] -= mat[i][j]; } } @@ -287,19 +274,7 @@ inline Matrix4D& Matrix4D::operator-=(const Matrix4D& mat) inline Matrix4D& Matrix4D::operator*=(const Matrix4D& mat) { - Matrix4D clMat; - - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = 0; - for (int ie = 0; ie < 4; ie++) { - clMat.dMtrx4D[iz][is] += dMtrx4D[iz][ie] * mat.dMtrx4D[ie][is]; - } - } - } - - (*this) = clMat; - + (*this) = (*this) * mat; return *this; } @@ -307,11 +282,11 @@ inline Matrix4D Matrix4D::operator*(const Matrix4D& mat) const { Matrix4D clMat; - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = 0; - for (int ie = 0; ie < 4; ie++) { - clMat.dMtrx4D[iz][is] += dMtrx4D[iz][ie] * mat.dMtrx4D[ie][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + clMat.dMtrx4D[i][j] = 0; + for (int e = 0; e < 4; e++) { + clMat.dMtrx4D[i][j] += dMtrx4D[i][e] * mat.dMtrx4D[e][j]; } } } @@ -325,9 +300,9 @@ inline Matrix4D& Matrix4D::operator=(const Matrix4D& mat) return *this; } - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] = mat.dMtrx4D[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] = mat.dMtrx4D[i][j]; } } @@ -336,23 +311,16 @@ inline Matrix4D& Matrix4D::operator=(const Matrix4D& mat) inline Vector3f Matrix4D::operator*(const Vector3f& vec) const { - // clang-format off - double sx = static_cast(vec.x); - double sy = static_cast(vec.y); - double sz = static_cast(vec.z); - return Vector3f(static_cast(dMtrx4D[0][0] * sx + dMtrx4D[0][1] * sy + dMtrx4D[0][2] * sz + dMtrx4D[0][3]), - static_cast(dMtrx4D[1][0] * sx + dMtrx4D[1][1] * sy + dMtrx4D[1][2] * sz + dMtrx4D[1][3]), - static_cast(dMtrx4D[2][0] * sx + dMtrx4D[2][1] * sy + dMtrx4D[2][2] * sz + dMtrx4D[2][3])); - // clang-format on + Vector3f dst; + multVec(vec, dst); + return dst; } inline Vector3d Matrix4D::operator*(const Vector3d& vec) const { - // clang-format off - return Vector3d((dMtrx4D[0][0] * vec.x + dMtrx4D[0][1] * vec.y + dMtrx4D[0][2] * vec.z + dMtrx4D[0][3]), - (dMtrx4D[1][0] * vec.x + dMtrx4D[1][1] * vec.y + dMtrx4D[1][2] * vec.z + dMtrx4D[1][3]), - (dMtrx4D[2][0] * vec.x + dMtrx4D[2][1] * vec.y + dMtrx4D[2][2] * vec.z + dMtrx4D[2][3])); - // clang-format on + Vector3d dst; + multVec(vec, dst); + return dst; } inline void Matrix4D::multVec(const Vector3d& src, Vector3d& dst) const @@ -379,21 +347,15 @@ inline void Matrix4D::multVec(const Vector3f& src, Vector3f& dst) const inline Matrix4D Matrix4D::operator*(double scalar) const { - Matrix4D matrix; - for (unsigned short i = 0; i < 4; i++) { - for (unsigned short j = 0; j < 4; j++) { - matrix.dMtrx4D[i][j] = dMtrx4D[i][j] * scalar; - } - } - - return matrix; + Matrix4D newMat(*this); + return newMat *= scalar; } inline Matrix4D& Matrix4D::operator*=(double scalar) { // NOLINTBEGIN - for (unsigned short i = 0; i < 4; i++) { - for (unsigned short j = 0; j < 4; j++) { + for (unsigned int i = 0; i < 4; i++) { + for (unsigned int j = 0; j < 4; j++) { dMtrx4D[i][j] *= scalar; } } @@ -403,9 +365,9 @@ inline Matrix4D& Matrix4D::operator*=(double scalar) inline bool Matrix4D::operator==(const Matrix4D& mat) const { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - if (fabs(dMtrx4D[iz][is] - mat.dMtrx4D[iz][is]) > traits_type::epsilon()) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (fabs(dMtrx4D[i][j] - mat.dMtrx4D[i][j]) > traits_type::epsilon()) { return false; } } @@ -421,26 +383,26 @@ inline bool Matrix4D::operator!=(const Matrix4D& mat) const inline Vector3f& operator*=(Vector3f& vec, const Matrix4D& mat) { - vec = mat * vec; + mat.multVec(vec, vec); return vec; } -inline double* Matrix4D::operator[](unsigned short usNdx) +inline std::array& Matrix4D::operator[](unsigned int usNdx) { return dMtrx4D[usNdx]; } -inline const double* Matrix4D::operator[](unsigned short usNdx) const +inline const std::array& Matrix4D::operator[](unsigned int usNdx) const { return dMtrx4D[usNdx]; } -inline Vector3d Matrix4D::getRow(unsigned short usNdx) const +inline Vector3d Matrix4D::getRow(unsigned int usNdx) const { return Vector3d(dMtrx4D[usNdx][0], dMtrx4D[usNdx][1], dMtrx4D[usNdx][2]); } -inline Vector3d Matrix4D::getCol(unsigned short usNdx) const +inline Vector3d Matrix4D::getCol(unsigned int usNdx) const { return Vector3d(dMtrx4D[0][usNdx], dMtrx4D[1][usNdx], dMtrx4D[2][usNdx]); } @@ -460,14 +422,14 @@ inline double Matrix4D::trace() const return dMtrx4D[0][0] + dMtrx4D[1][1] + dMtrx4D[2][2] + dMtrx4D[3][3]; } -inline void Matrix4D::setRow(unsigned short usNdx, const Vector3d& vec) +inline void Matrix4D::setRow(unsigned int usNdx, const Vector3d& vec) { dMtrx4D[usNdx][0] = vec.x; dMtrx4D[usNdx][1] = vec.y; dMtrx4D[usNdx][2] = vec.z; } -inline void Matrix4D::setCol(unsigned short usNdx, const Vector3d& vec) +inline void Matrix4D::setCol(unsigned int usNdx, const Vector3d& vec) { dMtrx4D[0][usNdx] = vec.x; dMtrx4D[1][usNdx] = vec.y; diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp index c839412deb..8588bc4d70 100644 --- a/src/Gui/Action.cpp +++ b/src/Gui/Action.cpp @@ -606,9 +606,37 @@ void ActionGroup::onActivated (QAction* act) } } -void ActionGroup::onHovered (QAction *act) +/** + * Shows tooltip at the right side when hovered. + */ +void ActionGroup::onHovered(QAction *act) { - QToolTip::showText(QCursor::pos(), act->toolTip()); + const auto topLevelWidgets = QApplication::topLevelWidgets(); + QMenu* foundMenu = nullptr; + + for (QWidget* widget : topLevelWidgets) { + QList menus = widget->findChildren(); + + for (QMenu* menu : menus) { + if (menu->isVisible() && menu->actions().contains(act)) { + foundMenu = menu; + break; + } + } + + if (foundMenu) { + break; + } + + } + + if (foundMenu) { + QRect actionRect = foundMenu->actionGeometry(act); + QPoint globalPos = foundMenu->mapToGlobal(actionRect.topRight()); + QToolTip::showText(globalPos, act->toolTip(), foundMenu, actionRect); + } else { + QToolTip::showText(QCursor::pos(), act->toolTip()); + } } diff --git a/src/Gui/ActiveObjectList.cpp b/src/Gui/ActiveObjectList.cpp index 0c0a5693c6..e76882167a 100644 --- a/src/Gui/ActiveObjectList.cpp +++ b/src/Gui/ActiveObjectList.cpp @@ -197,3 +197,15 @@ void ActiveObjectList::objectDeleted(const ViewProviderDocumentObject &vp) } } } + +App::DocumentObject* ActiveObjectList::getObjectWithExtension(const Base::Type extensionTypeId) const +{ + for (const auto& pair : _ObjectMap) { + App::DocumentObject* obj = getObject(pair.second, true); + if (obj && obj->hasExtension(extensionTypeId)) { + return obj; + } + } + + return nullptr; +} diff --git a/src/Gui/ActiveObjectList.h b/src/Gui/ActiveObjectList.h index 8a75ce87bb..bd058d0607 100644 --- a/src/Gui/ActiveObjectList.h +++ b/src/Gui/ActiveObjectList.h @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -67,6 +68,8 @@ namespace Gui void objectDeleted(const ViewProviderDocumentObject& viewProviderIn); bool hasObject(App::DocumentObject *obj, const char *, const char *subname=nullptr) const; + App::DocumentObject* getObjectWithExtension(Base::Type extensionTypeId) const; + private: struct ObjectInfo; void setHighlight(const ObjectInfo &info, Gui::HighlightMode mode, bool enable); diff --git a/src/Gui/CommandStd.cpp b/src/Gui/CommandStd.cpp index e8089ff289..2837ccd190 100644 --- a/src/Gui/CommandStd.cpp +++ b/src/Gui/CommandStd.cpp @@ -379,9 +379,9 @@ StdCmdDlgParameter::StdCmdDlgParameter() { sGroup = "Tools"; sMenuText = QT_TR_NOOP("E&dit parameters..."); - sToolTipText = QT_TR_NOOP("Opens a Dialog to edit the parameters"); + sToolTipText = QT_TR_NOOP("Opens a dialog to edit the parameters"); sWhatsThis = "Std_DlgParameter"; - sStatusTip = QT_TR_NOOP("Opens a Dialog to edit the parameters"); + sStatusTip = QT_TR_NOOP("Opens a dialog to edit the parameters"); sPixmap = "Std_DlgParameter"; eType = 0; } @@ -404,9 +404,9 @@ StdCmdDlgPreferences::StdCmdDlgPreferences() { sGroup = "Tools"; sMenuText = QT_TR_NOOP("Prefere&nces ..."); - sToolTipText = QT_TR_NOOP("Opens a Dialog to edit the preferences"); + sToolTipText = QT_TR_NOOP("Opens a dialog to edit the preferences"); sWhatsThis = "Std_DlgPreferences"; - sStatusTip = QT_TR_NOOP("Opens a Dialog to edit the preferences"); + sStatusTip = QT_TR_NOOP("Opens a dialog to edit the preferences"); sPixmap = "preferences-system"; eType = 0; sAccel = "Ctrl+,"; diff --git a/src/Gui/CommandStructure.cpp b/src/Gui/CommandStructure.cpp index b465c4055c..1fd797e675 100644 --- a/src/Gui/CommandStructure.cpp +++ b/src/Gui/CommandStructure.cpp @@ -32,6 +32,7 @@ #include "ActiveObjectList.h" #include "Application.h" #include "Document.h" +#include "MDIView.h" #include "ViewProviderDocumentObject.h" #include "Selection.h" @@ -121,9 +122,27 @@ void StdCmdGroup::activated(int iMsg) std::string GroupName; GroupName = getUniqueObjectName("Group"); QString label = QApplication::translate("Std_Group", "Group"); - doCommand(Doc,"App.activeDocument().Tip = App.activeDocument().addObject('App::DocumentObjectGroup','%s')",GroupName.c_str()); - doCommand(Doc,"App.activeDocument().%s.Label = '%s'", GroupName.c_str(), - label.toUtf8().data()); + + // create a group + doCommand(Doc,"group = App.activeDocument().addObject('App::DocumentObjectGroup','%s')",GroupName.c_str()); + doCommand(Doc,"group.Label = '%s'", label.toUtf8().data()); + doCommand(Doc,"App.activeDocument().Tip = group"); + + // try to add the group to any active object that supports grouping (has GroupExtension) + if (auto* activeDoc = Gui::Application::Instance->activeDocument()) { + if (auto* activeView = activeDoc->getActiveView()) { + // find the first active object with GroupExtension + if (auto* activeObj = activeView->getActiveObjectWithExtension( + App::GroupExtension::getExtensionClassTypeId())) { + doCommand(Doc, + "active_obj = App.activeDocument().getObject('%s')\n" + "if active_obj and active_obj.allowObject(group):\n" + " active_obj.Group += [group]", + activeObj->getNameInDocument()); + } + } + } // if we have no active object, group will be added to root doc + commitCommand(); Gui::Document* gui = Application::Instance->activeDocument(); diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index ccf00cb137..91ee820349 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -3710,7 +3710,7 @@ StdCmdDockOverlayToggleLeft::StdCmdDockOverlayToggleLeft() sWhatsThis = "Std_DockOverlayToggleLeft"; sStatusTip = sToolTipText; sAccel = "Ctrl+Left"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleLeft"; eType = 0; } @@ -3735,7 +3735,7 @@ StdCmdDockOverlayToggleRight::StdCmdDockOverlayToggleRight() sWhatsThis = "Std_DockOverlayToggleRight"; sStatusTip = sToolTipText; sAccel = "Ctrl+Right"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleRight"; eType = 0; } @@ -3760,7 +3760,7 @@ StdCmdDockOverlayToggleTop::StdCmdDockOverlayToggleTop() sWhatsThis = "Std_DockOverlayToggleTop"; sStatusTip = sToolTipText; sAccel = "Ctrl+Up"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleTop"; eType = 0; } @@ -3785,7 +3785,7 @@ StdCmdDockOverlayToggleBottom::StdCmdDockOverlayToggleBottom() sWhatsThis = "Std_DockOverlayToggleBottom"; sStatusTip = sToolTipText; sAccel = "Ctrl+Down"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleBottom"; eType = 0; } @@ -3847,8 +3847,8 @@ public: :GroupCommand("Std_DockOverlay") { sGroup = "View"; - sMenuText = QT_TR_NOOP("Dock window overlay"); - sToolTipText = QT_TR_NOOP("Setting docked window overlay mode"); + sMenuText = QT_TR_NOOP("Dock panel overlay"); + sToolTipText = QT_TR_NOOP("Setting docked panel overlay mode"); sWhatsThis = "Std_DockOverlay"; sStatusTip = sToolTipText; eType = 0; diff --git a/src/Gui/Dialogs/DlgPreferences.ui b/src/Gui/Dialogs/DlgPreferences.ui index 1f169e871c..14bbef221a 100644 --- a/src/Gui/Dialogs/DlgPreferences.ui +++ b/src/Gui/Dialogs/DlgPreferences.ui @@ -210,6 +210,35 @@ + + + + 4 + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + Search preferences... + + + true + + + + + diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index d5b539994e..75d9b1289d 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -28,18 +28,35 @@ # 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 +# include +# include +# include +# include +# include +# include #endif #include @@ -56,6 +73,154 @@ using namespace Gui::Dialog; +// Simple delegate to render first line bold, second line normal +// used by search box +class MixedFontDelegate : public QStyledItemDelegate +{ + static constexpr int horizontalPadding = 12; + static constexpr int verticalPadding = 4; + +public: + explicit MixedFontDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} + + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + if (!index.isValid()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + QString pathText, widgetText; + extractTextData(index, pathText, widgetText); + + if (pathText.isEmpty()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + QFont boldFont, normalFont; + createFonts(option.font, boldFont, normalFont); + + LayoutInfo layout = calculateLayout(pathText, widgetText, boldFont, normalFont, option.rect.width()); + + painter->save(); + + // draw selection background if selected + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.highlight()); + } + + // Set text color based on selection + QColor textColor = (option.state & QStyle::State_Selected) + ? option.palette.highlightedText().color() + : option.palette.text().color(); + painter->setPen(textColor); + + // draw path in bold (Tab/Page) with wrapping + painter->setFont(boldFont); + QRect boldRect(option.rect.left() + horizontalPadding, option.rect.top() + verticalPadding, + layout.availableWidth, layout.pathHeight); + painter->drawText(boldRect, Qt::TextWordWrap | Qt::AlignTop, pathText); + + // draw widget text in normal font (if present) + if (!widgetText.isEmpty()) { + painter->setFont(normalFont); + QRect normalRect(option.rect.left() + horizontalPadding, + option.rect.top() + verticalPadding + layout.pathHeight, + layout.availableWidth, + layout.widgetHeight); + painter->drawText(normalRect, Qt::TextWordWrap | Qt::AlignTop, widgetText); + } + + painter->restore(); + } + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + if (!index.isValid()) { + return QStyledItemDelegate::sizeHint(option, index); + } + + QString pathText, widgetText; + extractTextData(index, pathText, widgetText); + + if (pathText.isEmpty()) { + return QStyledItemDelegate::sizeHint(option, index); + } + + QFont boldFont, normalFont; + createFonts(option.font, boldFont, normalFont); + + LayoutInfo layout = calculateLayout(pathText, widgetText, boldFont, normalFont, option.rect.width()); + + return {layout.totalWidth, layout.totalHeight}; + } + +private: + struct LayoutInfo { + int availableWidth; + int pathHeight; + int widgetHeight; + int totalWidth; + int totalHeight; + }; + + void extractTextData(const QModelIndex& index, QString& pathText, QString& widgetText) const + { + // Use separate roles - all items should have proper role data + pathText = index.data(PreferencesSearchController::PathRole).toString(); + widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString(); + } + + void createFonts(const QFont& baseFont, QFont& boldFont, QFont& normalFont) const + { + boldFont = baseFont; + boldFont.setBold(true); + boldFont.setPointSize(boldFont.pointSize() - 1); // make header smaller like a subtitle + + normalFont = baseFont; // keep widget text at normal size + } + + LayoutInfo calculateLayout(const QString& pathText, const QString& widgetText, + const QFont& boldFont, const QFont& normalFont, int containerWidth) const + { + + QFontMetrics boldFm(boldFont); + QFontMetrics normalFm(normalFont); + + int availableWidth = containerWidth - horizontalPadding * 2; // account for left and right padding + if (availableWidth <= 0) { + constexpr int defaultPopupWidth = 300; + availableWidth = defaultPopupWidth - horizontalPadding * 2; // Fallback to popup width minus padding + } + + // Calculate dimensions for path text (bold) + QRect pathBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, pathText); + int pathHeight = pathBoundingRect.height(); + int pathWidth = pathBoundingRect.width(); + + // Calculate dimensions for widget text (normal font, if present) + int widgetHeight = 0; + int widgetWidth = 0; + if (!widgetText.isEmpty()) { + QRect widgetBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, widgetText); + widgetHeight = widgetBoundingRect.height(); + widgetWidth = widgetBoundingRect.width(); + } + + int totalWidth = qMax(pathWidth, widgetWidth) + horizontalPadding * 2; // +24 horizontal padding + int totalHeight = verticalPadding * 2 + pathHeight + widgetHeight; // 8 vertical padding + content heights + + LayoutInfo layout; + layout.availableWidth = availableWidth; + layout.pathHeight = pathHeight; + layout.widgetHeight = widgetHeight; + layout.totalWidth = totalWidth; + layout.totalHeight = totalHeight; + return layout; + } +}; + bool isParentOf(const QModelIndex& parent, const QModelIndex& child) { for (auto it = child; it.isValid(); it = it.parent()) { @@ -133,9 +298,17 @@ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) // remove unused help button setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + // Initialize search controller + m_searchController = std::make_unique(this, this); + setupConnections(); ui->groupsTreeView->setModel(&_model); + + // Configure search controller after UI setup + m_searchController->setPreferencesModel(&_model); + m_searchController->setGroupNameRole(GroupNameRole); + m_searchController->setPageNameRole(PageNameRole); setupPages(); @@ -150,6 +323,9 @@ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) */ DlgPreferencesImp::~DlgPreferencesImp() { + // Remove global event filter + qApp->removeEventFilter(this); + if (DlgPreferencesImp::_activeDialog == this) { DlgPreferencesImp::_activeDialog = nullptr; } @@ -185,6 +361,23 @@ void DlgPreferencesImp::setupConnections() &QStackedWidget::currentChanged, this, &DlgPreferencesImp::onStackWidgetChange); + // Connect search functionality to controller + connect(ui->searchBox, + &QLineEdit::textChanged, + m_searchController.get(), + &PreferencesSearchController::onSearchTextChanged); + + // Connect navigation signal from controller to dialog + connect(m_searchController.get(), + &PreferencesSearchController::navigationRequested, + this, + &DlgPreferencesImp::onNavigationRequested); + + // Install event filter on search box for arrow key navigation + ui->searchBox->installEventFilter(this); + + // Install global event filter to handle clicks outside popup + qApp->installEventFilter(this); } void DlgPreferencesImp::setupPages() @@ -903,6 +1096,45 @@ void DlgPreferencesImp::onStackWidgetChange(int index) ui->groupsTreeView->selectionModel()->select(currentIndex, QItemSelectionModel::ClearAndSelect); } +void DlgPreferencesImp::onNavigationRequested(const QString& groupName, const QString& pageName) +{ + navigateToSearchResult(groupName, pageName); +} + +void DlgPreferencesImp::navigateToSearchResult(const QString& groupName, const QString& pageName) +{ + // Find the group and page items + auto root = _model.invisibleRootItem(); + for (int i = 0; i < root->rowCount(); i++) { + auto groupItem = static_cast(root->child(i)); + if (groupItem->data(GroupNameRole).toString() == groupName) { + + // Find the specific page + for (int j = 0; j < groupItem->rowCount(); j++) { + auto pageItem = static_cast(groupItem->child(j)); + if (pageItem->data(PageNameRole).toString() == pageName) { + + // Expand the group if needed + ui->groupsTreeView->expand(groupItem->index()); + + // Select the page + ui->groupsTreeView->selectionModel()->select(pageItem->index(), QItemSelectionModel::ClearAndSelect); + + // Navigate to the page + onPageSelected(pageItem->index()); + + return; + } + } + + // If no specific page found, just navigate to the group + ui->groupsTreeView->selectionModel()->select(groupItem->index(), QItemSelectionModel::ClearAndSelect); + onPageSelected(groupItem->index()); + return; + } + } +} + void DlgPreferencesImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { @@ -1007,4 +1239,719 @@ PreferencesPageItem* DlgPreferencesImp::getCurrentPage() const return pageWidget->property(PreferencesPageItem::PropertyName).value(); } +// PreferencesSearchController implementation +PreferencesSearchController::PreferencesSearchController(DlgPreferencesImp* parentDialog, QObject* parent) + : QObject(parent) + , m_parentDialog(parentDialog) + , m_preferencesModel(nullptr) + , m_groupNameRole(0) + , m_pageNameRole(0) + , m_searchBox(nullptr) + , m_searchResultsList(nullptr) +{ + // Get reference to search box from parent dialog's UI + m_searchBox = m_parentDialog->ui->searchBox; + + // Create the search results popup list + m_searchResultsList = new QListWidget(m_parentDialog); + m_searchResultsList->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); + m_searchResultsList->setVisible(false); + m_searchResultsList->setMinimumWidth(300); + m_searchResultsList->setMaximumHeight(400); // Increased max height + m_searchResultsList->setFrameStyle(QFrame::Box | QFrame::Raised); + m_searchResultsList->setLineWidth(1); + m_searchResultsList->setFocusPolicy(Qt::NoFocus); // Don't steal focus from search box + m_searchResultsList->setAttribute(Qt::WA_ShowWithoutActivating); // Show without activating/stealing focus + m_searchResultsList->setWordWrap(true); // Enable word wrapping + m_searchResultsList->setTextElideMode(Qt::ElideNone); // Don't elide text, let it wrap instead + m_searchResultsList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Disable horizontal scrollbar + m_searchResultsList->setSpacing(0); // Remove spacing between items + m_searchResultsList->setContentsMargins(0, 0, 0, 0); // Remove margins + + // Set custom delegate for mixed font rendering (bold first line, normal second line) + m_searchResultsList->setItemDelegate(new MixedFontDelegate(m_searchResultsList)); + + // Connect search results list signals + connect(m_searchResultsList, + &QListWidget::itemSelectionChanged, + this, + &PreferencesSearchController::onSearchResultSelected); + connect(m_searchResultsList, + &QListWidget::itemDoubleClicked, + this, + &PreferencesSearchController::onSearchResultDoubleClicked); + connect(m_searchResultsList, + &QListWidget::itemClicked, + this, + &PreferencesSearchController::onSearchResultClicked); + + // Install event filter for keyboard navigation in search results + m_searchResultsList->installEventFilter(m_parentDialog); +} + +void PreferencesSearchController::setPreferencesModel(QStandardItemModel* model) +{ + m_preferencesModel = model; +} + +void PreferencesSearchController::setGroupNameRole(int role) +{ + m_groupNameRole = role; +} + +void PreferencesSearchController::setPageNameRole(int role) +{ + m_pageNameRole = role; +} + +QListWidget* PreferencesSearchController::getSearchResultsList() const +{ + return m_searchResultsList; +} + +bool PreferencesSearchController::isPopupVisible() const +{ + return m_searchResultsList && m_searchResultsList->isVisible(); +} + +bool PreferencesSearchController::isPopupUnderMouse() const +{ + return m_searchResultsList && m_searchResultsList->underMouse(); +} + +bool PreferencesSearchController::isPopupAncestorOf(QWidget* widget) const +{ + return m_searchResultsList && m_searchResultsList->isAncestorOf(widget); +} + +void PreferencesSearchController::onSearchTextChanged(const QString& text) +{ + if (text.isEmpty()) { + clearHighlights(); + m_searchResults.clear(); + m_lastSearchText.clear(); + hideSearchResultsList(); + return; + } + + // Only perform new search if text changed + if (text != m_lastSearchText) { + performSearch(text); + m_lastSearchText = text; + } +} + +void PreferencesSearchController::performSearch(const QString& searchText) +{ + clearHighlights(); + m_searchResults.clear(); + + if (searchText.length() < 2) { + hideSearchResultsList(); + return; + } + + // Search through all groups and pages to collect ALL results + auto root = m_preferencesModel->invisibleRootItem(); + for (int i = 0; i < root->rowCount(); i++) { + auto groupItem = static_cast(root->child(i)); + auto groupName = groupItem->data(m_groupNameRole).toString(); + auto groupStack = qobject_cast(groupItem->getWidget()); + + if (!groupStack) { + continue; + } + + // Search in each page of the group + for (int j = 0; j < groupItem->rowCount(); j++) { + auto pageItem = static_cast(groupItem->child(j)); + auto pageName = pageItem->data(m_pageNameRole).toString(); + auto pageWidget = qobject_cast(pageItem->getWidget()); + + if (!pageWidget) { + continue; + } + + // Collect all matching widgets in this page + collectSearchResults(pageWidget, searchText, groupName, pageName, pageItem->text(), groupItem->text()); + } + } + + // Sort results by score (highest first) + std::sort(m_searchResults.begin(), m_searchResults.end(), + [](const SearchResult& a, const SearchResult& b) { + return a.score > b.score; + }); + + // Update UI with search results + if (!m_searchResults.isEmpty()) { + populateSearchResultsList(); + showSearchResultsList(); + } else { + hideSearchResultsList(); + } +} + +void PreferencesSearchController::clearHighlights() +{ + // Restore original styles for all highlighted widgets + for (int i = 0; i < m_highlightedWidgets.size(); ++i) { + QWidget* widget = m_highlightedWidgets.at(i); + if (widget && m_originalStyles.contains(widget)) { + widget->setStyleSheet(m_originalStyles[widget]); + } + } + m_highlightedWidgets.clear(); + m_originalStyles.clear(); +} + +void PreferencesSearchController::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName) +{ + if (!widget) { + return; + } + + const QString lowerSearchText = searchText.toLower(); + + // First, check if the page display name itself matches (highest priority) + int pageScore = 0; + if (fuzzyMatch(searchText, pageDisplayName, pageScore)) { + SearchResult result + { + .groupName = groupName, + .pageName = pageName, + .widget = widget, // Use the page widget itself + .matchText = pageDisplayName, // Use display name, not internal name + .groupBoxName = QString(), // No groupbox for page-level match + .tabName = tabName, + .pageDisplayName = pageDisplayName, + .isPageLevelMatch = true, // Mark as page-level match + .score = pageScore + 2000 // Boost page-level matches + }; + m_searchResults.append(result); + // Continue searching for individual items even if page matches + } + + // Search different widget types using the template method + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); +} + +void PreferencesSearchController::onSearchResultSelected() +{ + // This method is called when a search result is selected (arrow keys or single click) + // Navigate immediately but keep popup open + if (m_searchResultsList && m_searchResultsList->currentItem()) { + navigateToCurrentSearchResult(PopupAction::KeepOpen); + } + + ensureSearchBoxFocus(); +} + +void PreferencesSearchController::onSearchResultClicked() +{ + // Handle single click - navigate immediately but keep popup open + if (m_searchResultsList && m_searchResultsList->currentItem()) { + navigateToCurrentSearchResult(PopupAction::KeepOpen); + } + + ensureSearchBoxFocus(); +} + +void PreferencesSearchController::onSearchResultDoubleClicked() +{ + // Handle double click - navigate and close popup + if (m_searchResultsList && m_searchResultsList->currentItem()) { + navigateToCurrentSearchResult(PopupAction::CloseAfter); + } +} + +void PreferencesSearchController::navigateToCurrentSearchResult(PopupAction action) +{ + QListWidgetItem* currentItem = m_searchResultsList->currentItem(); + + // Skip if it's a separator (non-selectable item) or no item selected + if (!currentItem || !(currentItem->flags() & Qt::ItemIsSelectable)) { + return; + } + + // Get the result index directly from the item data + bool ok; + int resultIndex = currentItem->data(Qt::UserRole).toInt(&ok); + + if (ok && resultIndex >= 0 && resultIndex < m_searchResults.size()) { + const SearchResult& result = m_searchResults.at(resultIndex); + + // Emit signal to request navigation + Q_EMIT navigationRequested(result.groupName, result.pageName); + + // Clear any existing highlights + clearHighlights(); + + // Only highlight specific widgets for non-page-level matches + if (!result.isPageLevelMatch && !result.widget.isNull()) { + applyHighlightToWidget(result.widget); + } + // For page-level matches, we just navigate without highlighting anything + + // Close popup only if requested (double-click or Enter) + if (action == PopupAction::CloseAfter) { + hideSearchResultsList(); + } + } +} + +void PreferencesSearchController::populateSearchResultsList() +{ + m_searchResultsList->clear(); + + for (int i = 0; i < m_searchResults.size(); ++i) { + const SearchResult& result = m_searchResults.at(i); + + // Create item without setting DisplayRole + QListWidgetItem* item = new QListWidgetItem(); + + // Store path and widget text in separate roles + if (result.isPageLevelMatch) { + // For page matches: parent group as header, page name as content + item->setData(PathRole, result.tabName); + item->setData(WidgetTextRole, result.pageDisplayName); + } else { + // For widget matches: full path as header, widget text as content + QString pathText = result.tabName + QStringLiteral("/") + result.pageDisplayName; + item->setData(PathRole, pathText); + item->setData(WidgetTextRole, result.matchText); + } + item->setData(Qt::UserRole, i); // Keep existing index storage + + m_searchResultsList->addItem(item); + } + + // Select first actual item (not separator) + if (!m_searchResults.isEmpty()) { + m_searchResultsList->setCurrentRow(0); + } +} + +void PreferencesSearchController::hideSearchResultsList() +{ + m_searchResultsList->setVisible(false); +} + +void PreferencesSearchController::showSearchResultsList() +{ + // Configure popup size and position + configurePopupSize(); + + // Show the popup + m_searchResultsList->setVisible(true); + m_searchResultsList->raise(); + + // Use QTimer to ensure focus returns to search box after Qt finishes processing the popup show event + QTimer::singleShot(0, this, [this]() { + if (m_searchBox) { + m_searchBox->setFocus(); + m_searchBox->activateWindow(); + } + }); +} + +QString PreferencesSearchController::findGroupBoxForWidget(QWidget* widget) +{ + if (!widget) { + return QString(); + } + + // Walk up the parent hierarchy to find a QGroupBox + QWidget* parent = widget->parentWidget(); + while (parent) { + QGroupBox* groupBox = qobject_cast(parent); + if (groupBox) { + return groupBox->title(); + } + parent = parent->parentWidget(); + } + + return QString(); +} + + + +template +void PreferencesSearchController::searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName) +{ + const QList widgets = parentWidget->findChildren(); + + for (WidgetType* widget : widgets) { + QString widgetText; + + // Get text based on widget type + if constexpr (std::is_same_v) { + widgetText = widget->text(); + } else if constexpr (std::is_same_v) { + widgetText = widget->text(); + } else if constexpr (std::is_same_v) { + widgetText = widget->text(); + } else if constexpr (std::is_same_v) { + widgetText = widget->text(); + } + + // Use fuzzy matching instead of simple contains + int score = 0; + if (fuzzyMatch(searchText, widgetText, score)) { + SearchResult result + { + .groupName = groupName, + .pageName = pageName, + .widget = widget, + .matchText = widgetText, + .groupBoxName = findGroupBoxForWidget(widget), + .tabName = tabName, + .pageDisplayName = pageDisplayName, + .isPageLevelMatch = false, + .score = score + }; + m_searchResults.append(result); + } + } +} + +int PreferencesSearchController::calculatePopupHeight(int popupWidth) +{ + int totalHeight = 0; + int itemCount = m_searchResultsList->count(); + int visibleItemCount = 0; + const int maxVisibleItems = 4; + + for (int i = 0; i < itemCount && visibleItemCount < maxVisibleItems; ++i) { + QListWidgetItem* item = m_searchResultsList->item(i); + if (!item) continue; + + // For separator items, use their widget height + if (m_searchResultsList->itemWidget(item)) { + totalHeight += m_searchResultsList->itemWidget(item)->sizeHint().height(); + } else { + // For text items, use the delegate's size hint instead of calculating manually + QStyleOptionViewItem option; + option.rect = QRect(0, 0, popupWidth, 100); // Temporary rect for calculation + option.font = m_searchResultsList->font(); + + QSize delegateSize = m_searchResultsList->itemDelegate()->sizeHint(option, m_searchResultsList->model()->index(i, 0)); + totalHeight += delegateSize.height(); + + visibleItemCount++; // Only count actual items, not separators + } + } + + return qMax(50, totalHeight); // Minimum 50px height +} + +void PreferencesSearchController::configurePopupSize() +{ + if (m_searchResults.isEmpty()) { + hideSearchResultsList(); + return; + } + + // Set a fixed width to prevent flashing when content changes + int popupWidth = 300; // Fixed width for consistent appearance + m_searchResultsList->setFixedWidth(popupWidth); + + // Calculate and set the height + int finalHeight = calculatePopupHeight(popupWidth); + m_searchResultsList->setFixedHeight(finalHeight); + + // Position the popup's upper-left corner at the upper-right corner of the search box + QPoint globalPos = m_searchBox->mapToGlobal(QPoint(m_searchBox->width(), 0)); + + // Check if popup would go off-screen to the right + QScreen* screen = QApplication::screenAt(globalPos); + if (!screen) { + screen = QApplication::primaryScreen(); + } + QRect screenGeometry = screen->availableGeometry(); + + // If popup would extend beyond right edge of screen, position it below the search box instead + if (globalPos.x() + popupWidth > screenGeometry.right()) { + globalPos = m_searchBox->mapToGlobal(QPoint(0, m_searchBox->height())); + } + + m_searchResultsList->move(globalPos); +} + +// Fuzzy search implementation + +bool PreferencesSearchController::isExactMatch(const QString& searchText, const QString& targetText) +{ + return targetText.toLower().contains(searchText.toLower()); +} + +bool PreferencesSearchController::fuzzyMatch(const QString& searchText, const QString& targetText, int& score) +{ + if (searchText.isEmpty()) { + score = 0; + return true; + } + + const QString lowerSearch = searchText.toLower(); + const QString lowerTarget = targetText.toLower(); + + // First check for exact substring match (highest score) + if (lowerTarget.contains(lowerSearch)) { + // Score based on how early the match appears and how much of the string it covers + int matchIndex = lowerTarget.indexOf(lowerSearch); + int coverage = (lowerSearch.length() * 100) / lowerTarget.length(); // Percentage coverage + score = 1000 - matchIndex + coverage; // Higher score for earlier matches and better coverage + return true; + } + + // For fuzzy matching, require minimum search length to avoid too many false positives + if (lowerSearch.length() < 3) { + score = 0; + return false; + } + + // Fuzzy matching: check if all characters appear in order + int searchIndex = 0; + int targetIndex = 0; + int consecutiveMatches = 0; + int maxConsecutive = 0; + int totalMatches = 0; + int firstMatchIndex = -1; + int lastMatchIndex = -1; + + while (searchIndex < lowerSearch.length() && targetIndex < lowerTarget.length()) { + if (lowerSearch[searchIndex] == lowerTarget[targetIndex]) { + if (firstMatchIndex == -1) { + firstMatchIndex = targetIndex; + } + lastMatchIndex = targetIndex; + searchIndex++; + totalMatches++; + consecutiveMatches++; + maxConsecutive = qMax(maxConsecutive, consecutiveMatches); + } else { + consecutiveMatches = 0; + } + targetIndex++; + } + + // Check if all search characters were found + if (searchIndex == lowerSearch.length()) { + // Calculate match density - how spread out are the matches? + int matchSpan = lastMatchIndex - firstMatchIndex + 1; + int density = (lowerSearch.length() * 100) / matchSpan; // Characters per span + + // Require minimum density - matches shouldn't be too spread out + if (density < 20) { // Less than 20% density is too sparse + score = 0; + return false; + } + + // Require minimum coverage of search term + int coverage = (lowerSearch.length() * 100) / lowerTarget.length(); + if (coverage < 15 && lowerTarget.length() > 20) { // For long strings, require better coverage + score = 0; + return false; + } + + // Calculate score based on: + // - Match density (how compact the matches are) + // - Consecutive matches bonus + // - Coverage (how much of target string is the search term) + // - Position bonus (earlier matches are better) + int densityScore = qMin(density, 100); // Cap at 100 + int consecutiveBonus = (maxConsecutive * 30) / lowerSearch.length(); + int coverageScore = qMin(coverage * 2, 100); // Coverage is important + int positionBonus = qMax(0, 50 - firstMatchIndex); // Earlier is better + + score = densityScore + consecutiveBonus + coverageScore + positionBonus; + + // Minimum score threshold for fuzzy matches + if (score < 80) { + score = 0; + return false; + } + + return true; + } + + score = 0; + return false; +} + +void PreferencesSearchController::ensureSearchBoxFocus() +{ + if (m_searchBox && !m_searchBox->hasFocus()) { + m_searchBox->setFocus(); + } +} + +QString PreferencesSearchController::getHighlightStyleForWidget(QWidget* widget) +{ + const QString baseStyle = QStringLiteral("background-color: #E3F2FD; color: #1565C0; border: 2px solid #2196F3; border-radius: 3px;"); + + if (qobject_cast(widget)) { + return QStringLiteral("QLabel { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QCheckBox { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QRadioButton { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QGroupBox::title { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QPushButton { ") + baseStyle + QStringLiteral(" }"); + } else { + return QStringLiteral("QWidget { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } +} + +void PreferencesSearchController::applyHighlightToWidget(QWidget* widget) +{ + if (!widget) { + return; + } + + m_originalStyles[widget] = widget->styleSheet(); + widget->setStyleSheet(getHighlightStyleForWidget(widget)); + m_highlightedWidgets.append(widget); +} + +bool PreferencesSearchController::handleSearchBoxKeyPress(QKeyEvent* keyEvent) +{ + if (!m_searchResultsList->isVisible() || m_searchResults.isEmpty()) { + return false; + } + + switch (keyEvent->key()) { + case Qt::Key_Down: { + // Move selection down in popup, skipping separators + int currentRow = m_searchResultsList->currentRow(); + int totalItems = m_searchResultsList->count(); + for (int i = 1; i < totalItems; ++i) { + int nextRow = (currentRow + i) % totalItems; + QListWidgetItem* item = m_searchResultsList->item(nextRow); + if (item && (item->flags() & Qt::ItemIsSelectable)) { + m_searchResultsList->setCurrentRow(nextRow); + break; + } + } + return true; + } + case Qt::Key_Up: { + // Move selection up in popup, skipping separators + int currentRow = m_searchResultsList->currentRow(); + int totalItems = m_searchResultsList->count(); + for (int i = 1; i < totalItems; ++i) { + int prevRow = (currentRow - i + totalItems) % totalItems; + QListWidgetItem* item = m_searchResultsList->item(prevRow); + if (item && (item->flags() & Qt::ItemIsSelectable)) { + m_searchResultsList->setCurrentRow(prevRow); + break; + } + } + return true; + } + case Qt::Key_Return: + case Qt::Key_Enter: + navigateToCurrentSearchResult(PopupAction::CloseAfter); + return true; + case Qt::Key_Escape: + hideSearchResultsList(); + return true; + default: + return false; + } +} + +bool PreferencesSearchController::handlePopupKeyPress(QKeyEvent* keyEvent) +{ + switch (keyEvent->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + navigateToCurrentSearchResult(PopupAction::CloseAfter); + return true; + case Qt::Key_Escape: + hideSearchResultsList(); + ensureSearchBoxFocus(); + return true; + default: + return false; + } +} + +bool PreferencesSearchController::isClickOutsidePopup(QMouseEvent* mouseEvent) +{ +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) + QPoint globalPos = mouseEvent->globalPos(); +#else + QPoint globalPos = mouseEvent->globalPosition().toPoint(); +#endif + QRect searchBoxRect = QRect(m_searchBox->mapToGlobal(QPoint(0, 0)), m_searchBox->size()); + QRect popupRect = QRect(m_searchResultsList->mapToGlobal(QPoint(0, 0)), m_searchResultsList->size()); + + return !searchBoxRect.contains(globalPos) && !popupRect.contains(globalPos); +} + +bool DlgPreferencesImp::eventFilter(QObject* obj, QEvent* event) +{ + // Handle search box key presses + if (obj == ui->searchBox && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + return m_searchController->handleSearchBoxKeyPress(keyEvent); + } + + // Handle popup key presses + if (obj == m_searchController->getSearchResultsList() && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + return m_searchController->handlePopupKeyPress(keyEvent); + } + + // Prevent popup from stealing focus + if (obj == m_searchController->getSearchResultsList() && event->type() == QEvent::FocusIn) { + m_searchController->ensureSearchBoxFocus(); + return true; + } + + // Handle search box focus loss + if (obj == ui->searchBox && event->type() == QEvent::FocusOut) { + QFocusEvent* focusEvent = static_cast(event); + if (focusEvent->reason() != Qt::PopupFocusReason && + focusEvent->reason() != Qt::MouseFocusReason) { + // Only hide if focus is going somewhere else, not due to popup interaction + QTimer::singleShot(100, this, [this]() { + if (!ui->searchBox->hasFocus() && !m_searchController->isPopupUnderMouse()) { + m_searchController->hideSearchResultsList(); + } + }); + } + } + + // Handle clicks outside popup + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseEvent = static_cast(event); + QWidget* widget = qobject_cast(obj); + + // Check if click is outside search area + if (m_searchController->isPopupVisible() && + obj != m_searchController->getSearchResultsList() && + obj != ui->searchBox && + widget && // Only check if obj is actually a QWidget + !m_searchController->isPopupAncestorOf(widget) && + !ui->searchBox->isAncestorOf(widget)) { + + if (m_searchController->isClickOutsidePopup(mouseEvent)) { + m_searchController->hideSearchResultsList(); + m_searchController->clearHighlights(); + } + } + } + + return QDialog::eventFilter(obj, event); +} + #include "moc_DlgPreferencesImp.cpp" diff --git a/src/Gui/Dialogs/DlgPreferencesImp.h b/src/Gui/Dialogs/DlgPreferencesImp.h index 9d40d1d308..57c9d367ed 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.h +++ b/src/Gui/Dialogs/DlgPreferencesImp.h @@ -29,16 +29,134 @@ #include #include #include +#include +#include +#include #include #include class QAbstractButton; class QListWidgetItem; class QTabWidget; +class QKeyEvent; +class QMouseEvent; namespace Gui::Dialog { class PreferencePage; class Ui_DlgPreferences; +class DlgPreferencesImp; + +class GuiExport PreferencesSearchController : public QObject +{ + Q_OBJECT + +private: + enum class PopupAction { + KeepOpen, // don't close popup (used for keyboard navigation) + CloseAfter // close popup (used for mouse clicks and Enter/Return) + }; + +public: + // Custom data roles for separating path and widget text + enum SearchDataRole { + PathRole = Qt::UserRole + 10, // Path to page (e.g., "Display/3D View") + WidgetTextRole = Qt::UserRole + 11 // Text from the widget (e.g., "Enable anti-aliasing") + }; + + // Search results structure + struct SearchResult { + QString groupName; + QString pageName; + QPointer widget; + QString matchText; + QString groupBoxName; + QString tabName; // The tab name (like "Display") + QString pageDisplayName; // The page display name (like "3D View") + bool isPageLevelMatch = false; // True if this is a page title match + int score = 0; // Fuzzy search score for sorting + }; + + explicit PreferencesSearchController(DlgPreferencesImp* parentDialog, QObject* parent = nullptr); + ~PreferencesSearchController() = default; + + // Setup methods + void setPreferencesModel(QStandardItemModel* model); + void setGroupNameRole(int role); + void setPageNameRole(int role); + + // UI access methods + QListWidget* getSearchResultsList() const; + bool isPopupVisible() const; + bool isPopupUnderMouse() const; + bool isPopupAncestorOf(QWidget* widget) const; + + // Event handling + bool handleSearchBoxKeyPress(QKeyEvent* keyEvent); + bool handlePopupKeyPress(QKeyEvent* keyEvent); + bool isClickOutsidePopup(QMouseEvent* mouseEvent); + + // Focus management + void ensureSearchBoxFocus(); + + // Search functionality + void performSearch(const QString& searchText); + void clearHighlights(); + void hideSearchResultsList(); + void showSearchResultsList(); + + // Navigation + void navigateToCurrentSearchResult(PopupAction action); + +Q_SIGNALS: + void navigationRequested(const QString& groupName, const QString& pageName); + +public Q_SLOTS: + void onSearchTextChanged(const QString& text); + void onSearchResultSelected(); + void onSearchResultClicked(); + void onSearchResultDoubleClicked(); + +private: + // Search implementation + void collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName); + void populateSearchResultsList(); + + template + void searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName); + + // UI helpers + void configurePopupSize(); + int calculatePopupHeight(int popupWidth); + void applyHighlightToWidget(QWidget* widget); + QString getHighlightStyleForWidget(QWidget* widget); + + // Search result navigation + void selectNextSearchResult(); + void selectPreviousSearchResult(); + + // Utility methods + QString findGroupBoxForWidget(QWidget* widget); + bool fuzzyMatch(const QString& searchText, const QString& targetText, int& score); + bool isExactMatch(const QString& searchText, const QString& targetText); + +private: + DlgPreferencesImp* m_parentDialog; + QStandardItemModel* m_preferencesModel; + int m_groupNameRole; + int m_pageNameRole; + + // UI components + QLineEdit* m_searchBox; + QListWidget* m_searchResultsList; + + // Search state + QList m_searchResults; + QString m_lastSearchText; + QList m_highlightedWidgets; + QMap m_originalStyles; +}; class PreferencesPageItem : public QStandardItem { @@ -155,6 +273,7 @@ public: protected: void changeEvent(QEvent *e) override; void showEvent(QShowEvent*) override; + bool eventFilter(QObject* obj, QEvent* event) override; protected Q_SLOTS: void onButtonBoxClicked(QAbstractButton*); @@ -163,6 +282,8 @@ protected Q_SLOTS: void onGroupExpanded(const QModelIndex &index); void onGroupCollapsed(const QModelIndex &index); + + void onNavigationRequested(const QString& groupName, const QString& pageName); private: /** @name for internal use only */ @@ -190,6 +311,9 @@ private: int minimumPageWidth() const; int minimumDialogWidth(int) const; void expandToMinimumDialogWidth(); + + // Navigation helper for search controller + void navigateToSearchResult(const QString& groupName, const QString& pageName); //@} private: @@ -210,6 +334,9 @@ private: bool invalidParameter; bool canEmbedScrollArea; bool restartRequired; + + // Search controller + std::unique_ptr m_searchController; /**< A name for our Qt::UserRole, used when storing user data in a list item */ static const int GroupNameRole; @@ -219,6 +346,9 @@ private: static constexpr char const* PageNameProperty = "PageName"; static DlgPreferencesImp* _activeDialog; /**< Defaults to the nullptr, points to the current instance if there is one */ + + // Friend class to allow search controller access to UI + friend class PreferencesSearchController; }; } // namespace Gui diff --git a/src/Gui/Dialogs/DlgPropertyLink.cpp b/src/Gui/Dialogs/DlgPropertyLink.cpp index 17dd3a06ef..87ec251ded 100644 --- a/src/Gui/Dialogs/DlgPropertyLink.cpp +++ b/src/Gui/Dialogs/DlgPropertyLink.cpp @@ -173,8 +173,8 @@ DlgPropertyLink::formatObject(App::Document* ownerDoc, App::DocumentObject* obj, if (obj->Label.getStrValue() == obj->getNameInDocument()) { return QLatin1String(objName); } - return QStringLiteral("%1 (%2)").arg(QLatin1String(objName), - QString::fromUtf8(obj->Label.getValue())); + return QStringLiteral("%1 (%2)").arg(QString::fromUtf8(obj->Label.getValue()), + QLatin1String(objName)); } auto sobj = obj->getSubObject(sub); @@ -182,10 +182,10 @@ DlgPropertyLink::formatObject(App::Document* ownerDoc, App::DocumentObject* obj, return QStringLiteral("%1.%2").arg(QLatin1String(objName), QString::fromUtf8(sub)); } - return QStringLiteral("%1.%2 (%3)") - .arg(QLatin1String(objName), - QString::fromUtf8(sub), - QString::fromUtf8(sobj->Label.getValue())); + return QStringLiteral("%1 (%2.%3)") + .arg(QString::fromUtf8(sobj->Label.getValue()), + QLatin1String(objName), + QString::fromUtf8(sub)); } static inline bool isLinkSub(const QList& links) diff --git a/src/Gui/EditableDatumLabel.cpp b/src/Gui/EditableDatumLabel.cpp index f863b76ab0..d0d36d1182 100644 --- a/src/Gui/EditableDatumLabel.cpp +++ b/src/Gui/EditableDatumLabel.cpp @@ -33,8 +33,13 @@ #include #include +#include +#include +#include +#include #include +#include #include #include @@ -62,6 +67,7 @@ EditableDatumLabel::EditableDatumLabel(View3DInventorViewer* view, , value(0.0) , viewer(view) , spinBox(nullptr) + , lockIconLabel(nullptr) , cameraSensor(nullptr) , function(Function::Positioning) { @@ -151,6 +157,9 @@ void EditableDatumLabel::startEdit(double val, QObject* eventFilteringObj, bool return; } + // Reset locked state when starting to edit + this->resetLockedState(); + QWidget* mdi = viewer->parentWidget(); label->string = " "; @@ -206,14 +215,43 @@ bool EditableDatumLabel::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::KeyPress) { auto* keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Tab) { if (auto* spinBox = qobject_cast(watched)) { - this->hasFinishedEditing = true; - Q_EMIT this->valueChanged(this->value); - return false; + // if tab has been pressed and user did not type anything previously, + // then just cycle but don't lock anything, otherwise we lock the label + if (keyEvent->key() == Qt::Key_Tab && !this->isSet) { + if (!this->spinBox->hasValidInput()) { + Q_EMIT this->spinBox->valueChanged(this->value); + return true; + } + return false; + } + + // for ctrl + enter we accept values as they are + if (keyEvent->modifiers() & Qt::ControlModifier) { + Q_EMIT this->finishEditingOnAllOVPs(); + return true; + } + else { + // regular enter + this->hasFinishedEditing = true; + Q_EMIT this->spinBox->valueChanged(this->value); + + // only set lock state if it passed validation + // (validation can unset isSet if value didn't pass + // confusion point for example) + if (this->isSet) + this->setLockedAppearance(true); + return true; + } } } + else if (this->hasFinishedEditing && keyEvent->key() != Qt::Key_Tab) + { + this->setLockedAppearance(false); + return false; + } } return QObject::eventFilter(watched, event); @@ -233,6 +271,9 @@ void EditableDatumLabel::stopEdit() spinBox->deleteLater(); spinBox = nullptr; + + // Lock icon will be automatically destroyed as it's a child of spinbox + lockIconLabel = nullptr; } } @@ -311,6 +352,19 @@ void EditableDatumLabel::positionSpinbox() pxCoord.setX(posX); pxCoord.setY(posY); spinBox->move(pxCoord); + + // Update lock icon position inside the spinbox if it exists and is visible + if (lockIconLabel && lockIconLabel->isVisible()) { + int iconSize = 14; + int padding = 4; + QSize spinboxSize = spinBox->size(); + lockIconLabel->setGeometry( + spinboxSize.width() - iconSize - padding, + (spinboxSize.height() - iconSize) / 2, + iconSize, + iconSize + ); + } } SbVec3f EditableDatumLabel::getTextCenterPoint() const @@ -441,6 +495,64 @@ void EditableDatumLabel::setSpinboxVisibleToMouse(bool val) spinBox->setAttribute(Qt::WA_TransparentForMouseEvents, !val); } +void EditableDatumLabel::setLockedAppearance(bool locked) +{ + if (locked) { + if (spinBox) { + QWidget* mdi = viewer->parentWidget(); + + // create lock icon label it it doesn't exist, if it does - show it + if (!lockIconLabel) { + lockIconLabel = new QLabel(spinBox); + lockIconLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true); + lockIconLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + // load icon and scale it to fit in spinbox + QPixmap lockIcon = Gui::BitmapFactory().pixmap("Constraint_Lock"); + QPixmap scaledIcon = + lockIcon.scaled(14, 14, Qt::KeepAspectRatio, Qt::SmoothTransformation); + lockIconLabel->setPixmap(scaledIcon); + + // position lock icon inside the spinbox + int iconSize = 14; + int padding = 4; + QSize spinboxSize = spinBox->size(); + lockIconLabel->setGeometry(spinboxSize.width() - iconSize - padding, + (spinboxSize.height() - iconSize) / 2, + iconSize, + iconSize); + // style spinbox and add padding for lock + QString styleSheet = QString::fromLatin1("QSpinBox { " + "padding-right: %1px; " + "}") + .arg(iconSize + padding + 2); + + spinBox->setStyleSheet(styleSheet); + } + + lockIconLabel->show(); + } + } else { + this->hasFinishedEditing = false; + + // if spinbox exists, reset its appearance + if (spinBox) { + spinBox->setStyleSheet(QString()); + + // hide lock icon if it exists for later reuse + if (lockIconLabel) { + lockIconLabel->hide(); + } + } + } +} + +void EditableDatumLabel::resetLockedState() +{ + hasFinishedEditing = false; + setLockedAppearance(false); +} + EditableDatumLabel::Function EditableDatumLabel::getFunction() { return function; diff --git a/src/Gui/EditableDatumLabel.h b/src/Gui/EditableDatumLabel.h index 380532a681..6b3aad0b78 100644 --- a/src/Gui/EditableDatumLabel.h +++ b/src/Gui/EditableDatumLabel.h @@ -26,6 +26,7 @@ #include #include +#include #include #include "SoDatumLabel.h" @@ -72,6 +73,7 @@ public: void setPoints(SbVec3f p1, SbVec3f p2); void setPoints(Base::Vector3d p1, Base::Vector3d p2); void setFocusToSpinbox(); + void clearSelection(); ///< Clears text selection in the spinbox void setLabelType(SoDatumLabel::Type type, Function function = Function::Positioning); void setLabelDistance(double val); void setLabelStartAngle(double val); @@ -79,6 +81,8 @@ public: void setLabelRecommendedDistance(); void setLabelAutoDistanceReverse(bool val); void setSpinboxVisibleToMouse(bool val); + void setLockedAppearance(bool locked); ///< Sets visual appearance to indicate locked state (finished editing) + void resetLockedState(); ///< Resets both hasFinishedEditing flag and locked appearance Function getFunction(); @@ -95,6 +99,7 @@ public: Q_SIGNALS: void valueChanged(double val); void parameterUnset(); + void finishEditingOnAllOVPs(); ///< Emitted when Ctrl+Enter is pressed to finish editing on all visible OVPs protected: bool eventFilter(QObject* watched, QEvent* event) override; @@ -109,6 +114,7 @@ private: SoTransform* transform; QPointer viewer; QuantitySpinBox* spinBox; + QLabel* lockIconLabel; ///< Label to display lock icon next to spinbox SoNodeSensor* cameraSensor; SbVec3f midpos; diff --git a/src/Gui/ExpressionCompleter.cpp b/src/Gui/ExpressionCompleter.cpp index 1499bf2ce6..332cfbc692 100644 --- a/src/Gui/ExpressionCompleter.cpp +++ b/src/Gui/ExpressionCompleter.cpp @@ -921,15 +921,15 @@ ExpressionLineEdit::ExpressionLineEdit(QWidget* parent, , noProperty(noProperty) , exactMatch(false) , checkInList(checkInList) - , checkPrefix(checkPrefix) { - setValidator(new ExpressionValidator(this)); + setPrefix(checkPrefix); connect(this, &QLineEdit::textEdited, this, &ExpressionLineEdit::slotTextChanged); } void ExpressionLineEdit::setPrefix(char prefix) { checkPrefix = prefix; + setValidator(checkPrefix == '=' ? nullptr : new ExpressionValidator(this)); } void ExpressionLineEdit::setDocumentObject(const App::DocumentObject* currentDocObj, diff --git a/src/Gui/Icons/Std_DockOverlayToggleBottom.svg b/src/Gui/Icons/Std_DockOverlayToggleBottom.svg new file mode 100755 index 0000000000..98038cb107 --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleBottom.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/Std_DockOverlayToggleLeft.svg b/src/Gui/Icons/Std_DockOverlayToggleLeft.svg new file mode 100755 index 0000000000..d19b0c4660 --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleLeft.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/Std_DockOverlayToggleRight.svg b/src/Gui/Icons/Std_DockOverlayToggleRight.svg new file mode 100755 index 0000000000..792c4db80d --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleRight.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/Std_DockOverlayToggleTop.svg b/src/Gui/Icons/Std_DockOverlayToggleTop.svg new file mode 100755 index 0000000000..81ceb9aa22 --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleTop.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/resource.qrc b/src/Gui/Icons/resource.qrc index 1621edbb4a..86f5c4d0f9 100644 --- a/src/Gui/Icons/resource.qrc +++ b/src/Gui/Icons/resource.qrc @@ -1,178 +1,181 @@ - freecadsplash.png - freecadsplash0.png - freecadsplash1.png - freecadsplash2.png - freecadsplash3.png - freecadsplash4.png - freecadsplash5.png - freecadsplash6.png - freecadsplash7.png - freecadsplash8.png - freecadsplash9.png - freecadsplash10.png - freecadsplash11.png - freecadsplash12.png - freecadsplash_2x.png - freecadsplash0_2x.png - freecadsplash1_2x.png - freecadsplash2_2x.png - freecadsplash3_2x.png - freecadsplash4_2x.png - freecadsplash5_2x.png - freecadsplash6_2x.png - freecadsplash7_2x.png - freecadsplash8_2x.png - freecadsplash9_2x.png - freecadsplash10_2x.png - freecadsplash11_2x.png - freecadsplash12_2x.png - freecadabout.png + 3dx_pivot.png + accessories-calculator.svg + accessories-text-editor.svg + AddonManager.svg + align-to-selection.svg + application-exit.svg + applications-accessories.svg + applications-python.svg background.png - mouse_pointer.svg - Document.svg - Feature.svg - delete.svg - list-remove.svg - list-add.svg - freecad.svg - freecad-doc.png - freecad-doc.svg + bound-expression-unset.svg + bound-expression.svg + breakpoint.svg bulb.svg - TextDocument.svg + button_add_all.svg button_down.svg + button_invalid.svg button_left.svg button_right.svg - button_up.svg button_sort.svg - button_add_all.svg + button_up.svg button_valid.svg - button_invalid.svg - media-playback-start.svg - media-playback-start-back.svg - media-playback-step.svg - media-playback-step-back.svg - media-record.svg - media-playback-stop.svg - preferences-display.svg - preferences-python.svg - preferences-general.svg - preferences-import-export.svg - preferences-workbenches.svg - utilities-terminal.svg ClassBrowser/const_member.png - ClassBrowser/member.png - ClassBrowser/method.png - ClassBrowser/property.png - ClassBrowser/type_class.png - ClassBrowser/type_enum.png - ClassBrowser/type_module.png ClassBrowser/const_member.svg + ClassBrowser/member.png ClassBrowser/member.svg + ClassBrowser/method.png ClassBrowser/method.svg + ClassBrowser/property.png ClassBrowser/property.svg + ClassBrowser/type_class.png ClassBrowser/type_class.svg + ClassBrowser/type_enum.png ClassBrowser/type_enum.svg + ClassBrowser/type_module.png ClassBrowser/type_module.svg - style/windows_branch_closed.png - style/windows_branch_open.png - Std_ViewScreenShot.svg - bound-expression.svg - bound-expression-unset.svg - breakpoint.svg + clear-selection.svg + colors.svg + critical-info.svg + cursor-through.svg + dagViewFail.svg + dagViewPass.svg + dagViewPending.svg + dagViewVisible.svg debug-marker.svg debug-start.svg debug-stop.svg + delete.svg document-new.svg document-open.svg - document-save.svg - document-save-as.svg - document-print.svg + document-package.svg document-print-preview.svg + document-print.svg document-properties.svg - application-exit.svg - edit_OK.svg + document-python.svg + document-save-as.svg + document-save.svg + Document.svg + DrawStyleAsIs.svg + DrawStyleFlatLines.svg + DrawStyleHiddenLine.svg + DrawStyleNoShading.svg + DrawStylePoints.svg + DrawStyleShaded.svg + DrawStyleWireFrame.svg + edge-selection.svg edit_Cancel.svg + edit_OK.svg + edit-cleartext.svg edit-copy.svg edit-cut.svg edit-delete.svg - edit-paste.svg - edit-select-all.svg - edit-select-box.svg - edit-select-box-cross.svg - edit-element-select-box.svg - edit-element-select-box-cross.svg - edit-redo.svg - edit-undo.svg edit-edit.svg - edit-cleartext.svg + edit-element-select-box-cross.svg + edit-element-select-box.svg + edit-paste.svg + edit-redo.svg + edit-select-all.svg + edit-select-box-cross.svg + edit-select-box.svg + edit-undo.svg + face-selection.svg + feature_suppressed.svg + Feature.svg + folder.svg + forbidden.svg + freecad-doc.png + freecad-doc.svg + freecad.svg + freecadabout.png + freecadsplash_2x.png + freecadsplash.png + freecadsplash0_2x.png + freecadsplash0.png + freecadsplash1_2x.png + freecadsplash1.png + freecadsplash10_2x.png + freecadsplash10.png + freecadsplash11_2x.png + freecadsplash11.png + freecadsplash12_2x.png + freecadsplash12.png + freecadsplash2_2x.png + freecadsplash2.png + freecadsplash3_2x.png + freecadsplash3.png + freecadsplash4_2x.png + freecadsplash4.png + freecadsplash5_2x.png + freecadsplash5.png + freecadsplash6_2x.png + freecadsplash6.png + freecadsplash7_2x.png + freecadsplash7.png + freecadsplash8_2x.png + freecadsplash8.png + freecadsplash9_2x.png + freecadsplash9.png + Geoassembly.svg + Geofeaturegroup.svg + Group.svg + help-browser.svg + image-open.svg + image-plane.svg + image-scaling.svg info.svg - critical-info.svg - tree-item-drag.svg - tree-goto-sel.svg - tree-rec-sel.svg - tree-pre-sel.svg - tree-sync-sel.svg - tree-sync-view.svg - tree-sync-pla.svg - tree-doc-single.svg - tree-doc-multi.svg - tree-doc-collapse.svg + internet-web-browser.svg + InTray_missed_notifications.svg + InTray.svg + Invisible.svg + Link.svg + LinkArray.svg + LinkArrayOverlay.svg + LinkElement.svg + LinkGroup.svg + LinkImport.svg + LinkImportAll.svg + LinkOverlay.svg + LinkReplace.svg + LinkSelect.svg + LinkSelectAll.svg + LinkSelectFinal.svg + LinkSub.svg + LinkSubElement.svg + LinkSubOverlay.svg + list-add.svg + list-remove.svg + MacroEditor.svg + MacroFolder.svg + media-playback-start-back.svg + media-playback-start.svg + media-playback-step-back.svg + media-playback-step.svg + media-playback-stop.svg + media-record.svg + mouse_pointer.svg + overlay_error.svg + overlay_recompute.svg + Param_Bool.svg + Param_Float.svg + Param_Int.svg + Param_Text.svg + Param_UInt.svg + PolygonPick.svg + preferences-display.svg + preferences-general.svg + preferences-import-export.svg + preferences-python.svg + preferences-system.svg + preferences-workbenches.svg + process-stop.svg + px.svg + safe-mode-restart.svg sel-back.svg + sel-bbox.svg sel-forward.svg sel-instance.svg - sel-bbox.svg - vertex-selection.svg - edge-selection.svg - face-selection.svg - clear-selection.svg - help-browser.svg - preferences-system.svg - process-stop.svg - window-new.svg - applications-accessories.svg - applications-python.svg - accessories-text-editor.svg - accessories-calculator.svg - internet-web-browser.svg - InTray.svg - InTray_missed_notifications.svg - safe-mode-restart.svg - view-select.svg - view-unselectable.svg - view-refresh.svg - view-fullscreen.svg - view-axonometric.svg - view-isometric.svg - view-perspective.svg - view-bottom.svg - view-front.svg - view-left.svg - view-rear.svg - view-right.svg - view-top.svg - zoom-all.svg - zoom-border.svg - zoom-border-cross.svg - zoom-fit-best.svg - zoom-in.svg - zoom-out.svg - zoom-selection.svg - view-rotate-left.svg - view-rotate-right.svg - view-measurement.svg - view-measurement-cross.svg - umf-measurement.svg - Tree_Annotation.svg - Tree_Dimension.svg - Tree_Python.svg - TreeItemVisible.svg - TreeItemInvisible.svg - dagViewVisible.svg - dagViewPass.svg - dagViewFail.svg - dagViewPending.svg spaceball_button.svg SpNav-PanLR.svg SpNav-PanUD.svg @@ -180,52 +183,54 @@ SpNav-Spin.svg SpNav-Tilt.svg SpNav-Zoom.svg - DrawStyleAsIs.svg - DrawStyleFlatLines.svg - DrawStylePoints.svg - DrawStyleShaded.svg - DrawStyleWireFrame.svg - DrawStyleHiddenLine.svg - DrawStyleNoShading.svg - user.svg + Std_Alignment.svg + Std_Axis.svg Std_AxisCross.svg - Std_CoordinateSystem.svg - Std_CoordinateSystem_alt.svg - Std_Placement.svg - MacroEditor.svg - MacroFolder.svg - Param_Bool.svg - Param_Float.svg - Param_Int.svg - Param_Text.svg - Param_UInt.svg - PolygonPick.svg Std_CloseActiveWindow.svg Std_CloseAllWindows.svg + Std_CoordinateSystem_alt.svg + Std_CoordinateSystem.svg + Std_DemoMode.svg + Std_DependencyGraph.svg + Std_DlgParameter.svg + Std_DockOverlayToggleBottom.svg + Std_DockOverlayToggleLeft.svg + Std_DockOverlayToggleRight.svg + Std_DockOverlayToggleTop.svg + Std_DuplicateSelection.svg Std_Export.svg Std_HideObjects.svg Std_HideSelection.svg Std_Import.svg - Std_MergeProjects.svg Std_MarkToRecompute.svg + Std_MergeProjects.svg + Std_Placement.svg + Std_Plane.svg + Std_Point.svg Std_PrintPdf.svg + Std_ProjectUtil.svg Std_RandomColor.svg Std_RecentFiles.svg Std_RecentMacros.svg Std_Revert.svg Std_SaveAll.svg Std_SaveCopy.svg + Std_SceneInspector.svg Std_SelectVisibleObjects.svg Std_SetAppearance.svg Std_ShowObjects.svg Std_ShowSelection.svg Std_TextureMapping.svg Std_ToggleClipPlane.svg + Std_ToggleFreeze.svg Std_ToggleNavigation.svg Std_ToggleObjects.svg - Std_ToggleVisibility.svg Std_ToggleTransparency.svg + Std_ToggleVisibility.svg Std_Tool1.svg + Std_Tool10.svg + Std_Tool11.svg + Std_Tool12.svg Std_Tool2.svg Std_Tool3.svg Std_Tool4.svg @@ -234,10 +239,11 @@ Std_Tool7.svg Std_Tool8.svg Std_Tool9.svg - Std_Tool10.svg - Std_Tool11.svg - Std_Tool12.svg Std_TransformManip.svg + Std_UserEditModeColor.svg + Std_UserEditModeCutting.svg + Std_UserEditModeDefault.svg + Std_UserEditModeTransform.svg Std_ViewDimetric.svg Std_ViewHome.svg Std_ViewIvIssueCamPos.svg @@ -246,73 +252,71 @@ Std_ViewIvStereoOff.svg Std_ViewIvStereoQuadBuff.svg Std_ViewIvStereoRedGreen.svg + Std_ViewScreenShot.svg Std_ViewTrimetric.svg - Std_Windows.svg Std_WindowCascade.svg Std_WindowNext.svg Std_WindowPrev.svg + Std_Windows.svg Std_WindowTileVer.svg - Std_DemoMode.svg - Std_DependencyGraph.svg - Std_DlgParameter.svg - Std_ProjectUtil.svg - Std_SceneInspector.svg - WhatsThis.svg - colors.svg - px.svg - AddonManager.svg - align-to-selection.svg - Group.svg - Geofeaturegroup.svg - Geoassembly.svg - Std_Point.svg - Std_Axis.svg - Std_Plane.svg - Link.svg - LinkArray.svg - LinkElement.svg - LinkGroup.svg - LinkOverlay.svg - LinkArrayOverlay.svg - LinkSubOverlay.svg - LinkSubElement.svg - LinkSub.svg - LinkReplace.svg - LinkImport.svg - LinkImportAll.svg - LinkSelect.svg - LinkSelectFinal.svg - LinkSelectAll.svg + style/windows_branch_closed.png + style/windows_branch_open.png + TextDocument.svg + Tree_Annotation.svg + Tree_Dimension.svg + Tree_Python.svg + tree-doc-collapse.svg + tree-doc-multi.svg + tree-doc-single.svg + tree-goto-sel.svg + tree-item-drag.svg + tree-pre-sel.svg + tree-rec-sel.svg + tree-sync-pla.svg + tree-sync-sel.svg + tree-sync-view.svg + TreeItemInvisible.svg + TreeItemVisible.svg + umf-measurement.svg Unlink.svg - Invisible.svg - folder.svg - document-python.svg - document-package.svg - cursor-through.svg - Std_Alignment.svg - Std_DuplicateSelection.svg - Std_UserEditModeDefault.svg - Std_UserEditModeTransform.svg - Std_UserEditModeCutting.svg - Std_UserEditModeColor.svg - Warning.svg - image-open.svg - image-plane.svg - image-scaling.svg - VarSet.svg - Std_ToggleFreeze.svg - 3dx_pivot.png - overlay_recompute.svg - overlay_error.svg - feature_suppressed.svg - forbidden.svg user-input/mouse-left.svg - user-input/mouse-right.svg - user-input/mouse-move.svg user-input/mouse-middle.svg - user-input/mouse-scroll.svg - user-input/mouse-scroll-up.svg + user-input/mouse-move.svg + user-input/mouse-right.svg user-input/mouse-scroll-down.svg + user-input/mouse-scroll-up.svg + user-input/mouse-scroll.svg + user.svg + utilities-terminal.svg + VarSet.svg + vertex-selection.svg + view-axonometric.svg + view-bottom.svg + view-front.svg + view-fullscreen.svg + view-isometric.svg + view-left.svg + view-measurement-cross.svg + view-measurement.svg + view-perspective.svg + view-rear.svg + view-refresh.svg + view-right.svg + view-rotate-left.svg + view-rotate-right.svg + view-select.svg + view-top.svg + view-unselectable.svg + Warning.svg + WhatsThis.svg + window-new.svg + zoom-all.svg + zoom-border-cross.svg + zoom-border.svg + zoom-fit-best.svg + zoom-in.svg + zoom-out.svg + zoom-selection.svg index.theme diff --git a/src/Gui/MDIView.h b/src/Gui/MDIView.h index f56a04578c..64c92ee4d8 100644 --- a/src/Gui/MDIView.h +++ b/src/Gui/MDIView.h @@ -147,6 +147,11 @@ public: return ActiveObjects.hasObject(o,n,subname); } + App::DocumentObject* getActiveObjectWithExtension(const Base::Type extensionTypeId) const + { + return ActiveObjects.getObjectWithExtension(extensionTypeId); + } + /*! * \brief containsViewProvider * Checks if the given view provider is part of this view. The default implementation diff --git a/src/Gui/PythonWrapper.cpp b/src/Gui/PythonWrapper.cpp index 1bda2cf2d1..e18d9b1d84 100644 --- a/src/Gui/PythonWrapper.cpp +++ b/src/Gui/PythonWrapper.cpp @@ -392,34 +392,39 @@ public: */ void addQObject(QObject* obj, PyObject* pyobj) { - const auto PyW_unique_name = QString::number(reinterpret_cast (pyobj)); - auto PyW_invalidator = findChild (PyW_unique_name, Qt::FindDirectChildrenOnly); + // static array to contain created connections so they can be safely disconnected later + static std::map connections = {}; + + const auto PyW_uniqueName = QString::number(reinterpret_cast(pyobj)); + auto PyW_invalidator = findChild(PyW_uniqueName, Qt::FindDirectChildrenOnly); if (PyW_invalidator == nullptr) { PyW_invalidator = new QObject(this); - PyW_invalidator->setObjectName(PyW_unique_name); + PyW_invalidator->setObjectName(PyW_uniqueName); Py_INCREF (pyobj); } - else { - PyW_invalidator->disconnect(); + else if (connections.contains(PyW_invalidator)) { + disconnect(connections[PyW_invalidator]); + connections.erase(PyW_invalidator); } - auto destroyedFun = [pyobj](){ + auto destroyedFun = [pyobj]() { Base::PyGILStateLocker lock; - auto sbk_ptr = reinterpret_cast (pyobj); - if (sbk_ptr != nullptr) { - Shiboken::Object::setValidCpp(sbk_ptr, false); + + if (auto sbkPtr = reinterpret_cast(pyobj); sbkPtr != nullptr) { + Shiboken::Object::setValidCpp(sbkPtr, false); } else { Base::Console().developerError("WrapperManager", "A QObject has just been destroyed after its Pythonic wrapper.\n"); } + Py_DECREF (pyobj); }; - QObject::connect(PyW_invalidator, &QObject::destroyed, this, destroyedFun); - QObject::connect(obj, &QObject::destroyed, PyW_invalidator, &QObject::deleteLater); -} + connections[PyW_invalidator] = connect(PyW_invalidator, &QObject::destroyed, this, destroyedFun); + connect(obj, &QObject::destroyed, PyW_invalidator, &QObject::deleteLater); + } private: void wrapQApplication() diff --git a/src/Gui/QuantitySpinBox.cpp b/src/Gui/QuantitySpinBox.cpp index 333f89d9b8..98749a06b0 100644 --- a/src/Gui/QuantitySpinBox.cpp +++ b/src/Gui/QuantitySpinBox.cpp @@ -572,10 +572,17 @@ void QuantitySpinBox::userInput(const QString & text) else { d->validInput = false; - // we have to emit here signal explicitly as validator will not pass - // this value further but we want to check it to disable isSet flag if - // it has been set previously - Q_EMIT valueChanged(d->quantity.getValue()); + // only emit signal to reset EditableDatumLabel if the input is truly empty or has + // no meaningful number don't emit for partially typed numbers like "71." which are + // temporarily invalid + const QString trimmedText = text.trimmed(); + static const QRegularExpression partialNumberRegex(QStringLiteral(R"([+-]?(\d+)?(\.,\d*)?)")); + if (trimmedText.isEmpty() || !trimmedText.contains(partialNumberRegex)) { + // we have to emit here signal explicitly as validator will not pass + // this value further but we want to check it to disable isSet flag if + // it has been set previously + Q_EMIT valueChanged(d->quantity.getValue()); + } return; } diff --git a/src/Gui/SoDatumLabel.cpp b/src/Gui/SoDatumLabel.cpp index d80be8fb71..b946d96b1a 100644 --- a/src/Gui/SoDatumLabel.cpp +++ b/src/Gui/SoDatumLabel.cpp @@ -134,6 +134,7 @@ SoDatumLabel::SoDatumLabel() SO_NODE_ADD_FIELD(name, ("Helvetica")); SO_NODE_ADD_FIELD(size, (10.F)); SO_NODE_ADD_FIELD(lineWidth, (2.F)); + SO_NODE_ADD_FIELD(sampling, (2.F)); SO_NODE_ADD_FIELD(datumtype, (SoDatumLabel::DISTANCE)); @@ -187,7 +188,8 @@ void SoDatumLabel::drawImage() QColor front; front.setRgbF(t[0],t[1], t[2]); - QImage image(w, h,QImage::Format_ARGB32_Premultiplied); + QImage image(w * sampling.getValue(), h * sampling.getValue(), QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(sampling.getValue()); image.fill(0x00000000); QPainter painter(&image); @@ -1165,7 +1167,7 @@ void SoDatumLabel::getDimension(float scale, int& srcw, int& srch) srch = imgsize[1]; float aspectRatio = (float) srcw / (float) srch; - this->imgHeight = scale * (float) (srch); + this->imgHeight = scale * (float) (srch) / sampling.getValue(); this->imgWidth = aspectRatio * (float) this->imgHeight; } diff --git a/src/Gui/SoDatumLabel.h b/src/Gui/SoDatumLabel.h index 70aa8c8b96..70dcc7fa90 100644 --- a/src/Gui/SoDatumLabel.h +++ b/src/Gui/SoDatumLabel.h @@ -85,6 +85,7 @@ public: SoSFVec3f norm; SoSFImage image; SoSFFloat lineWidth; + SoSFFloat sampling; bool useAntialiasing; protected: diff --git a/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss b/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss index bb885553b2..b093be348c 100644 --- a/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss +++ b/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss b/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss index 6cbdfe9743..de4b60cdee 100644 --- a/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss +++ b/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss b/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss index e73a7334ff..84e5a2af91 100644 --- a/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss +++ b/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss b/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss index a1367a3bcc..32c5503272 100644 --- a/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss +++ b/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/TaskView/TaskView.cpp b/src/Gui/TaskView/TaskView.cpp index 37cee0a18d..8efef9d6c0 100644 --- a/src/Gui/TaskView/TaskView.cpp +++ b/src/Gui/TaskView/TaskView.cpp @@ -32,6 +32,7 @@ # include # include # include +# include #endif #include @@ -268,9 +269,15 @@ QSize TaskPanel::minimumSizeHint() const //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TaskView::TaskView(QWidget *parent) - : QScrollArea(parent),ActiveDialog(nullptr),ActiveCtrl(nullptr) + : QWidget(parent),ActiveDialog(nullptr),ActiveCtrl(nullptr) { - taskPanel = new TaskPanel(this); + mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + this->setLayout(mainLayout); + scrollArea = new QScrollArea(this); + + taskPanel = new TaskPanel(scrollArea); QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); @@ -278,10 +285,11 @@ TaskView::TaskView(QWidget *parent) taskPanel->setSizePolicy(sizePolicy); taskPanel->setScheme(QSint::ActionPanelScheme::defaultScheme()); - this->setWidget(taskPanel); - setWidgetResizable(true); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - this->setMinimumWidth(200); + scrollArea->setWidget(taskPanel); + scrollArea->setWidgetResizable(true); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setMinimumWidth(200); + mainLayout->addWidget(scrollArea, 1); Gui::Selection().Attach(this); @@ -361,7 +369,7 @@ bool TaskView::event(QEvent* event) } } } - return QScrollArea::event(event); + return QWidget::event(event); } void TaskView::keyPressEvent(QKeyEvent* ke) @@ -423,7 +431,7 @@ void TaskView::keyPressEvent(QKeyEvent* ke) } } else { - QScrollArea::keyPressEvent(ke); + QWidget::keyPressEvent(ke); } } @@ -441,7 +449,7 @@ void TaskView::adjustMinimumSizeHint() QSize TaskView::minimumSizeHint() const { - QSize ms = QScrollArea::minimumSizeHint(); + QSize ms = QWidget::minimumSizeHint(); int spacing = 0; if (QLayout* layout = taskPanel->layout()) { spacing = 2 * layout->spacing(); @@ -592,7 +600,8 @@ void TaskView::showDialog(TaskDialog *dlg) dlg->modifyStandardButtons(ActiveCtrl->buttonBox); if (dlg->buttonPosition() == TaskDialog::North) { - taskPanel->addWidget(ActiveCtrl); + // Add button box to the top of the main layout + mainLayout->insertWidget(0, ActiveCtrl); for (const auto & it : cont){ taskPanel->addWidget(it); } @@ -601,7 +610,8 @@ void TaskView::showDialog(TaskDialog *dlg) for (const auto & it : cont){ taskPanel->addWidget(it); } - taskPanel->addWidget(ActiveCtrl); + // Add button box to the bottom of the main layout + mainLayout->addWidget(ActiveCtrl); } taskPanel->setScheme(QSint::ActionPanelScheme::defaultScheme()); @@ -627,7 +637,7 @@ void TaskView::removeDialog() getMainWindow()->updateActions(); if (ActiveCtrl) { - taskPanel->removeWidget(ActiveCtrl); + mainLayout->removeWidget(ActiveCtrl); delete ActiveCtrl; ActiveCtrl = nullptr; } diff --git a/src/Gui/TaskView/TaskView.h b/src/Gui/TaskView/TaskView.h index bf4537f1b7..92fada916f 100644 --- a/src/Gui/TaskView/TaskView.h +++ b/src/Gui/TaskView/TaskView.h @@ -138,7 +138,7 @@ public: * This elements get injected mostly by the ViewProvider classes of the selected * DocumentObjects. */ -class GuiExport TaskView : public QScrollArea, public Gui::SelectionSingleton::ObserverType +class GuiExport TaskView : public QWidget, public Gui::SelectionSingleton::ObserverType { Q_OBJECT @@ -188,6 +188,8 @@ private: void slotUndoDocument(const App::Document&); void slotRedoDocument(const App::Document&); void transactionChangeOnDocument(const App::Document&, bool undo); + QVBoxLayout* mainLayout; + QScrollArea* scrollArea; protected: void keyPressEvent(QKeyEvent* event) override; diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index 314bf1a934..919c2ffaa9 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -519,6 +519,7 @@ void TreeWidgetItemDelegate::initStyleOption(QStyleOptionViewItem *option, return; } + option->textElideMode = Qt::ElideMiddle; auto mousePos = option->widget->mapFromGlobal(QCursor::pos()); auto isHovered = option->rect.contains(mousePos); if (!isHovered) { diff --git a/src/Gui/ViewProviderDragger.cpp b/src/Gui/ViewProviderDragger.cpp index ad7ed97490..a78e890598 100644 --- a/src/Gui/ViewProviderDragger.cpp +++ b/src/Gui/ViewProviderDragger.cpp @@ -170,6 +170,20 @@ bool ViewProviderDragger::forwardToLink() return forwardedViewProvider != nullptr; } +App::PropertyPlacement* ViewProviderDragger::getPlacementProperty() const +{ + auto object = getObject(); + + if (auto linkExtension = object->getExtensionByType()) { + if (auto linkPlacementProp = linkExtension->getLinkPlacementProperty()) { + return linkPlacementProp; + } + + return linkExtension->getPlacementProperty(); + } + + return getObject()->getPropertyByName("Placement"); +} bool ViewProviderDragger::setEdit(int ModNum) { @@ -254,7 +268,7 @@ void ViewProviderDragger::dragMotionCallback(void* data, [[maybe_unused]] SoDrag void ViewProviderDragger::updatePlacementFromDragger(DraggerComponents components) { - const auto placement = getObject()->getPropertyByName("Placement"); + const auto placement = getPlacementProperty(); if (!placement) { return; @@ -379,7 +393,7 @@ void ViewProviderDragger::updateTransformFromDragger() Base::Placement ViewProviderDragger::getObjectPlacement() const { - if (auto placement = getObject()->getPropertyByName("Placement")) { + if (auto placement = getPlacementProperty()) { return placement->getValue(); } diff --git a/src/Gui/ViewProviderDragger.h b/src/Gui/ViewProviderDragger.h index ef1d89654c..6937dec79f 100644 --- a/src/Gui/ViewProviderDragger.h +++ b/src/Gui/ViewProviderDragger.h @@ -120,6 +120,9 @@ protected: bool forwardToLink(); + /// Gets placement property of the object + App::PropertyPlacement* getPlacementProperty() const; + /** * Returns a newly create dialog for the part to be placed in the task view * Must be reimplemented in subclasses. diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 6aecbb3b49..5acad2bc8f 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -718,24 +718,25 @@ MenuItem* StdWorkbench::setupMenuBar() const // Tools auto tool = new MenuItem( menuBar ); tool->setCommand("&Tools"); - *tool << "Std_DlgParameter" +#ifdef BUILD_ADDONMGR + *tool << "Std_AddonMgr" + << "Separator"; +#endif + *tool << "Std_Measure" + << "Std_UnitsCalculator" << "Separator" - << "Std_ViewScreenShot" << "Std_ViewLoadImage" + << "Std_ViewScreenShot" + << "Std_TextDocument" + << "Std_DemoMode" + << "Separator" << "Std_SceneInspector" << "Std_DependencyGraph" << "Std_ExportDependencyGraph" + << "Separator" << "Std_ProjectUtil" - << "Separator" - << "Std_TextDocument" - << "Separator" - << "Std_DemoMode" - << "Std_UnitsCalculator" - << "Separator" + << "Std_DlgParameter" << "Std_DlgCustomize"; -#ifdef BUILD_ADDONMGR - *tool << "Std_AddonMgr"; -#endif // Macro auto macro = new MenuItem( menuBar ); @@ -811,7 +812,7 @@ ToolBarItem* StdWorkbench::setupToolBars() const auto view = new ToolBarItem( root ); view->setCommand("View"); *view << "Std_ViewFitAll" << "Std_ViewFitSelection" << "Std_ViewGroup" << "Std_AlignToSelection" - << "Separator" << "Std_DrawStyle" << "Std_TreeViewActions"; + << "Separator" << "Std_DrawStyle" << "Std_TreeViewActions" << "Std_Measure"; // Individual views auto individualViews = new ToolBarItem(root, ToolBarItem::DefaultVisibility::Hidden); diff --git a/src/Gui/propertyeditor/PropertyItem.cpp b/src/Gui/propertyeditor/PropertyItem.cpp index 4bf7a42bb1..69e3c5b5eb 100644 --- a/src/Gui/propertyeditor/PropertyItem.cpp +++ b/src/Gui/propertyeditor/PropertyItem.cpp @@ -1376,7 +1376,7 @@ void PropertyBoolItem::setValue(const QVariant& value) } QWidget* PropertyBoolItem::createEditor(QWidget* parent, - const std::function& method, + const std::function& /*method*/, FrameOption /*frameOption*/) const { auto checkbox = new QCheckBox(parent); diff --git a/src/Mod/AddonManager b/src/Mod/AddonManager index 69a6e0dc7b..34d433a02c 160000 --- a/src/Mod/AddonManager +++ b/src/Mod/AddonManager @@ -1 +1 @@ -Subproject commit 69a6e0dc7b8f5fe17547f4d1234df1617b78c45e +Subproject commit 34d433a02c7ec5c73bec9c57d0a27ea70b36c90d diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index 29428a9c16..e4ba97910c 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -392,7 +392,7 @@ class CommandCreateJointGears: return { "Pixmap": "Assembly_CreateJointGears", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointGears", "Create Gears Joint"), - "Accel": "X", + "Accel": "T", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointGears", @@ -423,7 +423,7 @@ class CommandCreateJointBelt: return { "Pixmap": "Assembly_CreateJointPulleys", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointBelt", "Create Belt Joint"), - "Accel": "P", + "Accel": "L", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointBelt", diff --git a/src/Mod/Assembly/CommandCreateSimulation.py b/src/Mod/Assembly/CommandCreateSimulation.py index 1612069adf..56dc2268e2 100644 --- a/src/Mod/Assembly/CommandCreateSimulation.py +++ b/src/Mod/Assembly/CommandCreateSimulation.py @@ -65,7 +65,7 @@ class CommandCreateSimulation: return { "Pixmap": "Assembly_CreateSimulation", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateSimulation", "Create Simulation"), - "Accel": "S", + "Accel": "V", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateSimulation", diff --git a/src/Mod/BIM/ArchAxis.py b/src/Mod/BIM/ArchAxis.py index bdbd89482c..f39afa3a83 100644 --- a/src/Mod/BIM/ArchAxis.py +++ b/src/Mod/BIM/ArchAxis.py @@ -109,25 +109,20 @@ class _Axis: if distances and obj.Length.Value: if angles and len(distances) == len(angles): for i in range(len(distances)): - if hasattr(obj.Length,"Value"): - l = obj.Length.Value - else: - l = obj.Length dist += distances[i] ang = math.radians(angles[i]) - p1 = Vector(dist,0,0) - p2 = Vector(dist+(l/math.cos(ang))*math.sin(ang),l,0) + ln = obj.Length.Value + ln = 100 * ln if abs(math.cos(ang)) < 0.01 else ln / math.cos(ang) + unitvec = Vector(math.sin(ang), math.cos(ang), 0) + p1 = Vector(dist, 0, 0) + p2 = p1 + unitvec * ln if hasattr(obj,"Limit") and obj.Limit.Value: - p3 = p2.sub(p1) - p3.normalize() - p3.multiply(-obj.Limit.Value) - p4 = p1.sub(p2) - p4.normalize() - p4.multiply(-obj.Limit.Value) - geoms.append(Part.LineSegment(p1,p1.add(p4)).toShape()) - geoms.append(Part.LineSegment(p2,p2.add(p3)).toShape()) + p3 = unitvec * obj.Limit.Value + p4 = unitvec * -obj.Limit.Value + geoms.append(Part.LineSegment(p1, p1 + p3).toShape()) + geoms.append(Part.LineSegment(p2, p2 + p4).toShape()) else: - geoms.append(Part.LineSegment(p1,p2).toShape()) + geoms.append(Part.LineSegment(p1, p2).toShape()) if geoms: sh = Part.Compound(geoms) obj.Shape = sh diff --git a/src/Mod/BIM/ArchComponent.py b/src/Mod/BIM/ArchComponent.py index 62f0ec7355..9c403a3606 100644 --- a/src/Mod/BIM/ArchComponent.py +++ b/src/Mod/BIM/ArchComponent.py @@ -1353,19 +1353,16 @@ class ViewProviderComponent: #print(obj.Name," : updating ",prop) if prop == "Material": - if obj.Material and ( (not hasattr(obj.ViewObject,"UseMaterialColor")) or obj.ViewObject.UseMaterialColor): + if obj.Material and getattr(obj.ViewObject,"UseMaterialColor",True): if hasattr(obj.Material,"Material"): - if 'DiffuseColor' in obj.Material.Material: - if "(" in obj.Material.Material['DiffuseColor']: - c = tuple([float(f) for f in obj.Material.Material['DiffuseColor'].strip("()").split(",")]) - if obj.ViewObject: - if obj.ViewObject.ShapeColor != c: - obj.ViewObject.ShapeColor = c - if 'Transparency' in obj.Material.Material: - t = int(obj.Material.Material['Transparency']) - if obj.ViewObject: - if obj.ViewObject.Transparency != t: - obj.ViewObject.Transparency = t + if "DiffuseColor" in obj.Material.Material: + c = tuple([float(f) for f in obj.Material.Material["DiffuseColor"].strip("()").strip("[]").split(",")]) + if obj.ViewObject.ShapeColor != c: + obj.ViewObject.ShapeColor = c + if "Transparency" in obj.Material.Material: + t = int(obj.Material.Material["Transparency"]) + if obj.ViewObject.Transparency != t: + obj.ViewObject.Transparency = t elif prop == "Shape": if obj.Base: if obj.Base.isDerivedFrom("Part::Compound"): @@ -1375,11 +1372,7 @@ class ViewProviderComponent: obj.ViewObject.update() elif prop == "CloneOf": if obj.CloneOf: - mat = None - if hasattr(obj,"Material"): - if obj.Material: - mat = obj.Material - if (not mat) and hasattr(obj.CloneOf.ViewObject,"DiffuseColor"): + if (not getattr(obj,"Material",None)) and hasattr(obj.CloneOf.ViewObject,"DiffuseColor"): if obj.ViewObject.DiffuseColor != obj.CloneOf.ViewObject.DiffuseColor: if len(obj.CloneOf.ViewObject.DiffuseColor) > 1: obj.ViewObject.DiffuseColor = obj.CloneOf.ViewObject.DiffuseColor diff --git a/src/Mod/BIM/ArchCurtainWall.py b/src/Mod/BIM/ArchCurtainWall.py index 9246689545..fa394454b5 100644 --- a/src/Mod/BIM/ArchCurtainWall.py +++ b/src/Mod/BIM/ArchCurtainWall.py @@ -279,16 +279,52 @@ class CurtainWall(ArchComponent.Component): if not vdir.Length: vdir = FreeCAD.Vector(0,0,1) vdir.normalize() - basevector = face.valueAt(fp[1],fp[3]).sub(face.valueAt(fp[0],fp[2])) - a = basevector.getAngle(vdir) - if (a <= math.pi/2+ANGLETOLERANCE) and (a >= math.pi/2-ANGLETOLERANCE): - facedir = True - vertsec = obj.VerticalSections - horizsec = obj.HorizontalSections - else: - facedir = False - vertsec = obj.HorizontalSections - horizsec = obj.VerticalSections + + # Check if face if vertical in the first place + # Fix issue in 'Curtain wall vertical/horizontal mullion mix-up' + # https://github.com/FreeCAD/FreeCAD/issues/21845 + # + face_plane = face.findPlane() # Curve face (surface) seems return no Plane + if face_plane: + if -0.001 < face_plane.Axis[2] < 0.001: # i.e. face is vertical (normal pointing horizon) + faceVert = True + # Support 'Swap Horizontal Vertical' + # See issue 'Swap Horizontal Vertical does not work' + # https://github.com/FreeCAD/FreeCAD/issues/21866 + if obj.SwapHorizontalVertical: + vertsec = obj.HorizontalSections + horizsec = obj.VerticalSections + else: + vertsec = obj.VerticalSections + horizsec = obj.HorizontalSections + else: + faceVert = False + + # Guess algorithm if face is not vertical + if not faceVert: + # TODO 2025.6.15 : Need a more robust algorithm below + # See issue 'Curtain wall vertical/horizontal mullion mix-up' + # https://github.com/FreeCAD/FreeCAD/issues/21845 + # Partially improved by checking 'if face is vertical' above + # + basevector = face.valueAt(fp[1],fp[3]).sub(face.valueAt(fp[0],fp[2])) + bv_angle = basevector.getAngle(vdir) + if (bv_angle <= math.pi/2+ANGLETOLERANCE) and (bv_angle >= math.pi/2-ANGLETOLERANCE): + facedir = True + if obj.SwapHorizontalVertical: + vertsec = obj.HorizontalSections + horizsec = obj.VerticalSections + else: + vertsec = obj.VerticalSections + horizsec = obj.HorizontalSections + else: + facedir = False + if obj.SwapHorizontalVertical: + vertsec = obj.VerticalSections + horizsec = obj.HorizontalSections + else: + vertsec = obj.HorizontalSections + horizsec = obj.VerticalSections hstep = (fp[1]-fp[0]) if vertsec: diff --git a/src/Mod/BIM/ArchMaterial.py b/src/Mod/BIM/ArchMaterial.py index f6eaf4c053..6f589c7e2d 100644 --- a/src/Mod/BIM/ArchMaterial.py +++ b/src/Mod/BIM/ArchMaterial.py @@ -279,12 +279,18 @@ class _ArchMaterial: def execute(self,obj): if obj.Material: if FreeCAD.GuiUp: + c = None + t = None if "DiffuseColor" in obj.Material: - c = tuple([float(f) for f in obj.Material['DiffuseColor'].strip("()").strip("[]").split(",")]) - for p in obj.InList: - if hasattr(p,"Material") and ( (not hasattr(p.ViewObject,"UseMaterialColor")) or p.ViewObject.UseMaterialColor): - if p.Material.Name == obj.Name: - p.ViewObject.ShapeColor = c + c = tuple([float(f) for f in obj.Material["DiffuseColor"].strip("()").strip("[]").split(",")]) + if "Transparency" in obj.Material: + t = int(obj.Material["Transparency"]) + for p in obj.InList: + if hasattr(p,"Material") \ + and p.Material.Name == obj.Name \ + and getattr(obj.ViewObject,"UseMaterialColor",True): + if c: p.ViewObject.ShapeColor = c + if t: p.ViewObject.Transparency = t return def dumps(self): diff --git a/src/Mod/BIM/ArchProfile.py b/src/Mod/BIM/ArchProfile.py index 6a90217d4b..51b976797d 100644 --- a/src/Mod/BIM/ArchProfile.py +++ b/src/Mod/BIM/ArchProfile.py @@ -112,12 +112,12 @@ class _Profile(Draft._DraftObject): '''Remove all Profile properties''' - obj.removeProperty("Width") - obj.removeProperty("Height") - obj.removeProperty("WebThickness") - obj.removeProperty("FlangeThickness") - obj.removeProperty("OutDiameter") - obj.removeProperty("Thickness") + for prop in [ + "Width", "Height", "WebThickness", "FlangeThickness","OutDiameter", "Thickness" + ]: + if hasattr(obj, prop): + obj.setPropertyStatus(prop, "-LockDynamic") + obj.removeProperty(prop) class _ProfileC(_Profile): @@ -384,7 +384,7 @@ class ProfileTaskPanel: layout.addWidget(self.comboCategory) self.comboProfile = QtGui.QComboBox(self.form) layout.addWidget(self.comboProfile) - QtCore.QObject.connect(self.comboCategory, QtCore.SIGNAL("currentIndexChanged(QString)"), self.changeCategory) + QtCore.QObject.connect(self.comboCategory, QtCore.SIGNAL("currentTextChanged(QString)"), self.changeCategory) QtCore.QObject.connect(self.comboProfile, QtCore.SIGNAL("currentIndexChanged(int)"), self.changeProfile) # Read preset profiles and add relevant ones self.categories = [] diff --git a/src/Mod/BIM/ArchRoof.py b/src/Mod/BIM/ArchRoof.py index 6fb18227b4..4c323e24fb 100644 --- a/src/Mod/BIM/ArchRoof.py +++ b/src/Mod/BIM/ArchRoof.py @@ -762,17 +762,29 @@ class _Roof(ArchComponent.Component): # a Wall, but all portion of the wall above the roof solid would be # subtracted as well. # - # FC forum discussion : Sketch based Arch_Roof and wall substraction + + # FC forum discussion, 2024.1.15 : + # Sketch based Arch_Roof and wall substraction # - https://forum.freecad.org/viewtopic.php?t=84389 # + # Github issue #21633, 2025.5.29 : + # BIM: Holes in roof are causing troubles + # - https://github.com/FreeCAD/FreeCAD/issues/21633#issuecomment-2969640142 + + faces = [] solids = [] for f in obj.Base.Shape.Faces: # obj.Base.Shape.Solids.Faces p = f.findPlane() # Curve face (surface) seems return no Plane if p: - if p.Axis[2] < -1e-7: # i.e. normal pointing below horizon - faces.append(f) + # See github issue #21633, all planes are added for safety + #if p.Axis[2] < -1e-7: # i.e. normal pointing below horizon + faces.append(f) else: + # TODO 2025.6.15: See github issue #21633: Find better way + # to test and maybe to split suface point up and down + # and extrude separately + # Not sure if it is pointing towards and/or above horizon # (upward or downward), or it is curve surface, just add. faces.append(f) @@ -785,6 +797,11 @@ class _Roof(ArchComponent.Component): solid = f.extrude(Vector(0.0, 0.0, 1000000.0)) if not solid.isNull() and solid.isValid() and solid.Volume > 1e-3: solids.append(solid) + + # See github issue #21633: Solids are added for safety + for s in obj.Base.Shape.Solids: + solids.append(s) + compound = Part.Compound(solids) compound.Placement = obj.Placement return compound diff --git a/src/Mod/BIM/ArchSchedule.py b/src/Mod/BIM/ArchSchedule.py index d4d28ff894..305b4c73c6 100644 --- a/src/Mod/BIM/ArchSchedule.py +++ b/src/Mod/BIM/ArchSchedule.py @@ -799,9 +799,6 @@ class ArchScheduleTaskPanel: mw = FreeCADGui.getMainWindow() self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center()) - # maintain above FreeCAD window - self.form.setWindowFlags(self.form.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) - self.form.show() def add(self): diff --git a/src/Mod/BIM/ArchStructure.py b/src/Mod/BIM/ArchStructure.py index 15ee8f4bb9..f0844d6bf9 100644 --- a/src/Mod/BIM/ArchStructure.py +++ b/src/Mod/BIM/ArchStructure.py @@ -424,7 +424,7 @@ class _CommandStructure: self.doc.recompute() # gui_utils.end_all_events() # Causes a crash on Linux. self.tracker.finalize() - if FreeCADGui.draftToolBar.continueCmd.isChecked(): + if FreeCADGui.draftToolBar.continueMode: self.Activated() def _createItemlist(self, baselist): diff --git a/src/Mod/BIM/InitGui.py b/src/Mod/BIM/InitGui.py index 517b939ae0..2a61aaa3f8 100644 --- a/src/Mod/BIM/InitGui.py +++ b/src/Mod/BIM/InitGui.py @@ -572,10 +572,12 @@ class BIMWorkbench(Workbench): {"insert": "BIM_Help", "menuItem": "Std_ReportBug", "after": ""}, {"insert": "BIM_Welcome", "menuItem": "Std_ReportBug", "after": ""}, ] - if not hasattr(Gui, "BIM_WBManipulator"): + reload = hasattr(Gui, "BIM_WBManipulator") # BIM WB has previously been loaded. + if not getattr(Gui, "BIM_WBManipulator", None): Gui.BIM_WBManipulator = BIM_WBManipulator() Gui.addWorkbenchManipulator(Gui.BIM_WBManipulator) - Gui.activeWorkbench().reloadActive() + if reload: + Gui.activeWorkbench().reloadActive() Log("BIM workbench activated\n") @@ -626,7 +628,7 @@ class BIMWorkbench(Workbench): # remove manipulator if hasattr(Gui, "BIM_WBManipulator"): Gui.removeWorkbenchManipulator(Gui.BIM_WBManipulator) - del Gui.BIM_WBManipulator + Gui.BIM_WBManipulator = None Gui.activeWorkbench().reloadActive() Log("BIM workbench deactivated\n") diff --git a/src/Mod/BIM/Resources/ui/dialogConvertType.ui b/src/Mod/BIM/Resources/ui/dialogConvertType.ui index 4466ceacff..d846f5f083 100644 --- a/src/Mod/BIM/Resources/ui/dialogConvertType.ui +++ b/src/Mod/BIM/Resources/ui/dialogConvertType.ui @@ -37,6 +37,13 @@ + + + + Do not ask again and use this setting + + + diff --git a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui index 45c21b4e4c..136249f741 100644 --- a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui +++ b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui @@ -306,6 +306,50 @@ + + + + New type + + + + + + When enabled, converting objects to IFC types will always keep the original object + + + Always keep original object when converting to type + + + ConvertTypeKeepOriginal + + + Mod/NativeIFC + + + + + + + When enabled, a dialog will be shown each time when converting objects to IFC types + + + Show dialog when converting to type + + + true + + + ConvertTypeAskAgain + + + Mod/NativeIFC + + + + + + diff --git a/src/Mod/BIM/bimcommands/BimClassification.py b/src/Mod/BIM/bimcommands/BimClassification.py index a858215d52..5c38462f6f 100644 --- a/src/Mod/BIM/bimcommands/BimClassification.py +++ b/src/Mod/BIM/bimcommands/BimClassification.py @@ -159,7 +159,10 @@ class BIM_Classification: self.form.treeClass.itemDoubleClicked.connect(self.apply) self.form.search.up.connect(self.onUpArrow) self.form.search.down.connect(self.onDownArrow) - self.form.onlyVisible.stateChanged.connect(self.onVisible) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.onVisible) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.onVisible) # center the dialog over FreeCAD window mw = FreeCADGui.getMainWindow() diff --git a/src/Mod/BIM/bimcommands/BimIfcElements.py b/src/Mod/BIM/bimcommands/BimIfcElements.py index ced59f2d63..736f488e82 100644 --- a/src/Mod/BIM/bimcommands/BimIfcElements.py +++ b/src/Mod/BIM/bimcommands/BimIfcElements.py @@ -95,7 +95,10 @@ class BIM_IfcElements: ) self.form.groupMode.currentIndexChanged.connect(self.update) self.form.tree.clicked.connect(self.onClickTree) - self.form.onlyVisible.stateChanged.connect(self.update) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.update) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.update) self.form.buttonBox.accepted.connect(self.accept) self.form.globalMode.currentIndexChanged.connect(self.onObjectTypeChanged) self.form.globalMaterial.currentIndexChanged.connect(self.onMaterialChanged) diff --git a/src/Mod/BIM/bimcommands/BimIfcProperties.py b/src/Mod/BIM/bimcommands/BimIfcProperties.py index fc99e827b8..d72d7cc9d4 100644 --- a/src/Mod/BIM/bimcommands/BimIfcProperties.py +++ b/src/Mod/BIM/bimcommands/BimIfcProperties.py @@ -139,10 +139,15 @@ class BIM_IfcProperties: # connect signals self.form.tree.selectionModel().selectionChanged.connect(self.updateProperties) self.form.groupMode.currentIndexChanged.connect(self.update) - self.form.onlyVisible.stateChanged.connect(self.onVisible) - self.form.onlySelected.stateChanged.connect(self.onSelected) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.update) + self.form.onlySelected.checkStateChanged.connect(self.onSelected) + self.form.onlyMatches.checkStateChanged.connect(self.update) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.update) + self.form.onlySelected.stateChanged.connect(self.onSelected) + self.form.onlyMatches.stateChanged.connect(self.update) self.form.buttonBox.accepted.connect(self.accept) - self.form.onlyMatches.stateChanged.connect(self.update) self.form.searchField.currentIndexChanged.connect(self.update) self.form.searchField.editTextChanged.connect(self.update) self.form.comboProperty.currentIndexChanged.connect(self.addProperty) diff --git a/src/Mod/BIM/bimcommands/BimIfcQuantities.py b/src/Mod/BIM/bimcommands/BimIfcQuantities.py index 7622eeca9b..f85f8b61ca 100644 --- a/src/Mod/BIM/bimcommands/BimIfcQuantities.py +++ b/src/Mod/BIM/bimcommands/BimIfcQuantities.py @@ -122,7 +122,10 @@ class BIM_IfcQuantities: self.qmodel.dataChanged.connect(self.setChecked) self.form.buttonBox.accepted.connect(self.accept) self.form.quantities.clicked.connect(self.onClickTree) - self.form.onlyVisible.stateChanged.connect(self.update) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.update) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.update) self.form.buttonRefresh.clicked.connect(self.update) self.form.buttonApply.clicked.connect(self.add_qto) diff --git a/src/Mod/BIM/bimcommands/BimPanel.py b/src/Mod/BIM/bimcommands/BimPanel.py index 8e907ca5f7..bd9d8dff3b 100644 --- a/src/Mod/BIM/bimcommands/BimPanel.py +++ b/src/Mod/BIM/bimcommands/BimPanel.py @@ -127,7 +127,15 @@ class Arch_Panel: FreeCADGui.doCommand('s.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1.00,0.00,0.00),90.00)') self.doc.commitTransaction() self.doc.recompute() - if FreeCADGui.draftToolBar.continueCmd.isChecked(): + from PySide import QtCore + QtCore.QTimer.singleShot(100, self.check_continueMode) + + + def check_continueMode(self): + + "checks if continueMode is true and restarts Panel" + + if FreeCADGui.draftToolBar.continueMode: self.Activated() def taskbox(self): diff --git a/src/Mod/BIM/bimcommands/BimProjectManager.py b/src/Mod/BIM/bimcommands/BimProjectManager.py index e9d0ab1d21..ebde0fd12b 100644 --- a/src/Mod/BIM/bimcommands/BimProjectManager.py +++ b/src/Mod/BIM/bimcommands/BimProjectManager.py @@ -469,10 +469,6 @@ class BIM_ProjectManager: + "\n" ) s += "groups=" + ";;".join(groups) + "\n" - - s += "levelsWP=" + str(int(self.form.levelsWP.isChecked())) + "\n" - s += "levelsAxis=" + str(int(self.form.levelsAxis.isChecked())) + "\n" - s += ( "addHumanFigure=" + str(int(self.form.addHumanFigure.isChecked())) @@ -563,10 +559,6 @@ class BIM_ProjectManager: groups = s[1].split(";;") self.form.groupsList.clear() self.form.groupsList.addItems(groups) - elif s[0] == "levelsWP": - self.form.levelsWP.setChecked(bool(int(s[1]))) - elif s[0] == "levelsAxis": - self.form.levelsAxis.setChecked(bool(int(s[1]))) elif s[0] == "addHumanFigure": self.form.addHumanFigure.setChecked(bool(int(s[1]))) diff --git a/src/Mod/BIM/bimcommands/BimTDPage.py b/src/Mod/BIM/bimcommands/BimTDPage.py index 492093b8dd..b756ebe298 100644 --- a/src/Mod/BIM/bimcommands/BimTDPage.py +++ b/src/Mod/BIM/bimcommands/BimTDPage.py @@ -102,6 +102,7 @@ class BIM_TDPage: page.Scale = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/BIM" ).GetFloat("DefaultPageScale", 0.01) + page.ViewObject.show() FreeCAD.ActiveDocument.recompute() diff --git a/src/Mod/BIM/bimcommands/BimWall.py b/src/Mod/BIM/bimcommands/BimWall.py index e94a1f38a8..08b3036dec 100644 --- a/src/Mod/BIM/bimcommands/BimWall.py +++ b/src/Mod/BIM/bimcommands/BimWall.py @@ -192,7 +192,7 @@ class Arch_Wall: self.doc.recompute() # gui_utils.end_all_events() # Causes a crash on Linux. self.tracker.finalize() - if FreeCADGui.draftToolBar.continueCmd.isChecked(): + if FreeCADGui.draftToolBar.continueMode: self.Activated() def addDefault(self): @@ -358,7 +358,10 @@ class Arch_Wall: inputHeight.valueChanged.connect(self.setHeight) comboAlignment.currentIndexChanged.connect(self.setAlign) inputOffset.valueChanged.connect(self.setOffset) - checkboxUseSketches.stateChanged.connect(self.setUseSketch) + if hasattr(checkboxUseSketches, "checkStateChanged"): # Qt version >= 6.7.0 + checkboxUseSketches.checkStateChanged.connect(self.setUseSketch) + else: # Qt version < 6.7.0 + checkboxUseSketches.stateChanged.connect(self.setUseSketch) comboWallPresets.currentIndexChanged.connect(self.setMat) # Define the workflow of the input fields: diff --git a/src/Mod/BIM/bimcommands/BimWindow.py b/src/Mod/BIM/bimcommands/BimWindow.py index 05ab3d0e00..7ae2e08769 100644 --- a/src/Mod/BIM/bimcommands/BimWindow.py +++ b/src/Mod/BIM/bimcommands/BimWindow.py @@ -315,7 +315,10 @@ class Arch_Window: include = QtGui.QCheckBox(translate("Arch","Auto include in host object")) include.setChecked(True) grid.addWidget(include,0,0,1,2) - include.stateChanged.connect(self.setInclude) + if hasattr(include, "checkStateChanged"): # Qt version >= 6.7.0 + include.checkStateChanged.connect(self.setInclude) + else: # Qt version < 6.7.0 + include.stateChanged.connect(self.setInclude) # sill height labels = QtGui.QLabel(translate("Arch","Sill height")) diff --git a/src/Mod/BIM/nativeifc/ifc_objects.py b/src/Mod/BIM/nativeifc/ifc_objects.py index 245d1e213f..3557c72ed7 100644 --- a/src/Mod/BIM/nativeifc/ifc_objects.py +++ b/src/Mod/BIM/nativeifc/ifc_objects.py @@ -65,6 +65,7 @@ class ifc_object: elif prop == "Schema": self.edit_schema(obj, obj.Schema) elif prop == "Type": + self.edit_type(obj) self.assign_classification(obj) elif prop == "Classification": self.edit_classification(obj) diff --git a/src/Mod/BIM/nativeifc/ifc_status.py b/src/Mod/BIM/nativeifc/ifc_status.py index 6c3264fe89..4d4dcbbf91 100644 --- a/src/Mod/BIM/nativeifc/ifc_status.py +++ b/src/Mod/BIM/nativeifc/ifc_status.py @@ -481,14 +481,12 @@ def filter_out(objs): nobjs.append(obj) elif obj.isDerivedFrom("Mesh::Feature"): nobjs.append(obj) - elif obj.isDerivedFrom("App::DocumentObjectGroup"): + elif Draft.is_group(obj): if filter_out(obj.Group): # only append groups that contain exportable objects nobjs.append(obj) else: print("DEBUG: Filtering out",obj.Label) - elif obj.isDerivedFrom("Mesh::Feature"): - nobjs.append(obj) elif obj.isDerivedFrom("App::Feature"): if Draft.get_type(obj) in ("Dimension","LinearDimension","Layer","Text","DraftText"): nobjs.append(obj) diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index 675c2a99a1..8bc16126b4 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -1598,7 +1598,7 @@ def get_orphan_elements(ifcfile): products = ifcfile.by_type("IfcProduct") products = [p for p in products if not p.Decomposes] - products = [p for p in products if not p.ContainedInStructure] + products = [p for p in products if not getattr(p, "ContainedInStructure", [])] products = [ p for p in products if not hasattr(p, "VoidsElements") or not p.VoidsElements ] diff --git a/src/Mod/BIM/nativeifc/ifc_types.py b/src/Mod/BIM/nativeifc/ifc_types.py index 8d78796ee8..7967c471a9 100644 --- a/src/Mod/BIM/nativeifc/ifc_types.py +++ b/src/Mod/BIM/nativeifc/ifc_types.py @@ -30,6 +30,9 @@ from . import ifc_tools translate = FreeCAD.Qt.translate +# Parameters object for NativeIFC preferences +PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC") + def show_type(obj): """Adds the types of that object as FreeCAD objects""" @@ -73,17 +76,36 @@ def convert_to_type(obj, keep_object=False): return if not getattr(obj, "Shape", None): return - if FreeCAD.GuiUp: + + # Check preferences + always_keep = PARAMS.GetBool("ConvertTypeKeepOriginal", False) + ask_again = PARAMS.GetBool("ConvertTypeAskAgain", True) + + if FreeCAD.GuiUp and ask_again: import FreeCADGui dlg = FreeCADGui.PySideUic.loadUi(":/ui/dialogConvertType.ui") - + original_text = dlg.label.text() dlg.label.setText(original_text.replace("%1", obj.Class+"Type")) + # Set the initial state of the checkbox from the "always keep" preference + dlg.checkKeepObject.setChecked(always_keep) + result = dlg.exec_() if not result: return + keep_object = dlg.checkKeepObject.isChecked() + do_not_ask_again = dlg.checkDoNotAskAgain.isChecked() + + # If "Do not ask again" is checked, disable future dialogs and save the current choice + if do_not_ask_again: + PARAMS.SetBool("ConvertTypeAskAgain", False) + PARAMS.SetBool("ConvertTypeKeepOriginal", keep_object) + else: + # Use the saved preference when GUI is not available or user chose "do not ask again" + keep_object = always_keep + element = ifc_tools.get_ifc_element(obj) ifcfile = ifc_tools.get_ifcfile(obj) project = ifc_tools.get_project(obj) diff --git a/src/Mod/BIM/nativeifc/ifc_viewproviders.py b/src/Mod/BIM/nativeifc/ifc_viewproviders.py index ee9d67b4b0..3f50e774fe 100644 --- a/src/Mod/BIM/nativeifc/ifc_viewproviders.py +++ b/src/Mod/BIM/nativeifc/ifc_viewproviders.py @@ -224,7 +224,7 @@ class ifc_vp_object: """Recursively gets the children only used by this object""" children = [] for child in obj.OutList: - if len(child.InList) == 1 and child.InList[1] == obj: + if len(child.InList) == 1 and child.InList[0] == obj: children.append(child) children.extend(self.getOwnChildren(child)) return children diff --git a/src/Mod/CAM/App/Area.cpp b/src/Mod/CAM/App/Area.cpp index de73697705..2165865944 100644 --- a/src/Mod/CAM/App/Area.cpp +++ b/src/Mod/CAM/App/Area.cpp @@ -1920,7 +1920,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR builder.MakeCompound(comp); for (TopExp_Explorer xp(s.shape.Moved(loc), TopAbs_SOLID); xp.More(); xp.Next()) { - showShape(xp.Current(), nullptr, "section_%u_shape", i); + showShape(xp.Current(), nullptr, "section_%ul_shape", i); std::list wires; Part::CrossSection section(a, b, c, xp.Current()); Part::FuzzyHelper::withBooleanFuzzy(.0, [&]() { @@ -1930,7 +1930,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR // here for now to be on the safe side. wires = section.slice(-d); }); - showShapes(wires, nullptr, "section_%u_wire", i); + showShapes(wires, nullptr, "section_%ul_wire", i); if (wires.empty()) { AREA_LOG("Section returns no wires"); continue; @@ -1951,7 +1951,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR AREA_WARN("FaceMakerBullseye return null shape on section"); } else { - showShape(shape, nullptr, "section_%u_face", i); + showShape(shape, nullptr, "section_%ul_face", i); for (auto it = wires.begin(), itNext = it; it != wires.end(); it = itNext) { ++itNext; @@ -1979,7 +1979,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR // Make sure the compound has at least one edge if (TopExp_Explorer(comp, TopAbs_EDGE).More()) { const TopoDS_Shape& shape = comp.Moved(locInverse); - showShape(shape, nullptr, "section_%u_result", i); + showShape(shape, nullptr, "section_%ul_result", i); area->add(shape, s.op); } else if (area->myShapes.empty()) { @@ -1994,7 +1994,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR if (!area->myShapes.empty()) { sections.push_back(area); FC_TIME_LOG(t1, "makeSection " << z); - showShape(area->getShape(), nullptr, "section_%u_final", i); + showShape(area->getShape(), nullptr, "section_%ul_final", i); break; } if (retried) { diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py index f3102bf6c1..dad32fe85c 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py @@ -27,7 +27,7 @@ from unittest.mock import MagicMock from Path.Tool.toolbit.ui.editor import ToolBitPropertiesWidget from Path.Tool.toolbit.models.base import ToolBit from Path.Tool.shape.ui.shapewidget import ShapeWidget -from Path.Tool.ui.property import BasePropertyEditorWidget +from Path.Tool.docobject.ui.property import BasePropertyEditorWidget from .PathTestUtils import PathTestWithAssets diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py index 1361115a6d..6b9c60207e 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py @@ -24,7 +24,8 @@ import unittest import FreeCAD -from Path.Tool.ui.property import ( +from Path.Tool.docobject import DetachedDocumentObject +from Path.Tool.docobject.ui.property import ( BasePropertyEditorWidget, QuantityPropertyEditorWidget, BoolPropertyEditorWidget, @@ -32,7 +33,6 @@ from Path.Tool.ui.property import ( EnumPropertyEditorWidget, LabelPropertyEditorWidget, ) -from Path.Tool.toolbit.docobject import DetachedDocumentObject class TestPropertyEditorFactory(unittest.TestCase): diff --git a/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py index 22c14625fb..0112c5d385 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py @@ -26,7 +26,9 @@ import unittest from unittest.mock import MagicMock import FreeCAD from PySide import QtGui -from Path.Tool.ui.property import ( +from Path.Tool.docobject import DetachedDocumentObject +from Path.Tool.docobject.ui.docobject import DocumentObjectEditorWidget, _get_label_text +from Path.Tool.docobject.ui.property import ( BasePropertyEditorWidget, QuantityPropertyEditorWidget, BoolPropertyEditorWidget, @@ -34,8 +36,6 @@ from Path.Tool.ui.property import ( EnumPropertyEditorWidget, LabelPropertyEditorWidget, ) -from Path.Tool.ui.docobject import DocumentObjectEditorWidget, _get_label_text -from Path.Tool.toolbit.docobject import DetachedDocumentObject class TestDocumentObjectEditorWidget(unittest.TestCase): diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py index d1a33d5554..e9cd07cb35 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py +++ b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py @@ -882,6 +882,52 @@ G54 ############################################################################# + def test00145(self) -> None: + """Test the finish label argument.""" + # test the default finish label + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--comments", + ) + + # test a changed finish label + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(End operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(End operation) +(Begin operation) +(End operation) +(Begin postamble) +""", + "--finish_label='End' --comments", + ) + + ############################################################################# + def test00150(self): """Test output with an empty path. @@ -1178,6 +1224,9 @@ G0 Z8.000 --feed-precision FEED_PRECISION Number of digits of precision for feed rate, default is 3 + --finish_label FINISH_LABEL + The characters to use in the 'Finish operation' + comment, default is "Finish" --header Output headers (default) --no-header Suppress header output --line_number_increment LINE_NUMBER_INCREMENT @@ -1198,6 +1247,16 @@ G0 Z8.000 Output all of the available arguments --no-output_all_arguments Don't output all of the available arguments (default) + --output_machine_name + Output the machine name in the pre-operation + information + --no-output_machine_name + Don't output the machine name in the pre-operation + information (default) + --output_path_labels Output Path labels at the beginning of each Path + --no-output_path_labels + Don't output Path labels at the beginning of each Path + (default) --output_visible_arguments Output all of the visible arguments --no-output_visible_arguments @@ -1205,6 +1264,9 @@ G0 Z8.000 --postamble POSTAMBLE Set commands to be issued after the last command, default is "" + --post_operation POST_OPERATION + Set commands to be issued after every operation, + default is "" --preamble PREAMBLE Set commands to be issued before the first command, default is "" --precision PRECISION @@ -1293,6 +1355,146 @@ G0 X2.000 ############################################################################# + def test00205(self) -> None: + """Test output_machine_name argument.""" + # test the default behavior + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--comments", + ) + + # test outputting the machine name + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +(Machine: test, mm/min) +G54 +(Finish operation) +(Begin operation) +(Machine: test, mm/min) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Machine: test, mm/min) +(Finish operation) +(Begin postamble) +""", + "--output_machine_name --comments", + ) + + # test not outputting the machine name + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--no-output_machine_name --comments", + ) + + ############################################################################# + + def test00206(self) -> None: + """Test output_path_labels argument.""" + # test the default behavior + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--comments", + ) + + # test outputting the path labels + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +(Path: Fixture) +G54 +(Finish operation) +(Begin operation) +(Path: TC: Default Tool) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Path: Profile) +(Finish operation) +(Begin postamble) +""", + "--output_path_labels --comments", + ) + + # test not outputting the path labels + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--no-output_path_labels --comments", + ) + + ############################################################################# + def test00210(self): """Test Post-amble.""" nl = "\n" @@ -1307,6 +1509,36 @@ G0 X2.000 ############################################################################# + def test00215(self) -> None: + """Test the post_operation argument.""" + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +G90 G80 +G40 G49 +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +G90 G80 +G40 G49 +(Begin operation) +(Finish operation) +G90 G80 +G40 G49 +(Begin postamble) +""", + "--comments --post_operation='G90 G80\nG40 G49'", + ) + + ############################################################################# + def test00220(self): """Test Pre-amble.""" nl = "\n" diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index d9eff68df5..6ba07c39af 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -142,20 +142,28 @@ SET(PathPythonToolsAssetsUi_SRCS Path/Tool/assets/ui/util.py ) +SET(PathPythonToolsDocObject_SRCS + Path/Tool/docobject/__init__.py +) + +SET(PathPythonToolsDocObjectModels_SRCS + Path/Tool/docobject/models/__init__.py + Path/Tool/docobject/models/docobject.py +) + +SET(PathPythonToolsDocObjectUi_SRCS + Path/Tool/docobject/ui/__init__.py + Path/Tool/docobject/ui/docobject.py + Path/Tool/docobject/ui/property.py +) + SET(PathPythonToolsGui_SRCS Path/Tool/Gui/__init__.py Path/Tool/Gui/Controller.py ) -SET(PathPythonToolsUi_SRCS - Path/Tool/ui/__init__.py - Path/Tool/ui/docobject.py - Path/Tool/ui/property.py -) - SET(PathPythonToolsToolBit_SRCS Path/Tool/toolbit/__init__.py - Path/Tool/toolbit/docobject.py Path/Tool/toolbit/util.py ) @@ -554,15 +562,8 @@ SET(PathImages_Ops Images/Ops/chamfer.svg ) -SET(PathImages_Tools - Images/Tools/drill.svg - Images/Tools/endmill.svg - Images/Tools/v-bit.svg -) - SET(Path_Images ${PathImages_Ops} - ${PathImages_Tools} ) SET(PathData_Threads @@ -598,8 +599,10 @@ SET(all_files ${PathPythonToolsAssets_SRCS} ${PathPythonToolsAssetsStore_SRCS} ${PathPythonToolsAssetsUi_SRCS} + ${PathPythonToolsDocObject_SRCS} + ${PathPythonToolsDocObjectModels_SRCS} + ${PathPythonToolsDocObjectUi_SRCS} ${PathPythonToolsGui_SRCS} - ${PathPythonToolsUi_SRCS} ${PathPythonToolsShape_SRCS} ${PathPythonToolsShapeModels_SRCS} ${PathPythonToolsShapeUi_SRCS} @@ -772,9 +775,23 @@ INSTALL( INSTALL( FILES - ${PathPythonToolsUi_SRCS} + ${PathPythonToolsDocObject_SRCS} DESTINATION - Mod/CAM/Path/Tool/ui + Mod/CAM/Path/Tool/docobject +) + +INSTALL( + FILES + ${PathPythonToolsDocObjectModels_SRCS} + DESTINATION + Mod/CAM/Path/Tool/docobject/models +) + +INSTALL( + FILES + ${PathPythonToolsDocObjectUi_SRCS} + DESTINATION + Mod/CAM/Path/Tool/docobject/ui ) INSTALL( @@ -939,13 +956,6 @@ INSTALL( Mod/CAM/Images/Ops ) -INSTALL( - FILES - ${PathImages_Tools} - DESTINATION - Mod/CAM/Images/Tools -) - INSTALL( FILES ${PathData_Threads} diff --git a/src/Mod/CAM/Images/Tools/drill.svg b/src/Mod/CAM/Images/Tools/drill.svg deleted file mode 100644 index 5e4a0f177b..0000000000 --- a/src/Mod/CAM/Images/Tools/drill.svg +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - H - D - α - - - - diff --git a/src/Mod/CAM/Images/Tools/endmill.svg b/src/Mod/CAM/Images/Tools/endmill.svg deleted file mode 100644 index 3982fe1c12..0000000000 --- a/src/Mod/CAM/Images/Tools/endmill.svg +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - S - D - H - - - - - - - diff --git a/src/Mod/CAM/Images/Tools/reamer.svg b/src/Mod/CAM/Images/Tools/reamer.svg deleted file mode 100644 index 737600c528..0000000000 --- a/src/Mod/CAM/Images/Tools/reamer.svg +++ /dev/null @@ -1,433 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - D - H - - - - - - - - diff --git a/src/Mod/CAM/Images/Tools/v-bit.svg b/src/Mod/CAM/Images/Tools/v-bit.svg deleted file mode 100644 index d1f4f22e25..0000000000 --- a/src/Mod/CAM/Images/Tools/v-bit.svg +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - S - D - - α - d - - - - - - - - - H - - diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py index d9f92c86b7..6f3b6d434f 100644 --- a/src/Mod/CAM/InitGui.py +++ b/src/Mod/CAM/InitGui.py @@ -22,11 +22,39 @@ # * * # *************************************************************************** import FreeCAD +from PySide.QtCore import QT_TRANSLATE_NOOP +import Path.Dressup.Gui.Preferences as PathPreferencesPathDressup +import Path.Tool.assets.ui.preferences as AssetPreferences +import Path.Main.Gui.PreferencesJob as PathPreferencesPathJob +import Path.Base.Gui.PreferencesAdvanced as PathPreferencesAdvanced +import Path.Op.Base +import Path.Tool FreeCAD.__unit_test__ += ["TestCAMGui"] +if FreeCAD.GuiUp: + import FreeCADGui + + FreeCADGui.addPreferencePage( + PathPreferencesPathJob.JobPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + FreeCADGui.addPreferencePage( + AssetPreferences.AssetPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + FreeCADGui.addPreferencePage( + PathPreferencesPathDressup.DressupPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + FreeCADGui.addPreferencePage( + PathPreferencesAdvanced.AdvancedPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + + class PathCommandGroup: def __init__(self, cmdlist, menu, tooltip=None): self.cmdlist = cmdlist @@ -66,13 +94,10 @@ class CAMWorkbench(Workbench): import Path.Tool.assets.ui.preferences as AssetPreferences import Path.Main.Gui.PreferencesJob as PathPreferencesPathJob - translate = FreeCAD.Qt.translate - # load the builtin modules import Path import PathScripts import PathGui - from PySide import QtCore, QtGui FreeCADGui.addLanguagePath(":/translations") FreeCADGui.addIconPath(":/icons") @@ -89,19 +114,6 @@ class CAMWorkbench(Workbench): import subprocess from packaging.version import Version, parse - FreeCADGui.addPreferencePage( - PathPreferencesPathJob.JobPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) - FreeCADGui.addPreferencePage( - AssetPreferences.AssetPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) - FreeCADGui.addPreferencePage( - PathPreferencesPathDressup.DressupPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) - Path.GuiInit.Startup() # build commands list @@ -126,6 +138,7 @@ class CAMWorkbench(Workbench): "CAM_MillFace", "CAM_Helix", "CAM_Adaptive", + "CAM_Slot", ] threedopcmdlist = ["CAM_Pocket3D"] engravecmdlist = ["CAM_Engrave", "CAM_Deburr", "CAM_Vcarve"] @@ -177,7 +190,6 @@ class CAMWorkbench(Workbench): prepcmdlist.append("CAM_PathShapeTC") extracmdlist.extend(["CAM_Area", "CAM_Area_Workplane"]) specialcmdlist.append("CAM_ThreadMilling") - twodopcmdlist.append("CAM_Slot") if Path.Preferences.advancedOCLFeaturesEnabled(): try: @@ -291,14 +303,6 @@ class CAMWorkbench(Workbench): if curveAccuracy: Path.Area.setDefaultParams(Accuracy=curveAccuracy) - # keep this one the last entry in the preferences - import Path.Base.Gui.PreferencesAdvanced as PathPreferencesAdvanced - from Path.Preferences import preferences - - FreeCADGui.addPreferencePage( - PathPreferencesAdvanced.AdvancedPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) Log("Loading CAM workbench... done\n") def GetClassName(self): @@ -314,8 +318,6 @@ class CAMWorkbench(Workbench): pass def ContextMenu(self, recipient): - import PathScripts - menuAppended = False if len(FreeCADGui.Selection.getSelection()) == 1: obj = FreeCADGui.Selection.getSelection()[0] diff --git a/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py b/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py index 1cab794e0a..cbeffc76e6 100644 --- a/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py +++ b/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py @@ -33,8 +33,12 @@ else: class AdvancedPreferencesPage: def __init__(self, parent=None): self.form = FreeCADGui.PySideUic.loadUi(":preferences/Advanced.ui") - self.form.WarningSuppressAllSpeeds.stateChanged.connect(self.updateSelection) - self.form.EnableAdvancedOCLFeatures.stateChanged.connect(self.updateSelection) + if hasattr(self.form.WarningSuppressAllSpeeds, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.WarningSuppressAllSpeeds.checkStateChanged.connect(self.updateSelection) + self.form.EnableAdvancedOCLFeatures.checkStateChanged.connect(self.updateSelection) + else: # Qt version < 6.7.0 + self.form.WarningSuppressAllSpeeds.stateChanged.connect(self.updateSelection) + self.form.EnableAdvancedOCLFeatures.stateChanged.connect(self.updateSelection) def saveSettings(self): Path.Preferences.setPreferencesAdvanced( diff --git a/src/Mod/CAM/Path/Dressup/Gui/Boundary.py b/src/Mod/CAM/Path/Dressup/Gui/Boundary.py index 56482c31b3..dc2eac675d 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/Boundary.py +++ b/src/Mod/CAM/Path/Dressup/Gui/Boundary.py @@ -182,7 +182,10 @@ class TaskPanel(object): self.form.stockInside.setChecked(self.obj.Inside) self.form.stock.currentIndexChanged.connect(self.updateStockEditor) - self.form.stockInside.stateChanged.connect(self.setDirty) + if hasattr(self.form.stockInside, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.stockInside.checkStateChanged.connect(self.setDirty) + else: # Qt version < 6.7.0 + self.form.stockInside.stateChanged.connect(self.setDirty) self.form.stockExtXneg.textChanged.connect(self.setDirty) self.form.stockExtXpos.textChanged.connect(self.setDirty) self.form.stockExtYneg.textChanged.connect(self.setDirty) diff --git a/src/Mod/CAM/Path/Main/Gui/Job.py b/src/Mod/CAM/Path/Main/Gui/Job.py index 6e1c838af5..1efc2c14cd 100644 --- a/src/Mod/CAM/Path/Main/Gui/Job.py +++ b/src/Mod/CAM/Path/Main/Gui/Job.py @@ -150,6 +150,15 @@ class ViewProvider: def onChanged(self, vobj, prop): if prop == "Visibility": self.showOriginAxis(vobj.Visibility) + + # if we're currently restoring the document we do NOT want to call + # hideXXX as this would mark all currently hidden children as + # explicitly hidden by the user and prevent showing them when + # showing the job + + if self.obj.Document.Restoring: + return + if vobj.Visibility: self.restoreOperationsVisibility() self.restoreModelsVisibility() @@ -170,7 +179,8 @@ class ViewProvider: def restoreOperationsVisibility(self): if hasattr(self, "operationsVisibility"): for op in self.obj.Operations.Group: - op.Visibility = self.operationsVisibility[op.Name] + if self.operationsVisibility.get(op.Name, True): + op.Visibility = True else: for op in self.obj.Operations.Group: op.Visibility = True @@ -183,11 +193,12 @@ class ViewProvider: def restoreModelsVisibility(self): if hasattr(self, "modelsVisibility"): - for base in self.obj.Model.Group: - base.Visibility = self.modelsVisibility[base.Name] + for model in self.obj.Model.Group: + if self.modelsVisibility.get(model.Name, True): + model.Visibility = True else: - for base in self.obj.Model.Group: - base.Visibility = True + for model in self.obj.Model.Group: + model.Visibility = True def hideStock(self): self.stockVisibility = self.obj.Stock.Visibility @@ -195,7 +206,8 @@ class ViewProvider: def restoreStockVisibility(self): if hasattr(self, "stockVisibility"): - self.obj.Stock.Visibility = self.stockVisibility + if self.stockVisibility: + self.obj.Stock.Visibility = True def hideTools(self): self.toolsVisibility = {} @@ -206,7 +218,8 @@ class ViewProvider: def restoreToolsVisibility(self): if hasattr(self, "toolsVisibility"): for tc in self.obj.Tools.Group: - tc.Tool.Visibility = self.toolsVisibility[tc.Tool.Name] + if self.toolsVisibility.get(tc.Tool.Name, True): + tc.Tool.Visibility = True def showOriginAxis(self, yes): sw = coin.SO_SWITCH_ALL if yes else coin.SO_SWITCH_NONE diff --git a/src/Mod/CAM/Path/Op/Custom.py b/src/Mod/CAM/Path/Op/Custom.py index 5b409c5153..f4a217bc76 100644 --- a/src/Mod/CAM/Path/Op/Custom.py +++ b/src/Mod/CAM/Path/Op/Custom.py @@ -163,11 +163,26 @@ class ObjectCustom(PathOp.ObjectOp): def opExecute(self, obj): self.commandlist.append(Path.Command("(Begin Custom)")) + errorNumLines = [] + errorLines = [] + counter = 0 if obj.Source == "Text" and obj.Gcode: for l in obj.Gcode: - newcommand = Path.Command(str(l)) - self.commandlist.append(newcommand) + counter += 1 + try: + newcommand = Path.Command(str(l)) + self.commandlist.append(newcommand) + except ValueError: + errorNumLines.append(counter) + if len(errorLines) < 7: + errorLines.append(f"{counter}: {str(l).strip()}") + if errorLines: + Path.Log.warning( + translate("PathCustom", "Total invalid lines in Custom Text G-code: %s") + % len(errorNumLines) + ) + elif obj.Source == "File" and len(obj.GcodeFile) > 0: gcode_file = self.findGcodeFile(obj.GcodeFile) @@ -176,15 +191,33 @@ class ObjectCustom(PathOp.ObjectOp): Path.Log.error( translate("PathCustom", "Custom file %s could not be found.") % obj.GcodeFile ) + else: + with open(gcode_file) as fd: + for l in fd.readlines(): + counter += 1 + try: + newcommand = Path.Command(str(l)) + self.commandlist.append(newcommand) + except ValueError: + errorNumLines.append(counter) + if len(errorLines) < 7: + errorLines.append(f"{counter}: {str(l).strip()}") + if errorLines: + Path.Log.warning(f'"{gcode_file}"') + Path.Log.warning( + translate("PathCustom", "Total invalid lines in Custom File G-code: %s") + % len(errorNumLines) + ) - with open(gcode_file) as fd: - for l in fd.readlines(): - try: - newcommand = Path.Command(str(l)) - self.commandlist.append(newcommand) - except ValueError: - Path.Log.warning(translate("PathCustom", "Invalid G-code line: %s") % l) - continue + if errorNumLines: + Path.Log.warning( + translate("PathCustom", "Please check lines: %s") + % ", ".join(map(str, errorNumLines)) + ) + + if len(errorLines) > 7: + errorLines.append("...") + Path.Log.warning("\n" + "\n".join(errorLines)) self.commandlist.append(Path.Command("(End Custom)")) diff --git a/src/Mod/CAM/Path/Op/Gui/Adaptive.py b/src/Mod/CAM/Path/Op/Gui/Adaptive.py index c2438dd21c..d16b6ace33 100644 --- a/src/Mod/CAM/Path/Op/Gui/Adaptive.py +++ b/src/Mod/CAM/Path/Op/Gui/Adaptive.py @@ -68,10 +68,16 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.StockToLeave.valueChanged) signals.append(self.form.ZStockToLeave.valueChanged) signals.append(self.form.coolantController.currentIndexChanged) - signals.append(self.form.ForceInsideOut.stateChanged) - signals.append(self.form.FinishingProfile.stateChanged) - signals.append(self.form.useOutline.stateChanged) - signals.append(self.form.orderCutsByRegion.stateChanged) + if hasattr(self.form.ForceInsideOut, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.ForceInsideOut.checkStateChanged) + signals.append(self.form.FinishingProfile.checkStateChanged) + signals.append(self.form.useOutline.checkStateChanged) + signals.append(self.form.orderCutsByRegion.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.ForceInsideOut.stateChanged) + signals.append(self.form.FinishingProfile.stateChanged) + signals.append(self.form.useOutline.stateChanged) + signals.append(self.form.orderCutsByRegion.stateChanged) signals.append(self.form.StopButton.toggled) return signals diff --git a/src/Mod/CAM/Path/Op/Gui/Drilling.py b/src/Mod/CAM/Path/Op/Gui/Drilling.py index f39afee560..5d14580411 100644 --- a/src/Mod/CAM/Path/Op/Gui/Drilling.py +++ b/src/Mod/CAM/Path/Op/Gui/Drilling.py @@ -188,15 +188,23 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): signals.append(self.form.peckRetractHeight.editingFinished) signals.append(self.form.peckDepth.editingFinished) signals.append(self.form.dwellTime.editingFinished) - signals.append(self.form.dwellEnabled.stateChanged) - signals.append(self.form.peckEnabled.stateChanged) - signals.append(self.form.chipBreakEnabled.stateChanged) + if hasattr(self.form.dwellEnabled, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.dwellEnabled.checkStateChanged) + signals.append(self.form.peckEnabled.checkStateChanged) + signals.append(self.form.chipBreakEnabled.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.dwellEnabled.stateChanged) + signals.append(self.form.peckEnabled.stateChanged) + signals.append(self.form.chipBreakEnabled.stateChanged) signals.append(self.form.toolController.currentIndexChanged) signals.append(self.form.coolantController.currentIndexChanged) signals.append(self.form.ExtraOffset.currentIndexChanged) - signals.append(self.form.KeepToolDownEnabled.stateChanged) - signals.append(self.form.feedRetractEnabled.stateChanged) - + if hasattr(self.form.KeepToolDownEnabled, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.KeepToolDownEnabled.checkStateChanged) + signals.append(self.form.feedRetractEnabled.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.KeepToolDownEnabled.stateChanged) + signals.append(self.form.feedRetractEnabled.stateChanged) return signals def updateData(self, obj, prop): diff --git a/src/Mod/CAM/Path/Op/Gui/Profile.py b/src/Mod/CAM/Path/Op/Gui/Profile.py index f7aa0aff22..03146a4356 100644 --- a/src/Mod/CAM/Path/Op/Gui/Profile.py +++ b/src/Mod/CAM/Path/Op/Gui/Profile.py @@ -125,11 +125,18 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.extraOffset.editingFinished) signals.append(self.form.numPasses.editingFinished) signals.append(self.form.stepover.editingFinished) - signals.append(self.form.useCompensation.stateChanged) - signals.append(self.form.useStartPoint.stateChanged) - signals.append(self.form.processHoles.stateChanged) - signals.append(self.form.processPerimeter.stateChanged) - signals.append(self.form.processCircles.stateChanged) + if hasattr(self.form.useCompensation, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.useCompensation.checkStateChanged) + signals.append(self.form.useStartPoint.checkStateChanged) + signals.append(self.form.processHoles.checkStateChanged) + signals.append(self.form.processPerimeter.checkStateChanged) + signals.append(self.form.processCircles.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.useCompensation.stateChanged) + signals.append(self.form.useStartPoint.stateChanged) + signals.append(self.form.processHoles.stateChanged) + signals.append(self.form.processPerimeter.stateChanged) + signals.append(self.form.processCircles.stateChanged) return signals @@ -159,7 +166,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.stepover.setEnabled(self.obj.NumPasses > 1) def registerSignalHandlers(self, obj): - self.form.useCompensation.stateChanged.connect(self.updateVisibility) + if hasattr(self.form.useCompensation, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.useCompensation.checkStateChanged.connect(self.updateVisibility) + else: # Qt version < 6.7.0 + self.form.useCompensation.stateChanged.connect(self.updateVisibility) self.form.numPasses.editingFinished.connect(self.updateVisibility) diff --git a/src/Mod/CAM/Path/Op/Gui/Slot.py b/src/Mod/CAM/Path/Op/Gui/Slot.py index 0b6ad86cd7..f75d28a25c 100644 --- a/src/Mod/CAM/Path/Op/Gui/Slot.py +++ b/src/Mod/CAM/Path/Op/Gui/Slot.py @@ -142,7 +142,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.geo2Reference.currentIndexChanged) signals.append(self.form.layerMode.currentIndexChanged) signals.append(self.form.pathOrientation.currentIndexChanged) - signals.append(self.form.reverseDirection.stateChanged) + if hasattr(self.form.reverseDirection, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.reverseDirection.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.reverseDirection.stateChanged) return signals def updateVisibility(self, sentObj=None): diff --git a/src/Mod/CAM/Path/Op/Gui/Surface.py b/src/Mod/CAM/Path/Op/Gui/Surface.py index b1103a0a98..68a3ed40cc 100644 --- a/src/Mod/CAM/Path/Op/Gui/Surface.py +++ b/src/Mod/CAM/Path/Op/Gui/Surface.py @@ -217,10 +217,16 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.depthOffset.editingFinished) signals.append(self.form.stepOver.editingFinished) signals.append(self.form.sampleInterval.editingFinished) - signals.append(self.form.useStartPoint.stateChanged) - signals.append(self.form.boundaryEnforcement.stateChanged) - signals.append(self.form.optimizeEnabled.stateChanged) - signals.append(self.form.optimizeStepOverTransitions.stateChanged) + if hasattr(self.form.useStartPoint, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.useStartPoint.checkStateChanged) + signals.append(self.form.boundaryEnforcement.checkStateChanged) + signals.append(self.form.optimizeEnabled.checkStateChanged) + signals.append(self.form.optimizeStepOverTransitions.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.useStartPoint.stateChanged) + signals.append(self.form.boundaryEnforcement.stateChanged) + signals.append(self.form.optimizeEnabled.stateChanged) + signals.append(self.form.optimizeStepOverTransitions.stateChanged) return signals diff --git a/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py b/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py index 4b6d95c78b..d38c601227 100644 --- a/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py +++ b/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py @@ -229,7 +229,10 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): signals.append(self.form.threadTPI.editingFinished) signals.append(self.form.opDirection.currentIndexChanged) signals.append(self.form.opPasses.editingFinished) - signals.append(self.form.leadInOut.stateChanged) + if hasattr(self.form.leadInOut, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.leadInOut.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.leadInOut.stateChanged) signals.append(self.form.toolController.currentIndexChanged) diff --git a/src/Mod/CAM/Path/Op/Gui/Waterline.py b/src/Mod/CAM/Path/Op/Gui/Waterline.py index 23347f8a4c..dba1cd1533 100644 --- a/src/Mod/CAM/Path/Op/Gui/Waterline.py +++ b/src/Mod/CAM/Path/Op/Gui/Waterline.py @@ -126,7 +126,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.boundaryAdjustment.editingFinished) signals.append(self.form.stepOver.editingFinished) signals.append(self.form.sampleInterval.editingFinished) - signals.append(self.form.optimizeEnabled.stateChanged) + if hasattr(self.form.optimizeEnabled, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.optimizeEnabled.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.optimizeEnabled.stateChanged) return signals diff --git a/src/Mod/CAM/Path/Op/Slot.py b/src/Mod/CAM/Path/Op/Slot.py index d4fd14fa21..c67435a221 100644 --- a/src/Mod/CAM/Path/Op/Slot.py +++ b/src/Mod/CAM/Path/Op/Slot.py @@ -484,6 +484,7 @@ class ObjectSlot(PathOp.ObjectOp): """opExecute(obj) ... process surface operation""" Path.Log.track() + # Init operation state self.base = None self.shape1 = None self.shape2 = None @@ -494,53 +495,45 @@ class ObjectSlot(PathOp.ObjectOp): self.dYdX1 = None self.dYdX2 = None self.bottomEdges = None - self.stockZMin = None self.isArc = 0 self.arcCenter = None self.arcMidPnt = None self.arcRadius = 0.0 self.newRadius = 0.0 self.featureDetails = ["", ""] - self.isDebug = False if Path.Log.getLevel(Path.Log.thisModule()) != 4 else True - self.showDebugObjects = False + self.commandlist = [] self.stockZMin = self.job.Stock.Shape.BoundBox.ZMin - CMDS = list() - try: - dotIdx = __name__.index(".") + 1 - except Exception: - dotIdx = 0 - self.module = __name__[dotIdx:] + # Debug settings + self.isDebug = Path.Log.getLevel(Path.Log.thisModule()) == 4 + self.showDebugObjects = self.isDebug and obj.ShowTempObjects - # Setup debugging group for temp objects, when in DEBUG mode - if self.isDebug: - self.showDebugObjects = obj.ShowTempObjects if self.showDebugObjects: - FCAD = FreeCAD.ActiveDocument - for grpNm in ["tmpDebugGrp", "tmpDebugGrp001"]: - if hasattr(FCAD, grpNm): - for go in FCAD.getObject(grpNm).Group: - FCAD.removeObject(go.Name) - FCAD.removeObject(grpNm) - self.tmpGrp = FCAD.addObject("App::DocumentObjectGroup", "tmpDebugGrp") + self._clearDebugGroups() + self.tmpGrp = FreeCAD.ActiveDocument.addObject( + "App::DocumentObjectGroup", "tmpDebugGrp" + ) - # Begin GCode for operation with basic information - # ... and move cutter to clearance height and startpoint + # GCode operation header tool = obj.ToolController.Tool - toolType = tool.ToolType if hasattr(tool, "ToolType") else tool.ShapeName - output = "" - if obj.Comment != "": - self.commandlist.append(Path.Command("N ({})".format(obj.Comment), {})) - self.commandlist.append(Path.Command("N ({})".format(obj.Label), {})) - self.commandlist.append(Path.Command("N (Tool type: {})".format(toolType), {})) + toolType = getattr(tool, "ShapeType", None) + if toolType is None: + Path.Log.warning("Tool does not define ShapeType, using label as fallback.") + toolType = tool.Label + + if obj.Comment: + self.commandlist.append(Path.Command(f"N ({obj.Comment})", {})) + self.commandlist.append(Path.Command(f"N ({obj.Label})", {})) + self.commandlist.append(Path.Command(f"N (Tool type: {toolType})", {})) self.commandlist.append( - Path.Command("N (Compensated Tool Path. Diameter: {})".format(tool.Diameter), {}) + Path.Command(f"N (Compensated Tool Path. Diameter: {tool.Diameter})", {}) ) - self.commandlist.append(Path.Command("N ({})".format(output), {})) + self.commandlist.append(Path.Command("N ()", {})) + self.commandlist.append( Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) ) - if obj.UseStartPoint is True: + if obj.UseStartPoint: self.commandlist.append( Path.Command( "G0", @@ -552,10 +545,8 @@ class ObjectSlot(PathOp.ObjectOp): ) ) - # Impose property limits + # Enforce limits and prep depth steps self.opApplyPropertyLimits(obj) - - # Calculate default depthparams for operation self.depthParams = PathUtils.depth_params( obj.ClearanceHeight.Value, obj.SafeHeight.Value, @@ -565,26 +556,32 @@ class ObjectSlot(PathOp.ObjectOp): obj.FinalDepth.Value, ) - # ###### MAIN COMMANDS FOR OPERATION ###### - + # Main path generation cmds = self._makeOperation(obj) if cmds: - CMDS.extend(cmds) + self.commandlist.extend(cmds) - # Save gcode produced - CMDS.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid})) - self.commandlist.extend(CMDS) + # Final move to clearance height + self.commandlist.append( + Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) + ) - # ###### CLOSING COMMANDS FOR OPERATION ###### - - # Hide the temporary objects - if self.showDebugObjects: - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(self.tmpGrp.Name).Visibility = False + # Hide debug visuals + if self.showDebugObjects and FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(self.tmpGrp.Name).Visibility = False self.tmpGrp.purgeTouched() return True + def _clearDebugGroups(self): + doc = FreeCAD.ActiveDocument + for name in ["tmpDebugGrp", "tmpDebugGrp001"]: + grp = getattr(doc, name, None) + if grp: + for obj in grp.Group: + doc.removeObject(obj.Name) + doc.removeObject(name) + # Control methods for operation def _makeOperation(self, obj): """This method controls the overall slot creation process.""" @@ -951,10 +948,10 @@ class ObjectSlot(PathOp.ObjectOp): def _processSingleHorizFace(self, obj, shape): """Determine slot path endpoints from a single horizontally oriented face.""" Path.Log.debug("_processSingleHorizFace()") - lineTypes = ["Part::GeomLine"] + line_types = ["Part::GeomLine"] - def getRadians(self, E): - vect = self._dXdYdZ(E) + def get_edge_angle_deg(edge): + vect = self._dXdYdZ(edge) norm = self._normalizeVector(vect) rads = self._getVectorAngle(norm) deg = math.degrees(rads) @@ -968,87 +965,95 @@ class ObjectSlot(PathOp.ObjectOp): FreeCAD.Console.PrintError(msg + "\n") return False - # Create tuples as (edge index, length, angle) - eTups = list() - for i in range(0, 4): - eTups.append((i, shape.Edges[i].Length, getRadians(self, shape.Edges[i]))) + # Create tuples as (edge index, edge length, edge angle) + edge_info_list = [] + for edge_index in range(4): + edge = shape.Edges[edge_index] + edge_length = edge.Length + edge_angle = get_edge_angle_deg(edge) + edge_info_list.append((edge_index, edge_length, edge_angle)) - # Sort tuples by edge angle - eTups.sort(key=lambda tup: tup[2]) + # Sort edges by angle ascending + edge_info_list.sort(key=lambda tup: tup[2]) - # Identify parallel edges - parallel_edge_pairs = list() - parallel_edge_flags = list() - flag = 1 - eCnt = len(shape.Edges) - lstE = eCnt - 1 - for i in range(0, eCnt): # populate empty parallel edge flag list - parallel_edge_flags.append(0) - for i in range(0, eCnt): # Cycle through edges to identify parallel pairs - if i < lstE: - ni = i + 1 - A = eTups[i] - B = eTups[ni] - if abs(A[2] - B[2]) < 0.00000001: # test slopes(yaw angles) - debug = False - eA = shape.Edges[A[0]] - eB = shape.Edges[B[0]] - if eA.Curve.TypeId not in lineTypes: - debug = eA.Curve.TypeId - if not debug: - if eB.Curve.TypeId not in lineTypes: - debug = eB.Curve.TypeId - else: - parallel_edge_pairs.append((eA, eB)) - # set parallel flags for this pair of edges - parallel_edge_flags[A[0]] = flag - parallel_edge_flags[B[0]] = flag - flag += 1 - if debug: - msg = "Erroneous Curve.TypeId: {}".format(debug) - Path.Log.debug(msg) + # Identify parallel edge pairs and track flags + parallel_pairs = [] + parallel_flags = [0] * len(shape.Edges) + current_flag = 1 + last_edge_index = len(shape.Edges) - 1 - pairCnt = len(parallel_edge_pairs) - if pairCnt > 1: - parallel_edge_pairs.sort(key=lambda tup: tup[0].Length, reverse=True) + for i in range(len(shape.Edges)): + if i >= last_edge_index: + continue + + next_i = i + 1 + edge_a_info = edge_info_list[i] + edge_b_info = edge_info_list[next_i] + angle_a = edge_a_info[2] + angle_b = edge_b_info[2] + + if abs(angle_a - angle_b) >= 1e-6: # consider improving with normalized angle diff + continue + + edge_a = shape.Edges[edge_a_info[0]] + edge_b = shape.Edges[edge_b_info[0]] + + debug_type_id = None + if edge_a.Curve.TypeId not in line_types: + debug_type_id = edge_a.Curve.TypeId + elif edge_b.Curve.TypeId not in line_types: + debug_type_id = edge_b.Curve.TypeId + + if debug_type_id: + Path.Log.debug(f"Erroneous Curve.TypeId: {debug_type_id}") + else: + parallel_pairs.append((edge_a, edge_b)) + parallel_flags[edge_a_info[0]] = current_flag + parallel_flags[edge_b_info[0]] = current_flag + current_flag += 1 + + pair_count = len(parallel_pairs) + if pair_count > 1: + # Sort pairs by longest edge first + parallel_pairs.sort(key=lambda pair: pair[0].Length, reverse=True) if self.isDebug: - Path.Log.debug(" -pairCnt: {}".format(pairCnt)) - for a, b in parallel_edge_pairs: - Path.Log.debug(" -pair: {}, {}".format(round(a.Length, 4), round(b.Length, 4))) - Path.Log.debug(" -parallel_edge_flags: {}".format(parallel_edge_flags)) + Path.Log.debug(f" - Parallel pair count: {pair_count}") + for edge1, edge2 in parallel_pairs: + Path.Log.debug( + f" - Pair lengths: {round(edge1.Length, 4)}, {round(edge2.Length, 4)}" + ) + Path.Log.debug(f" - Parallel flags: {parallel_flags}") - if pairCnt == 0: + if pair_count == 0: msg = translate("CAM_Slot", "No parallel edges identified.") FreeCAD.Console.PrintError(msg + "\n") return False - elif pairCnt == 1: - # One pair of parallel edges identified - if eCnt == 4: - flag_set = list() - for i in range(0, 4): - e = parallel_edge_flags[i] - if e == 0: - flag_set.append(shape.Edges[i]) - if len(flag_set) == 2: - same = (flag_set[0], flag_set[1]) + + if pair_count == 1: + if len(shape.Edges) == 4: + # Find edges that are NOT in the identified parallel pair + non_parallel_edges = [ + shape.Edges[i] for i, flag in enumerate(parallel_flags) if flag == 0 + ] + if len(non_parallel_edges) == 2: + selected_edges = (non_parallel_edges[0], non_parallel_edges[1]) else: - same = parallel_edge_pairs[0] + selected_edges = parallel_pairs[0] else: - same = parallel_edge_pairs[0] + selected_edges = parallel_pairs[0] else: if obj.Reference1 == "Long Edge": - same = parallel_edge_pairs[1] + selected_edges = parallel_pairs[1] elif obj.Reference1 == "Short Edge": - same = parallel_edge_pairs[0] + selected_edges = parallel_pairs[0] else: - msg = "Reference1 " - msg += translate("CAM_Slot", "value error.") + msg = "Reference1 " + translate("CAM_Slot", "value error.") FreeCAD.Console.PrintError(msg + "\n") return False - (p1, p2) = self._getOppMidPoints(same) - return (p1, p2) + (point1, point2) = self._getOppMidPoints(selected_edges) + return (point1, point2) def _processSingleComplexFace(self, obj, shape): """Determine slot path endpoints from a single complex face.""" @@ -1098,12 +1103,11 @@ class ObjectSlot(PathOp.ObjectOp): def _processSingleEdge(self, obj, edge): """Determine slot path endpoints from a single horizontally oriented edge.""" Path.Log.debug("_processSingleEdge()") - tolrnc = 0.0000001 - lineTypes = ["Part::GeomLine"] - curveTypes = ["Part::GeomCircle"] + tol = 1e-7 + lineTypes = {"Part::GeomLine"} + curveTypes = {"Part::GeomCircle"} def oversizedTool(holeDiam): - # Test if tool larger than opening if self.tool.Diameter > holeDiam: msg = translate("CAM_Slot", "Current tool larger than arc diameter.") FreeCAD.Console.PrintError(msg + "\n") @@ -1111,52 +1115,37 @@ class ObjectSlot(PathOp.ObjectOp): return False def isHorizontal(z1, z2, z3): - # Check that all Z values are equal (isRoughly same) - if abs(z1 - z2) > tolrnc or abs(z1 - z3) > tolrnc: - # abs(z2 - z3) > tolrnc): 3rd test redundant. - return False - return True + return abs(z1 - z2) <= tol and abs(z1 - z3) <= tol def circumCircleFrom3Points(P1, P2, P3): - # Source code for this function copied from (with modifications): - # https://wiki.freecad.org/Macro_Draft_Circle_3_Points_3D - vP2P1 = P2 - P1 - vP3P2 = P3 - P2 - vP1P3 = P1 - P3 - - L = vP2P1.cross(vP3P2).Length - # Circle radius (not used) - # r = vP1P2.Length * vP2P3.Length * vP3P1.Length / 2 / l + v1 = P2 - P1 + v2 = P3 - P2 + v3 = P1 - P3 + L = v1.cross(v2).Length if round(L, 8) == 0.0: - Path.Log.error("The three points are colinear, arc is a straight.") + Path.Log.error("Three points are colinear. Arc is straight.") return False - - # Sphere center. - twolsqr = 2 * L * L - a = -vP3P2.dot(vP3P2) * vP2P1.dot(vP1P3) / twolsqr - b = -vP1P3.dot(vP1P3) * vP3P2.dot(vP2P1) / twolsqr - c = -vP2P1.dot(vP2P1) * vP1P3.dot(vP3P2) / twolsqr + twoL2 = 2 * L * L + a = -v2.dot(v2) * v1.dot(v3) / twoL2 + b = -v3.dot(v3) * v2.dot(v1) / twoL2 + c = -v1.dot(v1) * v3.dot(v2) / twoL2 return P1 * a + P2 * b + P3 * c - V1 = edge.Vertexes[0] + verts = edge.Vertexes + V1 = verts[0] p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0) - if len(edge.Vertexes) == 1: # circle has one virtex - p2 = FreeCAD.Vector(p1) - else: - V2 = edge.Vertexes[1] - p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0) + p2 = p1 if len(verts) == 1 else FreeCAD.Vector(verts[1].X, verts[1].Y, 0.0) - # Process edge based on curve type - if edge.Curve.TypeId in lineTypes: + curveType = edge.Curve.TypeId + if curveType in lineTypes: return (p1, p2) - elif edge.Curve.TypeId in curveTypes: - if len(edge.Vertexes) == 1: - # Circle edge - Path.Log.debug("Arc with single vertex.") + elif curveType in curveTypes: + if len(verts) == 1: + # Full circle + Path.Log.debug("Arc with single vertex (circle).") if oversizedTool(edge.BoundBox.XLength): return False - self.isArc = 1 tp1 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.33)) tp2 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.66)) @@ -1165,37 +1154,37 @@ class ObjectSlot(PathOp.ObjectOp): center = edge.BoundBox.Center self.arcCenter = FreeCAD.Vector(center.x, center.y, 0.0) - midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) - self.arcMidPnt = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0) + mid = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) + self.arcMidPnt = FreeCAD.Vector(mid.x, mid.y, 0.0) self.arcRadius = edge.BoundBox.XLength / 2.0 else: - # Arc edge + # Arc segment Path.Log.debug("Arc with multiple vertices.") - self.isArc = 2 - midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) - if not isHorizontal(V1.Z, V2.Z, midPnt.z): + V2 = verts[1] + mid = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) + if not isHorizontal(V1.Z, V2.Z, mid.z): + return False + mid.z = 0.0 + center = circumCircleFrom3Points(p1, p2, FreeCAD.Vector(mid.x, mid.y, 0.0)) + if not center: return False - midPnt.z = 0.0 - circleCenter = circumCircleFrom3Points(p1, p2, midPnt) - if not circleCenter: - return False - self.arcMidPnt = midPnt - self.arcCenter = circleCenter - self.arcRadius = p1.sub(circleCenter).Length + self.isArc = 2 + self.arcMidPnt = FreeCAD.Vector(mid.x, mid.y, 0.0) + self.arcCenter = center + self.arcRadius = (p1 - center).Length if oversizedTool(self.arcRadius * 2.0): return False return (p1, p2) + else: msg = translate( - "CAM_Slot", - "Failed, slot from edge only accepts lines, arcs and circles.", + "CAM_Slot", "Failed, slot from edge only accepts lines, arcs and circles." ) FreeCAD.Console.PrintError(msg + "\n") - - return False # not line , not circle + return False # Methods for processing double geometry def _processDouble(self, obj, shape_1, sub1, shape_2, sub2): @@ -1253,218 +1242,152 @@ class ObjectSlot(PathOp.ObjectOp): return FreeCAD.Vector(dX, dY, dZ) def _normalizeVector(self, v): - """_normalizeVector(v)... - Returns a copy of the vector received with values rounded to 10 decimal places.""" - posTol = 0.0000000001 # arbitrary, use job Geometry Tolerance ??? - negTol = -1 * posTol - V = FreeCAD.Vector(v.x, v.y, v.z) - V.normalize() - x = V.x - y = V.y - z = V.z + """Return a normalized vector with components rounded to nearest axis-aligned value if close.""" + tol = 1e-10 + V = FreeCAD.Vector(v).normalize() - if V.x != 0 and abs(V.x) < posTol: - x = 0.0 - if V.x != 1 and 1.0 - V.x < posTol: - x = 1.0 - if V.x != -1 and -1.0 - V.x > negTol: - x = -1.0 + def snap(val): + if abs(val) < tol: + return 0.0 + if abs(1.0 - abs(val)) < tol: + return 1.0 if val > 0 else -1.0 + return val - if V.y != 0 and abs(V.y) < posTol: - y = 0.0 - if V.y != 1 and 1.0 - V.y < posTol: - y = 1.0 - if V.y != -1 and -1.0 - V.y > negTol: - y = -1.0 + return FreeCAD.Vector(snap(V.x), snap(V.y), snap(V.z)) - if V.z != 0 and abs(V.z) < posTol: - z = 0.0 - if V.z != 1 and 1.0 - V.z < posTol: - z = 1.0 - if V.z != -1 and -1.0 - V.z > negTol: - z = -1.0 + def _getLowestPoint(self, shape): + """Return the average XY of the vertices with the lowest Z value.""" + vertices = shape.Vertexes + lowest_z = min(v.Z for v in vertices) + lowest_vertices = [v for v in vertices if v.Z == lowest_z] - return FreeCAD.Vector(x, y, z) + avg_x = sum(v.X for v in lowest_vertices) / len(lowest_vertices) + avg_y = sum(v.Y for v in lowest_vertices) / len(lowest_vertices) + return FreeCAD.Vector(avg_x, avg_y, lowest_z) - def _getLowestPoint(self, shape_1): - """_getLowestPoint(shape)... Returns lowest vertex of shape as vector.""" - # find lowest vertex - vMin = shape_1.Vertexes[0] - zmin = vMin.Z - same = [vMin] - for V in shape_1.Vertexes: - if V.Z < zmin: - zmin = V.Z - # vMin = V - elif V.Z == zmin: - same.append(V) - if len(same) > 1: - X = [E.X for E in same] - Y = [E.Y for E in same] - avgX = sum(X) / len(X) - avgY = sum(Y) / len(Y) - return FreeCAD.Vector(avgX, avgY, zmin) - else: - return FreeCAD.Vector(V.X, V.Y, V.Z) + def _getHighestPoint(self, shape): + """Return the average XY of the vertices with the highest Z value.""" + vertices = shape.Vertexes + highest_z = max(v.Z for v in vertices) + highest_vertices = [v for v in vertices if v.Z == highest_z] - def _getHighestPoint(self, shape_1): - """_getHighestPoint(shape)... Returns highest vertex of shape as vector.""" - # find highest vertex - vMax = shape_1.Vertexes[0] - zmax = vMax.Z - same = [vMax] - for V in shape_1.Vertexes: - if V.Z > zmax: - zmax = V.Z - # vMax = V - elif V.Z == zmax: - same.append(V) - if len(same) > 1: - X = [E.X for E in same] - Y = [E.Y for E in same] - avgX = sum(X) / len(X) - avgY = sum(Y) / len(Y) - return FreeCAD.Vector(avgX, avgY, zmax) - else: - return FreeCAD.Vector(V.X, V.Y, V.Z) + avg_x = sum(v.X for v in highest_vertices) / len(highest_vertices) + avg_y = sum(v.Y for v in highest_vertices) / len(highest_vertices) + return FreeCAD.Vector(avg_x, avg_y, highest_z) def _processFeature(self, obj, shape, sub, pNum): - """_processFeature(obj, shape, sub, pNum)... - This function analyzes a shape and returns a three item tuple containing: - working point, - shape orientation/slope, - shape category as face, edge, or vert.""" + """Analyze a shape and return a tuple: (working point, slope, category).""" p = None dYdX = None - cat = sub[:4] - Path.Log.debug("sub-feature is {}".format(cat)) - Ref = getattr(obj, "Reference" + str(pNum)) - if cat == "Face": + + Ref = getattr(obj, f"Reference{pNum}") + + if sub.startswith("Face"): + cat = "Face" BE = self._getBottomEdge(shape) if BE: self.bottomEdges.append(BE) - # calculate slope of face + + # Get slope from first vertex to center of mass V0 = shape.Vertexes[0] v1 = shape.CenterOfMass temp = FreeCAD.Vector(v1.x - V0.X, v1.y - V0.Y, 0.0) - dYdX = self._normalizeVector(temp) + dYdX = self._normalizeVector(temp) if temp.Length != 0 else FreeCAD.Vector(0, 0, 0) - # Determine normal vector for face + # Face normal must be vertical norm = shape.normalAt(0.0, 0.0) - # FreeCAD.Console.PrintMessage('{} normal {}.\n'.format(sub, norm)) if norm.z != 0: msg = translate("CAM_Slot", "The selected face is not oriented vertically:") - FreeCAD.Console.PrintError(msg + " {}.\n".format(sub)) + FreeCAD.Console.PrintError(f"{msg} {sub}.\n") return False + # Choose working point if Ref == "Center of Mass": - comS = shape.CenterOfMass - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + com = shape.CenterOfMass + p = FreeCAD.Vector(com.x, com.y, 0.0) elif Ref == "Center of BoundBox": - comS = shape.BoundBox.Center - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + bbox = shape.BoundBox.Center + p = FreeCAD.Vector(bbox.x, bbox.y, 0.0) elif Ref == "Lowest Point": p = self._getLowestPoint(shape) elif Ref == "Highest Point": p = self._getHighestPoint(shape) - elif cat == "Edge": + elif sub.startswith("Edge"): + cat = "Edge" featDetIdx = pNum - 1 if shape.Curve.TypeId == "Part::GeomCircle": self.featureDetails[featDetIdx] = "arc" - # calculate slope between end vertexes - v0 = shape.Edges[0].Vertexes[0] - v1 = shape.Edges[0].Vertexes[1] + + edge = shape.Edges[0] if hasattr(shape, "Edges") else shape + v0 = edge.Vertexes[0] + v1 = edge.Vertexes[1] temp = FreeCAD.Vector(v1.X - v0.X, v1.Y - v0.Y, 0.0) - dYdX = self._normalizeVector(temp) + dYdX = self._normalizeVector(temp) if temp.Length != 0 else FreeCAD.Vector(0, 0, 0) if Ref == "Center of Mass": - comS = shape.CenterOfMass - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + com = shape.CenterOfMass + p = FreeCAD.Vector(com.x, com.y, 0.0) elif Ref == "Center of BoundBox": - comS = shape.BoundBox.Center - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + bbox = shape.BoundBox.Center + p = FreeCAD.Vector(bbox.x, bbox.y, 0.0) elif Ref == "Lowest Point": p = self._findLowestPointOnEdge(shape) elif Ref == "Highest Point": p = self._findHighestPointOnEdge(shape) - elif cat == "Vert": + elif sub.startswith("Vert"): + cat = "Vert" V = shape.Vertexes[0] p = FreeCAD.Vector(V.X, V.Y, 0.0) + else: + Path.Log.warning(f"Unrecognized subfeature type: {sub}") + return False + if p: return (p, dYdX, cat) return False def _extendArcSlot(self, p1, p2, cent, begExt, endExt): - """_extendArcSlot(p1, p2, cent, begExt, endExt)... - This function extends an arc defined by two end points, p1 and p2, and the center. - The arc is extended along the circumference with begExt and endExt values. - The function returns the new end points as tuple (n1, n2) to replace p1 and p2.""" - cancel = True + """Extend an arc defined by endpoints p1, p2 and center cent. + begExt and endExt are extension lengths along the arc at each end. + Returns new (p1, p2) as (n1, n2).""" if not begExt and not endExt: return (p1, p2) - n1 = p1 - n2 = p2 - - # Create a chord of the right length, on XY plane, starting on x axis - def makeChord(rads): - x = self.newRadius * math.cos(rads) - y = self.newRadius * math.sin(rads) - a = FreeCAD.Vector(self.newRadius, 0.0, 0.0) - b = FreeCAD.Vector(x, y, 0.0) + def makeChord(angle_rad): + x = self.newRadius * math.cos(angle_rad) + y = self.newRadius * math.sin(angle_rad) + a = FreeCAD.Vector(self.newRadius, 0, 0) + b = FreeCAD.Vector(x, y, 0) return Part.makeLine(a, b) - # Convert extension to radians; make a generic chord ( line ) on XY plane from the x axis - # rotate and shift into place so it has same vertices as the required arc extension - # adjust rotation angle to provide +ve or -ve extension as needed - origin = FreeCAD.Vector(0.0, 0.0, 0.0) + origin = FreeCAD.Vector(0, 0, 0) + z_axis = FreeCAD.Vector(0, 0, 1) + + n1, n2 = p1, p2 + if begExt: - ExtRadians = abs(begExt / self.newRadius) - chord = makeChord(ExtRadians) - - beginRadians = self._getVectorAngle(p1.sub(self.arcCenter)) - if begExt < 0: - beginRadians += ( - 0 # negative Ext shortens slot so chord endpoint is slot start point - ) - else: - beginRadians -= ( - 2 * ExtRadians - ) # positive Ext lengthens slot so decrease start point angle - - # Path.Log.debug('begExt angles are: {}, {}'.format(beginRadians, math.degrees(beginRadians))) - - chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(beginRadians)) + ext_rad = abs(begExt / self.newRadius) + angle = self._getVectorAngle(p1.sub(self.arcCenter)) + angle += -2 * ext_rad if begExt > 0 else 0 + chord = makeChord(ext_rad) + chord.rotate(origin, z_axis, math.degrees(angle)) chord.translate(self.arcCenter) self._addDebugObject(chord, "ExtendStart") - - v1 = chord.Vertexes[1] - n1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) + n1 = chord.Vertexes[1].Point if endExt: - ExtRadians = abs(endExt / self.newRadius) - chord = makeChord(ExtRadians) - - endRadians = self._getVectorAngle(p2.sub(self.arcCenter)) - if endExt > 0: - endRadians += 0 # positive Ext lengthens slot so chord endpoint is good - else: - endRadians -= ( - 2 * ExtRadians - ) # negative Ext shortens slot so decrease end point angle - - # Path.Log.debug('endExt angles are: {}, {}'.format(endRadians, math.degrees(endRadians))) - - chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(endRadians)) + ext_rad = abs(endExt / self.newRadius) + angle = self._getVectorAngle(p2.sub(self.arcCenter)) + angle += 0 if endExt > 0 else -2 * ext_rad + chord = makeChord(ext_rad) + chord.rotate(origin, z_axis, math.degrees(angle)) chord.translate(self.arcCenter) self._addDebugObject(chord, "ExtendEnd") - - v1 = chord.Vertexes[1] - n2 = FreeCAD.Vector(v1.X, v1.Y, 0.0) + n2 = chord.Vertexes[1].Point return (n1, n2) @@ -1504,161 +1427,135 @@ class ObjectSlot(PathOp.ObjectOp): def _isParallel(self, dYdX1, dYdX2): """Determine if two orientation vectors are parallel.""" - # if dYdX1.add(dYdX2).Length == 0: - # return True - # if ((dYdX1.x + dYdX2.x) / 2.0 == dYdX1.x and - # (dYdX1.y + dYdX2.y) / 2.0 == dYdX1.y): - # return True - # return False return dYdX1.cross(dYdX2) == FreeCAD.Vector(0, 0, 0) def _makePerpendicular(self, p1, p2, length): - """_makePerpendicular(p1, p2, length)... - Using a line defined by p1 and p2, returns a perpendicular vector centered - at the midpoint of the line, with length value.""" - line = Part.makeLine(p1, p2) - midPnt = line.CenterOfMass + """Using a line defined by p1 and p2, returns a perpendicular vector + centered at the midpoint of the line, with given length.""" + midPnt = (p1.add(p2)).multiply(0.5) halfDist = length / 2.0 - if self.dYdX1: + + if getattr(self, "dYdX1", None): half = FreeCAD.Vector(self.dYdX1.x, self.dYdX1.y, 0.0).multiply(halfDist) n1 = midPnt.add(half) n2 = midPnt.sub(half) return (n1, n2) - elif self.dYdX2: + + elif getattr(self, "dYdX2", None): half = FreeCAD.Vector(self.dYdX2.x, self.dYdX2.y, 0.0).multiply(halfDist) n1 = midPnt.add(half) n2 = midPnt.sub(half) return (n1, n2) + else: toEnd = p2.sub(p1) - perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0) - perp.normalize() - perp.multiply(halfDist) + perp = FreeCAD.Vector(-toEnd.y, toEnd.x, 0.0) + perp = perp.normalize() # normalize() returns the vector normalized + perp = perp.multiply(halfDist) n1 = midPnt.add(perp) n2 = midPnt.sub(perp) return (n1, n2) def _findLowestPointOnEdge(self, E): - tol = 0.0000001 + tol = 1e-7 zMin = E.BoundBox.ZMin - # Test first vertex - v = E.Vertexes[0] - if abs(v.Z - zMin) < tol: - return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test second vertex - v = E.Vertexes[1] - if abs(v.Z - zMin) < tol: - return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test middle point of edge - eMidLen = E.Length / 2.0 - eMidPnt = E.valueAt(E.getParameterByLength(eMidLen)) - if abs(eMidPnt.z - zMin) < tol: - return eMidPnt - if E.BoundBox.ZLength < 0.000000001: # roughly horizontal edge - return eMidPnt + + # Try each vertex + for v in E.Vertexes: + if abs(v.Z - zMin) < tol: + return FreeCAD.Vector(v.X, v.Y, v.Z) + + # Try midpoint + mid = E.valueAt(E.getParameterByLength(E.Length / 2.0)) + if abs(mid.z - zMin) < tol or E.BoundBox.ZLength < 1e-9: + return mid + + # Fallback return self._findLowestEdgePoint(E) def _findLowestEdgePoint(self, E): zMin = E.BoundBox.ZMin - eLen = E.Length - L0 = 0.0 - L1 = eLen - p0 = None - p1 = None + L0, L1 = 0.0, E.Length + tol = 1e-5 + max_iter = 2000 cnt = 0 - while L1 - L0 > 0.00001 and cnt < 2000: - adj = (L1 - L0) * 0.1 - # Get points at L0 and L1 along edge + + while (L1 - L0) > tol and cnt < max_iter: p0 = E.valueAt(E.getParameterByLength(L0)) p1 = E.valueAt(E.getParameterByLength(L1)) - # Adjust points based on proximity to target depth + diff0 = p0.z - zMin diff1 = p1.z - zMin + + adj = (L1 - L0) * 0.1 if diff0 < diff1: L1 -= adj elif diff0 > diff1: L0 += adj else: + # When equal, narrow from both ends L0 += adj L1 -= adj cnt += 1 + midLen = (L0 + L1) / 2.0 return E.valueAt(E.getParameterByLength(midLen)) def _findHighestPointOnEdge(self, E): - tol = 0.0000001 + tol = 1e-7 zMax = E.BoundBox.ZMax - # Test first vertex + + # Check first vertex v = E.Vertexes[0] if abs(zMax - v.Z) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test second vertex + + # Check second vertex v = E.Vertexes[1] if abs(zMax - v.Z) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test middle point of edge - eMidLen = E.Length / 2.0 - eMidPnt = E.valueAt(E.getParameterByLength(eMidLen)) - if abs(zMax - eMidPnt.z) < tol: - return eMidPnt - if E.BoundBox.ZLength < 0.000000001: # roughly horizontal edge - return eMidPnt + + # Check midpoint on edge + midLen = E.Length / 2.0 + midPnt = E.valueAt(E.getParameterByLength(midLen)) + if abs(zMax - midPnt.z) < tol or E.BoundBox.ZLength < 1e-9: + return midPnt + return self._findHighestEdgePoint(E) def _findHighestEdgePoint(self, E): zMax = E.BoundBox.ZMax eLen = E.Length - L0 = 0 + L0 = 0.0 L1 = eLen - p0 = None - p1 = None cnt = 0 - while L1 - L0 > 0.00001 and cnt < 2000: + while L1 - L0 > 1e-5 and cnt < 2000: adj = (L1 - L0) * 0.1 - # Get points at L0 and L1 along edge p0 = E.valueAt(E.getParameterByLength(L0)) p1 = E.valueAt(E.getParameterByLength(L1)) - # Adjust points based on proximity to target depth + diff0 = zMax - p0.z diff1 = zMax - p1.z - if diff0 < diff1: - L1 -= adj - elif diff0 > diff1: + + # Closer to zMax means smaller diff (diff >= 0) + if diff0 > diff1: + # p1 is closer to zMax, so move L0 up to narrow range toward p1 L0 += adj + elif diff0 < diff1: + # p0 is closer, move L1 down to narrow range toward p0 + L1 -= adj else: L0 += adj L1 -= adj + cnt += 1 + midLen = (L0 + L1) / 2.0 return E.valueAt(E.getParameterByLength(midLen)) def _getVectorAngle(self, v): - # Assumes Z value of vector is zero - halfPi = math.pi / 2 - - if v.y == 1 and v.x == 0: - return halfPi - if v.y == -1 and v.x == 0: - return math.pi + halfPi - if v.y == 0 and v.x == 1: - return 0.0 - if v.y == 0 and v.x == -1: - return math.pi - - x = abs(v.x) - y = abs(v.y) - rads = math.atan(y / x) - if v.x > 0: - if v.y > 0: - return rads - else: - return (2 * math.pi) - rads - if v.x < 0: - if v.y > 0: - return math.pi - rads - else: - return math.pi + rads + return math.atan2(v.y, v.x) % (2 * math.pi) def _getCutSidePoints(self, obj, v0, v1, a1, a2, b1, b2): ea1 = Part.makeLine(v0, a1) @@ -1693,29 +1590,28 @@ class ObjectSlot(PathOp.ObjectOp): return False def _getVertFaceType(self, shape): - wires = list() + bottom_edge = self._getBottomEdge(shape) + if bottom_edge: + return ("Edge", bottom_edge) - bottomEdge = self._getBottomEdge(shape) - if bottomEdge: - return ("Edge", bottomEdge) + # Extrude vertically to create a sliceable solid + z_length = shape.BoundBox.ZLength + extrude_vec = FreeCAD.Vector(0, 0, z_length * 2.2 + 10) + extruded = shape.extrude(extrude_vec) - # Extract cross-section of face - extFwd = (shape.BoundBox.ZLength * 2.2) + 10 - extShp = shape.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - sliceZ = shape.BoundBox.ZMin + (extFwd / 2.0) - slcs = extShp.slice(FreeCAD.Vector(0, 0, 1), sliceZ) - for i in slcs: - wires.append(i) - if len(wires) > 0: - if wires[0].isClosed(): - face = Part.Face(wires[0]) - if face.Area > 0: - face.translate( - FreeCAD.Vector(0.0, 0.0, shape.BoundBox.ZMin - face.BoundBox.ZMin) - ) - return ("Face", face) - return ("Wire", wires[0]) - return False + # Slice halfway up the extrusion + slice_z = shape.BoundBox.ZMin + extrude_vec.z / 2.0 + slices = extruded.slice(FreeCAD.Vector(0, 0, 1), slice_z) + + if not slices: + return False + + if (wire := slices[0]).isClosed() and (face := Part.Face(wire)) > 0: + # Align face Z with original shape + z_offset = shape.BoundBox.ZMin - face.BoundBox.ZMin + face.translate(FreeCAD.Vector(0, 0, z_offset)) + return ("Face", face) + return ("Wire", wire) def _makeReference1Enumerations(self, sub, single=False): """Customize Reference1 enumerations based on feature type.""" @@ -1742,184 +1638,137 @@ class ObjectSlot(PathOp.ObjectOp): return ["Center of Mass", "Center of BoundBox", "Lowest Point", "Highest Point"] def _lineCollisionCheck(self, obj, p1, p2): - """Make simple circle with diameter of tool, at start point. - Extrude it latterally along path. - Extrude it vertically. - Check for collision with model.""" - # Make path travel of tool as 3D solid. - rad = self.tool.Diameter / 2.0 + """Model the swept volume of a linear tool move and check for collision with the model.""" + rad = getattr(self.tool.Diameter, "Value", self.tool.Diameter) / 2.0 + extVect = FreeCAD.Vector(0.0, 0.0, obj.StartDepth.Value - obj.FinalDepth.Value) - def getPerp(p1, p2, dist): + def make_cylinder(point): + circle = Part.makeCircle(rad, point) + face = Part.Face(Part.Wire(circle.Edges)) + face.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - face.BoundBox.ZMin)) + return face.extrude(extVect) + + def make_rect_prism(p1, p2): toEnd = p2.sub(p1) - perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0) - if perp.x == 0 and perp.y == 0: - return perp + if toEnd.Length == 0: + return None + perp = FreeCAD.Vector(-toEnd.y, toEnd.x, 0.0) + if perp.Length == 0: + return None perp.normalize() - perp.multiply(dist) - return perp + perp.multiply(rad) - # Make first cylinder - ce1 = Part.Wire(Part.makeCircle(rad, p1).Edges) - C1 = Part.Face(ce1) - zTrans = obj.FinalDepth.Value - C1.BoundBox.ZMin - C1.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - extFwd = obj.StartDepth.Value - obj.FinalDepth.Value - extVect = FreeCAD.Vector(0.0, 0.0, extFwd) - startShp = C1.extrude(extVect) + v1, v2 = p1.add(perp), p1.sub(perp) + v3, v4 = p2.sub(perp), p2.add(perp) + edges = Part.__sortEdges__( + [ + Part.makeLine(v1, v2), + Part.makeLine(v2, v3), + Part.makeLine(v3, v4), + Part.makeLine(v4, v1), + ] + ) + face = Part.Face(Part.Wire(edges)) + face.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - face.BoundBox.ZMin)) + return face.extrude(extVect) - if p2.sub(p1).Length > 0: - # Make second cylinder - ce2 = Part.Wire(Part.makeCircle(rad, p2).Edges) - C2 = Part.Face(ce2) - zTrans = obj.FinalDepth.Value - C2.BoundBox.ZMin - C2.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - endShp = C2.extrude(extVect) + # Build swept volume + startShp = make_cylinder(p1) + endShp = make_cylinder(p2) if p1 != p2 else None + boxShp = make_rect_prism(p1, p2) - # Make extruded rectangle to connect cylinders - perp = getPerp(p1, p2, rad) - v1 = p1.add(perp) - v2 = p1.sub(perp) - v3 = p2.sub(perp) - v4 = p2.add(perp) - e1 = Part.makeLine(v1, v2) - e2 = Part.makeLine(v2, v3) - e3 = Part.makeLine(v3, v4) - e4 = Part.makeLine(v4, v1) - edges = Part.__sortEdges__([e1, e2, e3, e4]) - rectFace = Part.Face(Part.Wire(edges)) - zTrans = obj.FinalDepth.Value - rectFace.BoundBox.ZMin - rectFace.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - boxShp = rectFace.extrude(extVect) - - # Fuse two cylinders and box together - part1 = startShp.fuse(boxShp) - pathTravel = part1.fuse(endShp) - else: - pathTravel = startShp + pathTravel = startShp + if boxShp: + pathTravel = pathTravel.fuse(boxShp) + if endShp: + pathTravel = pathTravel.fuse(endShp) self._addDebugObject(pathTravel, "PathTravel") - # Check for collision with model try: cmn = self.base.Shape.common(pathTravel) - if cmn.Volume > 0.000001: - return True + return cmn.Volume > 1e-6 except Exception: Path.Log.debug("Failed to complete path collision check.") - - return False + return False def _arcCollisionCheck(self, obj, p1, p2, arcCenter, arcRadius): - """Make simple circle with diameter of tool, at start and end points. - Make arch face between circles. Fuse and extrude it vertically. - Check for collision with model.""" - # Make path travel of tool as 3D solid. - if hasattr(self.tool.Diameter, "Value"): - rad = self.tool.Diameter.Value / 2.0 - else: - rad = self.tool.Diameter / 2.0 - extFwd = obj.StartDepth.Value - obj.FinalDepth.Value - extVect = FreeCAD.Vector(0.0, 0.0, extFwd) + """Check for collision by modeling the swept volume of an arc toolpath.""" - if self.isArc == 1: - # full circular slot - # make outer circle - oCircle = Part.makeCircle(arcRadius + rad, arcCenter) - oWire = Part.Wire(oCircle.Edges[0]) - outer = Part.Face(oWire) - # make inner circle - iRadius = arcRadius - rad - if iRadius > 0: - iCircle = Part.makeCircle(iRadius, arcCenter) - iWire = Part.Wire(iCircle.Edges[0]) - inner = Part.Face(iWire) - # Cut outer with inner - path = outer.cut(inner) - else: - path = outer - zTrans = obj.FinalDepth.Value - path.BoundBox.ZMin - path.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - pathTravel = path.extrude(extVect) - else: - # arc slot - # Make first cylinder - ce1 = Part.Wire(Part.makeCircle(rad, p1).Edges) - C1 = Part.Face(ce1) - zTrans = obj.FinalDepth.Value - C1.BoundBox.ZMin - C1.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - startShp = C1.extrude(extVect) - # self._addDebugObject(startShp, 'StartCyl') + def make_cylinder_at_point(point, radius, height, final_depth): + circle = Part.makeCircle(radius, point) + face = Part.Face(Part.Wire(circle.Edges)) + face.translate(FreeCAD.Vector(0, 0, final_depth - face.BoundBox.ZMin)) + return face.extrude(FreeCAD.Vector(0, 0, height)) - # Make second cylinder - ce2 = Part.Wire(Part.makeCircle(rad, p2).Edges) - C2 = Part.Face(ce2) - zTrans = obj.FinalDepth.Value - C2.BoundBox.ZMin - C2.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - endShp = C2.extrude(extVect) - # self._addDebugObject(endShp, 'EndCyl') + def make_arc_face(p1, p2, center, inner_radius, outer_radius): + (pA, pB) = self._makeOffsetArc(p1, p2, center, inner_radius) + arc_inside = Arcs.arcFrom2Pts(pA, pB, center) - # Make wire with inside and outside arcs, and lines on ends. - # Convert wire to face, then extrude + (pC, pD) = self._makeOffsetArc(p1, p2, center, outer_radius) + arc_outside = Arcs.arcFrom2Pts(pC, pD, center) - # verify offset does not force radius < 0 - newRadius = arcRadius - rad - # Path.Log.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius)) - if newRadius <= 0: - msg = translate("CAM_Slot", "Current offset value produces negative radius.") - FreeCAD.Console.PrintError(msg + "\n") - return False - else: - (pA, pB) = self._makeOffsetArc(p1, p2, arcCenter, newRadius) - arc_inside = Arcs.arcFrom2Pts(pA, pB, arcCenter) + pa = FreeCAD.Vector(*arc_inside.Vertexes[0].Point[:2], 0.0) + pb = FreeCAD.Vector(*arc_inside.Vertexes[1].Point[:2], 0.0) + pc = FreeCAD.Vector(*arc_outside.Vertexes[1].Point[:2], 0.0) + pd = FreeCAD.Vector(*arc_outside.Vertexes[0].Point[:2], 0.0) - # Arc 2 - outside - # verify offset does not force radius < 0 - newRadius = arcRadius + rad - # Path.Log.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius)) - if newRadius <= 0: - msg = translate("CAM_Slot", "Current offset value produces negative radius.") - FreeCAD.Console.PrintError(msg + "\n") - return False - else: - (pC, pD) = self._makeOffsetArc(p1, p2, arcCenter, newRadius) - arc_outside = Arcs.arcFrom2Pts(pC, pD, arcCenter) - - # Make end lines to connect arcs - vA = arc_inside.Vertexes[0] - vB = arc_inside.Vertexes[1] - vC = arc_outside.Vertexes[1] - vD = arc_outside.Vertexes[0] - pa = FreeCAD.Vector(vA.X, vA.Y, 0.0) - pb = FreeCAD.Vector(vB.X, vB.Y, 0.0) - pc = FreeCAD.Vector(vC.X, vC.Y, 0.0) - pd = FreeCAD.Vector(vD.X, vD.Y, 0.0) - - # Make closed arch face and extrude e1 = Part.makeLine(pb, pc) e2 = Part.makeLine(pd, pa) edges = Part.__sortEdges__([arc_inside, e1, arc_outside, e2]) - rectFace = Part.Face(Part.Wire(edges)) - zTrans = obj.FinalDepth.Value - rectFace.BoundBox.ZMin - rectFace.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - boxShp = rectFace.extrude(extVect) - # self._addDebugObject(boxShp, 'ArcBox') + return Part.Face(Part.Wire(edges)) - # Fuse two cylinders and box together - part1 = startShp.fuse(boxShp) - pathTravel = part1.fuse(endShp) + # Radius and extrusion direction + rad = getattr(self.tool.Diameter, "Value", self.tool.Diameter) / 2.0 + extVect = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value) + + if self.isArc == 1: + # Full circle slot: make annular ring + outer = Part.Face(Part.Wire(Part.makeCircle(arcRadius + rad, arcCenter).Edges)) + iRadius = arcRadius - rad + path = ( + outer.cut(Part.Face(Part.Wire(Part.makeCircle(iRadius, arcCenter).Edges))) + if iRadius > 0 + else outer + ) + path.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - path.BoundBox.ZMin)) + pathTravel = path.extrude(extVect) + + else: + # Arc slot with entry and exit cylinders + startShp = make_cylinder_at_point(p1, rad, extVect.z, obj.FinalDepth.Value) + endShp = make_cylinder_at_point(p2, rad, extVect.z, obj.FinalDepth.Value) + + # Validate inner arc + inner_radius = arcRadius - rad + if inner_radius <= 0: + FreeCAD.Console.PrintError( + translate("CAM_Slot", "Current offset value produces negative radius.") + "\n" + ) + return False + + # Validate outer arc + outer_radius = arcRadius + rad + if outer_radius <= 0: + FreeCAD.Console.PrintError( + translate("CAM_Slot", "Current offset value produces negative radius.") + "\n" + ) + return False + + rectFace = make_arc_face(p1, p2, arcCenter, inner_radius, outer_radius) + rectFace.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - rectFace.BoundBox.ZMin)) + arcShp = rectFace.extrude(extVect) + + pathTravel = startShp.fuse(arcShp).fuse(endShp) self._addDebugObject(pathTravel, "PathTravel") - # Check for collision with model try: cmn = self.base.Shape.common(pathTravel) - if cmn.Volume > 0.000001: - # print("volume=", cmn.Volume) - return True + return cmn.Volume > 1e-6 except Exception: Path.Log.debug("Failed to complete path collision check.") - - return False + return False def _addDebugObject(self, objShape, objName): if self.showDebugObjects: diff --git a/src/Mod/CAM/Path/Post/Processor.py b/src/Mod/CAM/Path/Post/Processor.py index 1c153c309a..7cffd34264 100644 --- a/src/Mod/CAM/Path/Post/Processor.py +++ b/src/Mod/CAM/Path/Post/Processor.py @@ -222,6 +222,13 @@ class PostProcessor: curlist = [] # list of ops for tool, will repeat for each fixture sublist = [] # list of ops for output splitting + def commitToPostlist(): + if len(curlist) > 0: + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + postlist.append((toolstring, sublist)) + Path.Log.track(self._job.PostProcessorOutputFile) for idx, obj in enumerate(self._job.Operations.Group): Path.Log.track(obj.Label) @@ -231,40 +238,36 @@ class PostProcessor: Path.Log.track() continue - # Determine the proper string for the Op's TC tc = PathUtil.toolControllerForOp(obj) - if tc is None: - tcstring = "None" - elif "%T" in self._job.PostProcessorOutputFile: - tcstring = f"{tc.ToolNumber}" - else: - tcstring = re.sub(r"[^\w\d-]", "_", tc.Label) - Path.Log.track(toolstring) + + # The operation has no ToolController or uses the same + # ToolController as the previous operations if tc is None or tc.ToolNumber == currTool: + # Queue current operation curlist.append(obj) - elif tc.ToolNumber != currTool and currTool is None: # first TC - sublist.append(tc) - curlist.append(obj) - currTool = tc.ToolNumber - toolstring = tcstring - elif tc.ToolNumber != currTool and currTool is not None: # TC - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) - postlist.append((toolstring, sublist)) + # The operation is the first operation or uses a different + # ToolController as the previous operations + + else: + # Commit previous operations + commitToPostlist() + + # Queue current ToolController and operation sublist = [tc] curlist = [obj] currTool = tc.ToolNumber - toolstring = tcstring - if idx == len(self._job.Operations.Group) - 1: # Last operation. - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) + # Determine the proper string for the operation's + # ToolController + if "%T" in self._job.PostProcessorOutputFile: + toolstring = f"{tc.ToolNumber}" + else: + toolstring = re.sub(r"[^\w\d-]", "_", tc.Label) - postlist.append((toolstring, sublist)) + # Commit remaining operations + commitToPostlist() elif orderby == "Operation": Path.Log.debug("Ordering by Operation") diff --git a/src/Mod/CAM/Path/Post/UtilsArguments.py b/src/Mod/CAM/Path/Post/UtilsArguments.py index 3642bbad35..c75cd5b50a 100644 --- a/src/Mod/CAM/Path/Post/UtilsArguments.py +++ b/src/Mod/CAM/Path/Post/UtilsArguments.py @@ -82,6 +82,8 @@ def init_argument_defaults(argument_defaults: Dict[str, bool]) -> None: argument_defaults["metric_inches"] = True argument_defaults["modal"] = False argument_defaults["output_all_arguments"] = False + argument_defaults["output_machine_name"] = False + argument_defaults["output_path_labels"] = False argument_defaults["output_visible_arguments"] = False argument_defaults["show-editor"] = True argument_defaults["tlo"] = True @@ -102,6 +104,7 @@ def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None: arguments_visible["enable_machine_specific_commands"] = False arguments_visible["end_of_line_characters"] = False arguments_visible["feed-precision"] = True + arguments_visible["finish_label"] = False arguments_visible["header"] = True arguments_visible["line_number_increment"] = False arguments_visible["line_number_start"] = False @@ -110,8 +113,11 @@ def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None: arguments_visible["metric_inches"] = True arguments_visible["modal"] = True arguments_visible["output_all_arguments"] = True + arguments_visible["output_machine_name"] = False + arguments_visible["output_path_labels"] = False arguments_visible["output_visible_arguments"] = True arguments_visible["postamble"] = True + arguments_visible["post_operation"] = False arguments_visible["preamble"] = True arguments_visible["precision"] = True arguments_visible["return-to"] = False @@ -263,6 +269,17 @@ def init_shared_arguments( type=int, help=help_message, ) + if arguments_visible["finish_label"]: + help_message = ( + "The characters to use in the 'Finish operation' comment, " + f'default is "{values["FINISH_LABEL"]}"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--finish_label", + help=help_message, + ) add_flag_type_arguments( shared, argument_defaults["header"], @@ -332,6 +349,24 @@ def init_shared_arguments( "Don't output all of the available arguments", arguments_visible["output_all_arguments"], ) + add_flag_type_arguments( + shared, + argument_defaults["output_machine_name"], + "--output_machine_name", + "--no-output_machine_name", + "Output the machine name in the pre-operation information", + "Don't output the machine name in the pre-operation information", + arguments_visible["output_machine_name"], + ) + add_flag_type_arguments( + shared, + argument_defaults["output_path_labels"], + "--output_path_labels", + "--no-output_path_labels", + "Output Path labels at the beginning of each Path", + "Don't output Path labels at the beginning of each Path", + arguments_visible["output_path_labels"], + ) add_flag_type_arguments( shared, argument_defaults["output_visible_arguments"], @@ -349,6 +384,14 @@ def init_shared_arguments( else: help_message = argparse.SUPPRESS shared.add_argument("--postamble", help=help_message) + if arguments_visible["post_operation"]: + help_message = ( + f"Set commands to be issued after every operation, " + f'default is "{values["POST_OPERATION"]}"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument("--post_operation", help=help_message) if arguments_visible["preamble"]: help_message = ( f"Set commands to be issued before the first command, " @@ -817,6 +860,8 @@ def process_shared_arguments( values["END_OF_LINE_CHARACTERS"] = "\r\n" else: print("invalid end_of_line_characters, ignoring") + if args.finish_label: + values["FINISH_LABEL"] = args.finish_label if args.header: values["OUTPUT_HEADER"] = True if args.no_header: @@ -837,8 +882,18 @@ def process_shared_arguments( values["MODAL"] = True if args.no_modal: values["MODAL"] = False + if args.output_machine_name: + values["OUTPUT_MACHINE_NAME"] = True + if args.no_output_machine_name: + values["OUTPUT_MACHINE_NAME"] = False + if args.output_path_labels: + values["OUTPUT_PATH_LABELS"] = True + if args.no_output_path_labels: + values["OUTPUT_PATH_LABELS"] = False if args.postamble is not None: values["POSTAMBLE"] = args.postamble.replace("\\n", "\n") + if args.post_operation is not None: + values["POST_OPERATION"] = args.post_operation.replace("\\n", "\n") if args.preamble is not None: values["PREAMBLE"] = args.preamble.replace("\\n", "\n") if args.return_to != "": diff --git a/src/Mod/CAM/Path/Tool/docobject/__init__.py b/src/Mod/CAM/Path/Tool/docobject/__init__.py new file mode 100644 index 0000000000..34c6eeb233 --- /dev/null +++ b/src/Mod/CAM/Path/Tool/docobject/__init__.py @@ -0,0 +1,3 @@ +from .models.docobject import DetachedDocumentObject + +__all__ = ["DetachedDocumentObject"] diff --git a/src/Mod/CAM/Path/Tool/ui/__init__.py b/src/Mod/CAM/Path/Tool/docobject/models/__init__.py similarity index 100% rename from src/Mod/CAM/Path/Tool/ui/__init__.py rename to src/Mod/CAM/Path/Tool/docobject/models/__init__.py diff --git a/src/Mod/CAM/Path/Tool/toolbit/docobject.py b/src/Mod/CAM/Path/Tool/docobject/models/docobject.py similarity index 100% rename from src/Mod/CAM/Path/Tool/toolbit/docobject.py rename to src/Mod/CAM/Path/Tool/docobject/models/docobject.py diff --git a/src/Mod/CAM/Path/Tool/docobject/ui/__init__.py b/src/Mod/CAM/Path/Tool/docobject/ui/__init__.py new file mode 100644 index 0000000000..cc014a3a35 --- /dev/null +++ b/src/Mod/CAM/Path/Tool/docobject/ui/__init__.py @@ -0,0 +1,3 @@ +from .docobject import DocumentObjectEditorWidget + +__all__ = ["DocumentObjectEditorWidget"] diff --git a/src/Mod/CAM/Path/Tool/ui/docobject.py b/src/Mod/CAM/Path/Tool/docobject/ui/docobject.py similarity index 100% rename from src/Mod/CAM/Path/Tool/ui/docobject.py rename to src/Mod/CAM/Path/Tool/docobject/ui/docobject.py diff --git a/src/Mod/CAM/Path/Tool/ui/property.py b/src/Mod/CAM/Path/Tool/docobject/ui/property.py similarity index 100% rename from src/Mod/CAM/Path/Tool/ui/property.py rename to src/Mod/CAM/Path/Tool/docobject/ui/property.py diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index ec81cf3ccc..6a8d85bd98 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -33,10 +33,10 @@ from lazy_loader.lazy_loader import LazyLoader from typing import Any, List, Optional, Tuple, Type, Union, Mapping, cast from PySide.QtCore import QT_TRANSLATE_NOOP from Path.Base.Generator import toolchange -from ...assets import Asset +from ...docobject import DetachedDocumentObject +from ...assets.asset import Asset from ...camassets import cam_assets from ...shape import ToolBitShape, ToolBitShapeCustom, ToolBitShapeIcon -from ..docobject import DetachedDocumentObject from ..util import to_json, format_value diff --git a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py index 73b3f40dbd..09c310e5c1 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py +++ b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py @@ -22,13 +22,12 @@ """Widget for editing a ToolBit object.""" -from functools import partial import FreeCAD import FreeCADGui from PySide import QtGui, QtCore -from ..models.base import ToolBit from ...shape.ui.shapewidget import ShapeWidget -from ...ui.docobject import DocumentObjectEditorWidget +from ...docobject.ui import DocumentObjectEditorWidget +from ..models.base import ToolBit class ToolBitPropertiesWidget(QtGui.QWidget): diff --git a/src/Mod/CAM/PathScripts/PathUtilsGui.py b/src/Mod/CAM/PathScripts/PathUtilsGui.py index d2e9b3ed77..b73dbe350b 100644 --- a/src/Mod/CAM/PathScripts/PathUtilsGui.py +++ b/src/Mod/CAM/PathScripts/PathUtilsGui.py @@ -63,36 +63,38 @@ class PathUtilsUserInput(object): def chooseJob(self, jobs): job = None selected = FreeCADGui.Selection.getSelection() - if 1 == len(selected) and selected[0] in jobs: - job = selected[0] + if 1 == len(selected): + found = PathUtils.findParentJob(selected[0]) + if found: + return found + + modelSelected = [] + for job in jobs: + if all([o in job.Model.Group for o in selected]): + modelSelected.append(job) + if 1 == len(modelSelected): + job = modelSelected[0] else: - modelSelected = [] + modelObjectSelected = [] for job in jobs: - if all([o in job.Model.Group for o in selected]): - modelSelected.append(job) - if 1 == len(modelSelected): - job = modelSelected[0] + if all([o in job.Proxy.baseObjects(job) for o in selected]): + modelObjectSelected.append(job) + if 1 == len(modelObjectSelected): + job = modelObjectSelected[0] else: - modelObjectSelected = [] - for job in jobs: - if all([o in job.Proxy.baseObjects(job) for o in selected]): - modelObjectSelected.append(job) - if 1 == len(modelObjectSelected): - job = modelObjectSelected[0] + if modelObjectSelected: + mylist = [j.Label for j in modelObjectSelected] else: - if modelObjectSelected: - mylist = [j.Label for j in modelObjectSelected] - else: - mylist = [j.Label for j in jobs] + mylist = [j.Label for j in jobs] - jobname, result = QtGui.QInputDialog.getItem( - None, translate("Path", "Choose a CAM Job"), None, mylist - ) + jobname, result = QtGui.QInputDialog.getItem( + None, translate("Path", "Choose a CAM Job"), None, mylist + ) - if result is False: - return None - else: - job = [j for j in jobs if j.Label == jobname][0] + if result is False: + return None + else: + job = [j for j in jobs if j.Label == jobname][0] return job def createJob(self): diff --git a/src/Mod/Draft/DraftGui.py b/src/Mod/Draft/DraftGui.py index 96dc3e97c6..01b7f1f95d 100644 --- a/src/Mod/Draft/DraftGui.py +++ b/src/Mod/Draft/DraftGui.py @@ -432,7 +432,10 @@ class DraftToolBar: QtCore.QObject.connect(self.zValue,QtCore.SIGNAL("valueChanged(double)"),self.changeZValue) QtCore.QObject.connect(self.lengthValue,QtCore.SIGNAL("valueChanged(double)"),self.changeLengthValue) QtCore.QObject.connect(self.angleValue,QtCore.SIGNAL("valueChanged(double)"),self.changeAngleValue) - QtCore.QObject.connect(self.angleLock,QtCore.SIGNAL("stateChanged(int)"),self.toggleAngle) + if hasattr(self.angleLock, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.connect(self.angleLock,QtCore.SIGNAL("checkStateChanged(int)"),self.toggleAngle) + else: # Qt version < 6.7.0 + QtCore.QObject.connect(self.angleLock,QtCore.SIGNAL("stateChanged(int)"),self.toggleAngle) QtCore.QObject.connect(self.radiusValue,QtCore.SIGNAL("valueChanged(double)"),self.changeRadiusValue) QtCore.QObject.connect(self.xValue,QtCore.SIGNAL("returnPressed()"),self.checkx) QtCore.QObject.connect(self.yValue,QtCore.SIGNAL("returnPressed()"),self.checky) @@ -457,15 +460,22 @@ class DraftToolBar: QtCore.QObject.connect(self.orientWPButton,QtCore.SIGNAL("pressed()"),self.orientWP) QtCore.QObject.connect(self.undoButton,QtCore.SIGNAL("pressed()"),self.undoSegment) QtCore.QObject.connect(self.selectButton,QtCore.SIGNAL("pressed()"),self.selectEdge) - QtCore.QObject.connect(self.continueCmd,QtCore.SIGNAL("stateChanged(int)"),self.setContinue) - QtCore.QObject.connect(self.chainedModeCmd,QtCore.SIGNAL("stateChanged(int)"),self.setChainedMode) - - QtCore.QObject.connect(self.isCopy,QtCore.SIGNAL("stateChanged(int)"),self.setCopymode) - QtCore.QObject.connect(self.isSubelementMode, QtCore.SIGNAL("stateChanged(int)"), self.setSubelementMode) - - QtCore.QObject.connect(self.isRelative,QtCore.SIGNAL("stateChanged(int)"),self.setRelative) - QtCore.QObject.connect(self.isGlobal,QtCore.SIGNAL("stateChanged(int)"),self.setGlobal) - QtCore.QObject.connect(self.makeFace,QtCore.SIGNAL("stateChanged(int)"),self.setMakeFace) + if hasattr(self.continueCmd, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.connect(self.continueCmd,QtCore.SIGNAL("checkStateChanged(int)"),self.setContinue) + QtCore.QObject.connect(self.chainedModeCmd,QtCore.SIGNAL("checkStateChanged(int)"),self.setChainedMode) + QtCore.QObject.connect(self.isCopy,QtCore.SIGNAL("checkStateChanged(int)"),self.setCopymode) + QtCore.QObject.connect(self.isSubelementMode, QtCore.SIGNAL("checkStateChanged(int)"), self.setSubelementMode) + QtCore.QObject.connect(self.isRelative,QtCore.SIGNAL("checkStateChanged(int)"),self.setRelative) + QtCore.QObject.connect(self.isGlobal,QtCore.SIGNAL("checkStateChanged(int)"),self.setGlobal) + QtCore.QObject.connect(self.makeFace,QtCore.SIGNAL("checkStateChanged(int)"),self.setMakeFace) + else: # Qt version < 6.7.0 + QtCore.QObject.connect(self.continueCmd,QtCore.SIGNAL("stateChanged(int)"),self.setContinue) + QtCore.QObject.connect(self.chainedModeCmd,QtCore.SIGNAL("stateChanged(int)"),self.setChainedMode) + QtCore.QObject.connect(self.isCopy,QtCore.SIGNAL("stateChanged(int)"),self.setCopymode) + QtCore.QObject.connect(self.isSubelementMode, QtCore.SIGNAL("stateChanged(int)"), self.setSubelementMode) + QtCore.QObject.connect(self.isRelative,QtCore.SIGNAL("stateChanged(int)"),self.setRelative) + QtCore.QObject.connect(self.isGlobal,QtCore.SIGNAL("stateChanged(int)"),self.setGlobal) + QtCore.QObject.connect(self.makeFace,QtCore.SIGNAL("stateChanged(int)"),self.setMakeFace) QtCore.QObject.connect(self.baseWidget,QtCore.SIGNAL("resized()"),self.relocate) QtCore.QObject.connect(self.baseWidget,QtCore.SIGNAL("retranslate()"),self.retranslateUi) @@ -962,7 +972,12 @@ class DraftToolBar: # gui_stretch.py def setRelative(self, val=-1): if val < 0: - QtCore.QObject.disconnect(self.isRelative, + if hasattr(self.isRelative, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.disconnect(self.isRelative, + QtCore.SIGNAL("checkStateChanged(int)"), + self.setRelative) + else: # Qt version < 6.7.0 + QtCore.QObject.disconnect(self.isRelative, QtCore.SIGNAL("stateChanged(int)"), self.setRelative) if val == -1: @@ -972,9 +987,14 @@ class DraftToolBar: val = params.get_param("RelativeMode") self.isRelative.setChecked(val) self.relativeMode = val - QtCore.QObject.connect(self.isRelative, - QtCore.SIGNAL("stateChanged(int)"), - self.setRelative) + if hasattr(self.isRelative, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.disconnect(self.isRelative, + QtCore.SIGNAL("checkStateChanged(int)"), + self.setRelative) + else: # Qt version < 6.7.0 + QtCore.QObject.disconnect(self.isRelative, + QtCore.SIGNAL("stateChanged(int)"), + self.setRelative) else: params.set_param("RelativeMode", bool(val)) self.relativeMode = bool(val) diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf.ui b/src/Mod/Draft/Resources/ui/preferences-dxf.ui index 4736743fd8..45b348d707 100644 --- a/src/Mod/Draft/Resources/ui/preferences-dxf.ui +++ b/src/Mod/Draft/Resources/ui/preferences-dxf.ui @@ -394,11 +394,11 @@ Note that this can take a while! - Objects from the same layers will be joined into Draft Blocks, + Objects from the same layers will be joined into Part Compounds, turning the display faster, but making them less easily editable. - Group layers into blocks + Merge layer contents into blocks groupLayers diff --git a/src/Mod/Draft/draftgeoutils/offsets.py b/src/Mod/Draft/draftgeoutils/offsets.py index ec9e64eefe..e09ba1ddec 100644 --- a/src/Mod/Draft/draftgeoutils/offsets.py +++ b/src/Mod/Draft/draftgeoutils/offsets.py @@ -338,8 +338,6 @@ def offsetWire(wire, dvec, bind=False, occ=False, if not isinstance(basewireOffset, list): basewireOffset = [basewireOffset] - else: - basewireOffset = basewireOffset # for backward compatibility for i in range(len(edges)): # make a copy so it do not reverse the self.baseWires edges diff --git a/src/Mod/Draft/draftguitools/gui_fillets.py b/src/Mod/Draft/draftguitools/gui_fillets.py index 187c2e75ac..e36610fe59 100644 --- a/src/Mod/Draft/draftguitools/gui_fillets.py +++ b/src/Mod/Draft/draftguitools/gui_fillets.py @@ -100,8 +100,12 @@ class Fillet(gui_base_original.Creator): "Create chamfer")) self.ui.check_chamfer.show() - self.ui.check_delete.stateChanged.connect(self.set_delete) - self.ui.check_chamfer.stateChanged.connect(self.set_chamfer) + if hasattr(self.ui.check_delete, "checkStateChanged"): # Qt version >= 6.7.0 + self.ui.check_delete.checkStateChanged.connect(self.set_delete) + self.ui.check_chamfer.checkStateChanged.connect(self.set_chamfer) + else: # Qt version < 6.7.0 + self.ui.check_delete.stateChanged.connect(self.set_delete) + self.ui.check_chamfer.stateChanged.connect(self.set_chamfer) # TODO: somehow we need to set up the trackers # to show a preview of the fillet. diff --git a/src/Mod/Draft/draftguitools/gui_selectplane.py b/src/Mod/Draft/draftguitools/gui_selectplane.py index a9cdcf4959..5baae69e38 100644 --- a/src/Mod/Draft/draftguitools/gui_selectplane.py +++ b/src/Mod/Draft/draftguitools/gui_selectplane.py @@ -126,7 +126,10 @@ class Draft_SelectPlane: form.buttonPrevious.clicked.connect(self.on_click_previous) form.buttonNext.clicked.connect(self.on_click_next) form.fieldOffset.textEdited.connect(self.on_set_offset) - form.checkCenter.stateChanged.connect(self.on_set_center) + if hasattr(form.checkCenter, "checkStateChanged"): # Qt version >= 6.7.0 + form.checkCenter.checkStateChanged.connect(self.on_set_center) + else: # Qt version < 6.7.0 + form.checkCenter.stateChanged.connect(self.on_set_center) form.fieldGridSpacing.textEdited.connect(self.on_set_grid_size) form.fieldGridMainLine.valueChanged.connect(self.on_set_main_line) form.fieldGridExtension.valueChanged.connect(self.on_set_extension) diff --git a/src/Mod/Draft/drafttaskpanels/task_circulararray.py b/src/Mod/Draft/drafttaskpanels/task_circulararray.py index 31d5361b35..1c555fc998 100644 --- a/src/Mod/Draft/drafttaskpanels/task_circulararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_circulararray.py @@ -129,8 +129,12 @@ class TaskPanelCircularArray: self.form.button_reset.clicked.connect(self.reset_point) # When the checkbox changes, change the internal value - self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - self.form.checkbox_link.stateChanged.connect(self.set_link) + if hasattr(self.form.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.checkbox_fuse.checkStateChanged.connect(self.set_fuse) + self.form.checkbox_link.checkStateChanged.connect(self.set_link) + else: # Qt version < 6.7.0 + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) def accept(self): diff --git a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py index 5be612a1a1..d68ed094da 100644 --- a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py +++ b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py @@ -154,8 +154,12 @@ class TaskPanelOrthoArray: self.form.button_reset_Z.clicked.connect(lambda: self.reset_v("Z")) # When the checkbox changes, change the internal value - self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - self.form.checkbox_link.stateChanged.connect(self.set_link) + if hasattr(self.form.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.checkbox_fuse.checkStateChanged.connect(self.set_fuse) + self.form.checkbox_link.checkStateChanged.connect(self.set_link) + else: # Qt version < 6.7.0 + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) # Linear mode callbacks - only set up if the UI elements exist self.form.button_linear_mode.clicked.connect(self.toggle_linear_mode) diff --git a/src/Mod/Draft/drafttaskpanels/task_polararray.py b/src/Mod/Draft/drafttaskpanels/task_polararray.py index 267b8193c5..324a0ec0e1 100644 --- a/src/Mod/Draft/drafttaskpanels/task_polararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_polararray.py @@ -122,8 +122,12 @@ class TaskPanelPolarArray: self.form.button_reset.clicked.connect(self.reset_point) # When the checkbox changes, change the internal value - self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - self.form.checkbox_link.stateChanged.connect(self.set_link) + if hasattr(self.form.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.checkbox_fuse.checkStateChanged.connect(self.set_fuse) + self.form.checkbox_link.checkStateChanged.connect(self.set_link) + else: # Qt version < 6.7.0 + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) def accept(self): diff --git a/src/Mod/Draft/drafttaskpanels/task_shapestring.py b/src/Mod/Draft/drafttaskpanels/task_shapestring.py index 13bb81b45e..a1a182cf50 100644 --- a/src/Mod/Draft/drafttaskpanels/task_shapestring.py +++ b/src/Mod/Draft/drafttaskpanels/task_shapestring.py @@ -86,7 +86,10 @@ class ShapeStringTaskPanel: self.form.sbX.valueChanged.connect(self.set_point_x) self.form.sbY.valueChanged.connect(self.set_point_y) self.form.sbZ.valueChanged.connect(self.set_point_z) - self.form.cbGlobalMode.stateChanged.connect(self.set_global_mode) + if hasattr(self.form.cbGlobalMode.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.cbGlobalMode.checkStateChanged.connect(self.set_global_mode) + else: # Qt version < 6.7.0 + self.form.cbGlobalMode.stateChanged.connect(self.set_global_mode) self.form.pbReset.clicked.connect(self.reset_point) self.form.sbHeight.valueChanged.connect(self.set_height) self.form.leText.textEdited.connect(self.set_text) diff --git a/src/Mod/Fem/App/FemPostFilter.cpp b/src/Mod/Fem/App/FemPostFilter.cpp index d7d054f083..79da85c5a1 100644 --- a/src/Mod/Fem/App/FemPostFilter.cpp +++ b/src/Mod/Fem/App/FemPostFilter.cpp @@ -382,18 +382,21 @@ FemPostDataAlongLineFilter::FemPostDataAlongLineFilter() m_line->SetPoint2(vec2.x, vec2.y, vec2.z); m_line->SetResolution(Resolution.getValue()); + m_arclength = vtkSmartPointer::New(); + m_arclength->SetInputConnection(m_line->GetOutputPort(0)); + auto passthrough = vtkSmartPointer::New(); m_probe = vtkSmartPointer::New(); m_probe->SetSourceConnection(passthrough->GetOutputPort(0)); - m_probe->SetInputConnection(m_line->GetOutputPort()); - m_probe->SetValidPointMaskArrayName("ValidPointArray"); + m_probe->SetInputConnection(m_arclength->GetOutputPort()); m_probe->SetPassPointArrays(1); m_probe->SetPassCellArrays(1); m_probe->ComputeToleranceOff(); m_probe->SetTolerance(0.01); clip.source = passthrough; + clip.algorithmStorage.push_back(m_arclength); clip.target = m_probe; addFilterPipeline(clip, "DataAlongLine"); @@ -488,12 +491,7 @@ void FemPostDataAlongLineFilter::GetAxisData() return; } - vtkDataArray* tcoords = dset->GetPointData()->GetTCoords("Texture Coordinates"); - - const Base::Vector3d& vec1 = Point1.getValue(); - const Base::Vector3d& vec2 = Point2.getValue(); - const Base::Vector3d diff = vec1 - vec2; - double Len = diff.Length(); + vtkDataArray* alength = dset->GetPointData()->GetArray("arc_length"); for (vtkIdType i = 0; i < dset->GetNumberOfPoints(); ++i) { double value = 0; @@ -517,8 +515,7 @@ void FemPostDataAlongLineFilter::GetAxisData() } values.push_back(value); - double tcoord = tcoords->GetComponent(i, 0); - coords.push_back(tcoord * Len); + coords.push_back(alength->GetTuple1(i)); } YAxisData.setValues(values); diff --git a/src/Mod/Fem/App/FemPostFilter.h b/src/Mod/Fem/App/FemPostFilter.h index 273bd63c0f..38499d01d3 100644 --- a/src/Mod/Fem/App/FemPostFilter.h +++ b/src/Mod/Fem/App/FemPostFilter.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -181,6 +182,7 @@ protected: private: vtkSmartPointer m_line; + vtkSmartPointer m_arclength; vtkSmartPointer m_probe; }; diff --git a/src/Mod/Fem/App/FemPostFilterPy.xml b/src/Mod/Fem/App/FemPostFilterPy.xml index 28d1823f69..3fe0e4fd88 100644 --- a/src/Mod/Fem/App/FemPostFilterPy.xml +++ b/src/Mod/Fem/App/FemPostFilterPy.xml @@ -49,6 +49,13 @@ Note: Can lead to a full recompute of the whole pipeline, hence best to call thi Returns the names of all scalar fields available on this filter's input. Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. + + + + + + +Returns the filters vtk algorithm currently used as output (the one generating the Data field). Note that the output algorithm may change depending on filter settings. " diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index dddf9048e1..69a5d8f23e 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -38,6 +38,7 @@ #ifdef FC_USE_VTK_PYTHON #include #include +#include #endif // BUILD_FEM_VTK using namespace Fem; @@ -129,6 +130,9 @@ PyObject* FemPostFilterPy::getInputData(PyObject* args) case VTK_UNSTRUCTURED_GRID: copy = vtkUnstructuredGrid::New(); break; + case VTK_POLY_DATA: + copy = vtkPolyData::New(); + break; default: PyErr_SetString(PyExc_TypeError, "cannot return datatype object; not unstructured grid"); @@ -183,6 +187,25 @@ PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args) return Py::new_reference_to(list); } +PyObject* FemPostFilterPy::getOutputAlgorithm(PyObject* args) +{ +#ifdef FC_USE_VTK_PYTHON + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the algorithm + auto algorithm = getFemPostFilterPtr()->getFilterOutput(); + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); + + return Py::new_reference_to(py_algorithm); +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/FemPostObjectPy.xml b/src/Mod/Fem/App/FemPostObjectPy.xml index cc4d4dacef..8f5603234b 100644 --- a/src/Mod/Fem/App/FemPostObjectPy.xml +++ b/src/Mod/Fem/App/FemPostObjectPy.xml @@ -23,6 +23,13 @@ filename: str File extension is automatically detected from data type. + + + getDataset() -> vtkDataSet + +Returns the current output dataset. For normal filters this is equal to the objects Data property output. However, a pipelines Data property could store multiple frames, and hence Data can be of type vtkCompositeData, which is not a vtkDataset. To simplify implementations this function always returns a vtkDataSet, and for a pipeline it will be the dataset of the currently selected frame. Note that the returned value could be None, if no data is set at all. + + diff --git a/src/Mod/Fem/App/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp index 81ee5119ac..8b242abcf7 100644 --- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp +++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp @@ -29,6 +29,10 @@ #include "FemPostObjectPy.h" #include "FemPostObjectPy.cpp" +#ifdef FC_USE_VTK_PYTHON +#include +#include +#endif // BUILD_FEM_VTK using namespace Fem; @@ -55,6 +59,27 @@ PyObject* FemPostObjectPy::writeVTK(PyObject* args) Py_Return; } +PyObject* FemPostObjectPy::getDataSet(PyObject* args) +{ +#ifdef FC_USE_VTK_PYTHON + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the dataset + auto dataset = getFemPostObjectPtr()->getDataSet(); + if (dataset) { + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(dataset); + return Py::new_reference_to(py_algorithm); + } + return Py_None; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostObjectPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/FemPostPipeline.h b/src/Mod/Fem/App/FemPostPipeline.h index 15d9149705..d590915cb4 100644 --- a/src/Mod/Fem/App/FemPostPipeline.h +++ b/src/Mod/Fem/App/FemPostPipeline.h @@ -118,6 +118,12 @@ public: unsigned int getFrameNumber(); std::vector getFrameValues(); + // output algorithm handling + vtkSmartPointer getOutputAlgorithm() + { + return m_source_algorithm; + } + protected: void onChanged(const App::Property* prop) override; bool allowObject(App::DocumentObject* obj) override; diff --git a/src/Mod/Fem/App/FemPostPipelinePy.xml b/src/Mod/Fem/App/FemPostPipelinePy.xml index c71981393b..ab15496be9 100644 --- a/src/Mod/Fem/App/FemPostPipelinePy.xml +++ b/src/Mod/Fem/App/FemPostPipelinePy.xml @@ -71,5 +71,12 @@ Load a single result object or create a multiframe result by loading multiple re Change name of data arrays + + + +Returns the pipeline vtk algorithm, which generates the data passed to the pipelines filters. Note that the output algorithm may change depending on pipeline settings. + + + " diff --git a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp index be59cdefb2..2c493074e6 100644 --- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp +++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp @@ -34,6 +34,10 @@ #include "FemPostPipelinePy.cpp" // clang-format on +#ifdef FC_USE_VTK_PYTHON +#include +#endif // BUILD_FEM_VTK + using namespace Fem; @@ -313,6 +317,25 @@ PyObject* FemPostPipelinePy::renameArrays(PyObject* args) Py_Return; } +PyObject* FemPostPipelinePy::getOutputAlgorithm(PyObject* args) +{ +#ifdef FC_USE_VTK_PYTHON + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the algorithm + auto algorithm = getFemPostPipelinePtr()->getOutputAlgorithm(); + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); + + return Py::new_reference_to(py_algorithm); +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostPipelinePy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp index 3ab5e1cbbf..83648350ad 100644 --- a/src/Mod/Fem/App/PropertyPostDataObject.cpp +++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp @@ -32,8 +32,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -243,6 +246,9 @@ void PropertyPostDataObject::createDataObjectByExternalType(vtkSmartPointer::New(); break; + case VTK_TABLE: + m_dataObject = vtkSmartPointer::New(); + break; default: throw Base::TypeError("Unsupported VTK data type"); }; @@ -313,6 +319,9 @@ void PropertyPostDataObject::Save(Base::Writer& writer) const case VTK_MULTIBLOCK_DATA_SET: extension = "zip"; break; + case VTK_TABLE: + extension = ".vtt"; + break; default: break; }; @@ -382,13 +391,16 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const xmlWriter = vtkSmartPointer::New(); xmlWriter->SetInputDataObject(m_dataObject); xmlWriter->SetFileName(datafile.filePath().c_str()); - xmlWriter->SetDataModeToBinary(); + } + else if (m_dataObject->IsA("vtkTable")) { + xmlWriter = vtkSmartPointer::New(); + xmlWriter->SetInputDataObject(m_dataObject); + xmlWriter->SetFileName(fi.filePath().c_str()); } else { xmlWriter = vtkSmartPointer::New(); xmlWriter->SetInputDataObject(m_dataObject); xmlWriter->SetFileName(fi.filePath().c_str()); - xmlWriter->SetDataModeToBinary(); #ifdef VTK_CELL_ARRAY_V2 // Looks like an invalid data object that causes a crash with vtk9 @@ -399,6 +411,7 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const } #endif } + xmlWriter->SetDataModeToBinary(); if (xmlWriter->Write() != 1) { // Note: Do NOT throw an exception here because if the tmp. file could @@ -481,6 +494,9 @@ void PropertyPostDataObject::RestoreDocFile(Base::Reader& reader) else if (extension == "vti") { xmlReader = vtkSmartPointer::New(); } + else if (extension == "vtt") { + xmlReader = vtkSmartPointer::New(); + } else if (extension == "zip") { // first unzip the file into a datafolder diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index a0390b1035..aca1b443f0 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -214,9 +214,15 @@ SET(FemObjects_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemObjects_SRCS - ${FemObjects_SRCS} + list(APPEND FemObjects_SRCS + femobjects/base_fempostextractors.py + femobjects/base_fempostvisualizations.py femobjects/post_glyphfilter.py + femobjects/post_extract1D.py + femobjects/post_extract2D.py + femobjects/post_histogram.py + femobjects/post_lineplot.py + femobjects/post_table.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -597,6 +603,7 @@ SET(FemGuiTaskPanels_SRCS femtaskpanels/__init__.py femtaskpanels/base_femtaskpanel.py femtaskpanels/base_femlogtaskpanel.py + femtaskpanels/base_fempostpanel.py femtaskpanels/task_constraint_bodyheatsource.py femtaskpanels/task_constraint_centrif.py femtaskpanels/task_constraint_currentdensity.py @@ -625,9 +632,12 @@ SET(FemGuiTaskPanels_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemGuiTaskPanels_SRCS - ${FemGuiTaskPanels_SRCS} + list(APPEND FemGuiTaskPanels_SRCS femtaskpanels/task_post_glyphfilter.py + femtaskpanels/task_post_histogram.py + femtaskpanels/task_post_lineplot.py + femtaskpanels/task_post_table.py + femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -641,9 +651,18 @@ SET(FemGuiUtils_SRCS femguiutils/disambiguate_solid_selection.py femguiutils/migrate_gui.py femguiutils/selection_widgets.py - femguiutils/vtk_module_handling.py ) +if(BUILD_FEM_VTK_PYTHON) + list(APPEND FemGuiUtils_SRCS + femguiutils/vtk_module_handling.py + femguiutils/vtk_table_view.py + femguiutils/data_extraction.py + femguiutils/extract_link_view.py + femguiutils/post_visualization.py + ) +endif(BUILD_FEM_VTK_PYTHON) + SET(FemGuiViewProvider_SRCS femviewprovider/__init__.py femviewprovider/view_base_femconstraint.py @@ -683,9 +702,13 @@ SET(FemGuiViewProvider_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemGuiViewProvider_SRCS - ${FemGuiViewProvider_SRCS} + list(APPEND FemGuiViewProvider_SRCS + femviewprovider/view_base_fempostextractors.py + femviewprovider/view_base_fempostvisualization.py femviewprovider/view_post_glyphfilter.py + femviewprovider/view_post_histogram.py + femviewprovider/view_post_lineplot.py + femviewprovider/view_post_table.py ) endif(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 7abc3b4df0..7729f8af55 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -291,6 +291,8 @@ if(BUILD_FEM_VTK) SphereWidget.ui TaskPostBoxes.h TaskPostBoxes.cpp + TaskPostExtraction.h + TaskPostExtraction.cpp TaskPostCalculator.ui TaskPostClip.ui TaskPostContours.ui @@ -440,6 +442,16 @@ SET(FemGuiPythonUI_SRCS Resources/ui/SolverCalculiX.ui Resources/ui/SolverCcxTools.ui Resources/ui/TaskPostGlyph.ui + Resources/ui/TaskPostExtraction.ui + Resources/ui/TaskPostHistogram.ui + Resources/ui/TaskPostLineplot.ui + Resources/ui/PostHistogramFieldViewEdit.ui + Resources/ui/PostHistogramFieldAppEdit.ui + Resources/ui/PostHistogramIndexAppEdit.ui + Resources/ui/PostLineplotFieldViewEdit.ui + Resources/ui/PostLineplotFieldAppEdit.ui + Resources/ui/PostLineplotIndexAppEdit.ui + Resources/ui/PostTableFieldViewEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index 7e15fdf17e..351dad3e48 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -86,6 +86,11 @@ icons/FEM_PostFrames.svg icons/FEM_PostBranchFilter.svg icons/FEM_PostPipelineFromResult.svg + icons/FEM_PostLineplot.svg + icons/FEM_PostHistogram.svg + icons/FEM_PostSpreadsheet.svg + icons/FEM_PostField.svg + icons/FEM_PostIndex.svg icons/FEM_ResultShow.svg icons/FEM_ResultsPurge.svg diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg new file mode 100644 index 0000000000..a93343fd25 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg new file mode 100644 index 0000000000..333e138d83 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg @@ -0,0 +1,69 @@ + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg new file mode 100644 index 0000000000..9198dcdba0 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg new file mode 100644 index 0000000000..6e90515778 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg @@ -0,0 +1,46 @@ + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg new file mode 100644 index 0000000000..b8453c0756 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui new file mode 100644 index 0000000000..8e611e7790 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -0,0 +1,78 @@ + + + Form + + + + 0 + 0 + 317 + 118 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Frames: + + + + + + + One field for each frames + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui new file mode 100644 index 0000000000..5fe4a7d3dc --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -0,0 +1,167 @@ + + + PostHistogramEdit + + + + 0 + 0 + 278 + 110 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + Outline draw style (None does not draw outlines) + + + + None + + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Hatch pattern + + + + None + + + + + + + + Lines: + + + + + + + + 0 + 0 + + + + Density of hatch pattern + + + 1 + + + + + + + Bars: + + + + + + + + + + Legend: + + + + + + + + 0 + 0 + + + + Color of all lines (bar outline and hatches) + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + + Legend + BarColor + Hatch + HatchDensity + LineColor + LineStyle + LineWidth + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui new file mode 100644 index 0000000000..496f42229b --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui @@ -0,0 +1,84 @@ + + + Form + + + + 0 + 0 + 261 + 110 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + 999999999 + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui new file mode 100644 index 0000000000..b0d1830852 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui @@ -0,0 +1,101 @@ + + + Form + + + + 0 + 0 + 271 + 174 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + X Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Y Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Frames: + + + + + + + One Y field for each frames + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui new file mode 100644 index 0000000000..f197016d12 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui @@ -0,0 +1,151 @@ + + + PostHistogramEdit + + + + 0 + 0 + 274 + 114 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Marker: + + + + + + + + 0 + 0 + + + + Hatch pattern + + + + None + + + + + + + + + + + Legend: + + + + + + + + 0 + 0 + + + + Outline draw style (None does not draw outlines) + + + + None + + + + + + + + Line: + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + 0 + 0 + + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + + Legend + Color + LineStyle + MarkerStyle + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui new file mode 100644 index 0000000000..ba4ab0ead3 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui @@ -0,0 +1,85 @@ + + + Form + + + + 0 + 0 + 310 + 108 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Y Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + 99999999 + + + + + + + Index + YField + YComponent + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui new file mode 100644 index 0000000000..6b3000248a --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui @@ -0,0 +1,50 @@ + + + PostHistogramEdit + + + + 0 + 0 + 279 + 38 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + Name: + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui new file mode 100644 index 0000000000..8f082da23f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui @@ -0,0 +1,54 @@ + + + TaskPostExtraction + + + + 0 + 0 + 515 + 36 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Data Summary + + + + + + + Show Data + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui new file mode 100644 index 0000000000..a753071f9a --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui @@ -0,0 +1,241 @@ + + + TaskPostGlyph + + + + 0 + 0 + 343 + 498 + + + + Glyph settings + + + Qt::LeftToRight + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + The form of the glyph + + + Bins + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + 2 + + + 1000 + + + + + + + Which vector field is used to orient the glyphs + + + Type + + + + + + + + 0 + 0 + + + + Which vector field is used to orient the glyphs + + + + None + + + + + + + + Cumulative + + + + + + + + + Legend + + + + + + Qt::LeftToRight + + + Show + + + + + + + + 0 + 0 + + + + + + + + + + + + 1 + 0 + + + + Labels + + + false + + + false + + + false + + + + + + + + + Y Axis + + + + + + + + + + If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components + + + X Axis + + + + + + + A constant multiplier the glyphs are scaled with + + + Title + + + + + + + + + + + + + Visuals + + + + + + 1.000000000000000 + + + 0.050000000000000 + + + + + + + Hatch Line Width + + + + + + + Bar width + + + + + + + + + + + + + Bins + Type + Cumulative + LegendShow + LegendPos + Title + XLabel + YLabel + BarWidth + HatchWidth + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui new file mode 100644 index 0000000000..bec95e063f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui @@ -0,0 +1,181 @@ + + + TaskPostGlyph + + + + 0 + 0 + 302 + 302 + + + + Glyph settings + + + Qt::LeftToRight + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + The form of the glyph + + + Grid + + + + + + + Show + + + + + + + Qt::LeftToRight + + + Show + + + + + + + Legend + + + + + + + + 0 + 0 + + + + + + + + Which vector field is used to orient the glyphs + + + Scale + + + + + + + + 0 + 0 + + + + Which vector field is used to orient the glyphs + + + + None + + + + + + + + + + + 1 + 0 + + + + Labels + + + false + + + false + + + false + + + + + + + + + Y Axis + + + + + + + + + + If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components + + + X Axis + + + + + + + A constant multiplier the glyphs are scaled with + + + Title + + + + + + + + + + + + + Grid + LegendShow + LegendPos + Scale + Title + XLabel + YLabel + + + + diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 805977f6d2..e50bb0bf98 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -64,7 +64,6 @@ #include "ui_TaskPostFrames.h" #include "ui_TaskPostBranch.h" - #include "FemSettings.h" #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" @@ -72,7 +71,6 @@ #include "ViewProviderFemPostObject.h" #include "ViewProviderFemPostBranchFilter.h" - using namespace FemGui; using namespace Gui; @@ -214,9 +212,18 @@ TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view, setWindowTitle(title); setWindowIcon(icon); m_icon = icon; + + m_connection = + m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, + this, + boost::placeholders::_1, + boost::placeholders::_2)); } -TaskPostWidget::~TaskPostWidget() = default; +TaskPostWidget::~TaskPostWidget() +{ + m_connection.disconnect(); +}; bool TaskPostWidget::autoApply() { @@ -256,6 +263,14 @@ void TaskPostWidget::updateEnumerationList(App::PropertyEnumeration& prop, QComb box->setCurrentIndex(index); } +void TaskPostWidget::handlePropertyChange(const App::DocumentObject& obj, const App::Property& prop) +{ + if (auto postobj = m_object.get()) { + if (&prop == &postobj->Data) { + this->onPostDataChanged(postobj); + } + } +} // *************************************************************************** // simulation dialog for the TaskView @@ -393,6 +408,24 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) } } +void TaskDlgPost::processCollapsedWidgets() +{ + + for (auto& widget : Content) { + auto* task_box = dynamic_cast(widget); + if (!task_box) { + continue; + } + // get the task widget and check if it is a post widget + auto* taskwidget = task_box->groupLayout()->itemAt(0)->widget(); + auto* post_widget = dynamic_cast(taskwidget); + if (!post_widget || !post_widget->initiallyCollapsed()) { + continue; + } + post_widget->setGeometry(QRect(QPoint(0, 0), post_widget->sizeHint())); + task_box->hideGroupBox(); + } +} // *************************************************************************** // box to set the coloring @@ -475,7 +508,6 @@ void TaskPostDisplay::onTransparencyValueChanged(int i) void TaskPostDisplay::applyPythonCode() {} - // *************************************************************************** // functions TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent) @@ -557,6 +589,11 @@ void TaskPostFrames::applyPythonCode() // we apply the views widgets python code } +bool TaskPostFrames::initiallyCollapsed() +{ + + return (ui->FrameTable->rowCount() == 0); +} // *************************************************************************** // in the following, the different filters sorted alphabetically diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index d37742dd27..816dafb080 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -42,6 +42,7 @@ class Ui_TaskPostWarpVector; class Ui_TaskPostCut; class Ui_TaskPostFrames; class Ui_TaskPostBranch; +class Ui_TaskPostExtraction; class SoFontStyle; class SoText2; @@ -155,6 +156,12 @@ public: // executed when the apply button is pressed in the task dialog virtual void apply() {}; + // returns if the widget shall be collapsed when opening the task dialog + virtual bool initiallyCollapsed() + { + return false; + }; + protected: App::DocumentObject* getObject() const { @@ -187,10 +194,15 @@ protected: static void updateEnumerationList(App::PropertyEnumeration&, QComboBox* box); + // object update handling + void handlePropertyChange(const App::DocumentObject&, const App::Property&); + virtual void onPostDataChanged(Fem::FemPostObject*) {}; + private: QPixmap m_icon; App::DocumentObjectWeakPtrT m_object; Gui::ViewProviderWeakPtrT m_view; + boost::signals2::connection m_connection; }; @@ -229,6 +241,9 @@ public: /// returns for Close and Help button QDialogButtonBox::StandardButtons getStandardButtons() const override; + /// makes sure all widgets are collapsed, if they want to be + void processCollapsedWidgets(); + protected: void recompute(); @@ -267,7 +282,6 @@ private: std::unique_ptr ui; }; - // *************************************************************************** // functions class ViewProviderFemPostFunction; @@ -295,6 +309,8 @@ public: void applyPythonCode() override; + bool initiallyCollapsed() override; + private: void setupConnections(); void onSelectionChanged(); diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp new file mode 100644 index 0000000000..e61033957c --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -0,0 +1,181 @@ +/*************************************************************************** + * Copyright (c) 2015 Stefan Tröger * + * * + * 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 "ViewProviderFemPostObject.h" +#include "TaskPostExtraction.h" + +using namespace FemGui; +using namespace Gui; + + +// *************************************************************************** +// box to handle data extractions + +TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent) + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(), parent) +{ + // we load the python implementation, and try to get the widget from it, to add + // directly our widget + + setWindowTitle(tr("Data and extractions")); + + Base::PyGILStateLocker lock; + + + try { + Py::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true); + if (mod.isNull()) { + Base::Console().error("Unable to import data extraction widget\n"); + return; + } + + Py::Callable method(mod.getAttr(std::string("DataExtraction"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(view->getPyObject())); + m_panel = Py::Object(method.apply(args)); + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.reportException(); + } + + if (m_panel.hasAttr(std::string("widget"))) { + Py::Object pywidget(m_panel.getAttr(std::string("widget"))); + + Gui::PythonWrapper wrap; + if (wrap.loadCoreModule()) { + if (auto* widget = qobject_cast(wrap.toQObject(pywidget))) { + // finally we have the usable QWidget. Add to us! + + auto layout = new QVBoxLayout(); + layout->addWidget(widget); + setLayout(layout); + return; + } + } + } + + // if we are here something went wrong! + Base::Console().error("Unable to import data extraction widget\n"); +}; + +TaskPostExtraction::~TaskPostExtraction() +{ + + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("widget"))) { + m_panel.setAttr(std::string("widget"), Py::None()); + } + m_panel = Py::None(); + } + catch (Py::AttributeError& e) { + e.clear(); + } +} + +void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj) +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("onPostDataChanged"))) { + Py::Callable method(m_panel.getAttr(std::string("onPostDataChanged"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(obj->getPyObject())); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.reportException(); + } +}; + +bool TaskPostExtraction::isGuiTaskOnly() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("isGuiTaskOnly"))) { + Py::Callable method(m_panel.getAttr(std::string("isGuiTaskOnly"))); + auto result = Py::Boolean(method.apply()); + return result.as_bool(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.reportException(); + } + + return false; +}; + +void TaskPostExtraction::apply() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("apply"))) { + Py::Callable method(m_panel.getAttr(std::string("apply"))); + method.apply(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.reportException(); + } +} + +bool TaskPostExtraction::initiallyCollapsed() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("initiallyCollapsed"))) { + Py::Callable method(m_panel.getAttr(std::string("initiallyCollapsed"))); + auto result = Py::Boolean(method.apply()); + return result.as_bool(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.reportException(); + } + + return false; +} + +#include "moc_TaskPostExtraction.cpp" diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.h b/src/Mod/Fem/Gui/TaskPostExtraction.h new file mode 100644 index 0000000000..5fe2518760 --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.h @@ -0,0 +1,68 @@ +/*************************************************************************** + * Copyright (c) 2025 Stefan Tröger * + * * + * 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_TaskPostExtraction_H +#define GUI_TASKVIEW_TaskPostExtraction_H + +#include +#include +#include +#include + +#include + +#include "TaskPostBoxes.h" + +#include +#include + +class Ui_TaskPostExtraction; + + +namespace FemGui +{ + +// *************************************************************************** +// box to handle data extractions: It is implemented in python, the c++ +// code is used to access it and manage it for the c++ task panels +class TaskPostExtraction: public TaskPostWidget +{ + Q_OBJECT + +public: + explicit TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent = nullptr); + ~TaskPostExtraction(); + +protected: + bool isGuiTaskOnly() override; + void apply() override; + void onPostDataChanged(Fem::FemPostObject* obj) override; + bool initiallyCollapsed() override; + +private: + Py::Object m_panel; +}; + + +} // namespace FemGui + +#endif // GUI_TASKVIEW_TaskPostExtraction_H diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/TaskPostExtraction.ui new file mode 100644 index 0000000000..7387ffb7de --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.ui @@ -0,0 +1,135 @@ + + + TaskPostExtraction + + + + 0 + 0 + 375 + 302 + + + + Form + + + + + + + + Data Summary + + + + + + + Show Data + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 20 + 10 + + + + + + + + + + + 0 + 0 + + + + Data used in: + + + Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + Add data to + + + + + + + + + 0 + 0 + + + + + Create and add + + + + + + + + + + true + + + + + 0 + 0 + 359 + 188 + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index 4cbacb5cad..f6c60491b8 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -32,6 +32,9 @@ #include "ViewProviderFemPostFilter.h" #include "ViewProviderFemPostFilterPy.h" +#ifdef FC_USE_VTK_PYTHON +#include "TaskPostExtraction.h" +#endif using namespace FemGui; @@ -89,6 +92,12 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); auto panel = new TaskPostDataAlongLine(this); dlg->addTaskBox(panel->getIcon(), panel); + +#ifdef FC_USE_VTK_PYTHON + // and the extraction + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } @@ -138,6 +147,12 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); auto panel = new TaskPostDataAtPoint(this); dlg->addTaskBox(panel->getIcon(), panel); + +#ifdef FC_USE_VTK_PYTHON + // and the extraction + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml index c41959e24d..9a41e8e972 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml @@ -20,5 +20,10 @@ Returns the display option task panel for a post processing edit task dialog. + + + Returns the data extraction task panel for a post processing edit task dialog. + + diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp index c922d76840..5683ce2467 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp @@ -27,6 +27,9 @@ #include #include "ViewProviderFemPostFilter.h" #include "TaskPostBoxes.h" +#ifdef FC_USE_VTK_PYTHON +#include "TaskPostExtraction.h" +#endif // inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml) #include "ViewProviderFemPostFilterPy.h" #include "ViewProviderFemPostFilterPy.cpp" @@ -60,6 +63,29 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args) return nullptr; } +PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args) +{ +#ifdef FC_USE_VTK_PYTHON + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + auto panel = new TaskPostExtraction(getViewProviderFemPostObjectPtr()); + + Gui::PythonWrapper wrap; + if (wrap.loadCoreModule()) { + return Py::new_reference_to(wrap.fromQWidget(panel)); + } + + PyErr_SetString(PyExc_TypeError, "creating the panel failed"); + return nullptr; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 0d44e6486e..9205e2d708 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -67,6 +67,9 @@ #include #include "TaskPostBoxes.h" +#ifdef FC_USE_VTK_PYTHON +#include "TaskPostExtraction.h" +#endif #include "ViewProviderAnalysis.h" #include "ViewProviderFemPostObject.h" @@ -1006,6 +1009,7 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) postDlg = new TaskDlgPost(this); setupTaskDialog(postDlg); postDlg->connectSlots(); + postDlg->processCollapsedWidgets(); Gui::Control().showDialog(postDlg); } @@ -1019,8 +1023,13 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) { assert(dlg->getView() == this); - auto panel = new TaskPostDisplay(this); - dlg->addTaskBox(panel->windowIcon().pixmap(32), panel); + auto dispPanel = new TaskPostDisplay(this); + dlg->addTaskBox(dispPanel->windowIcon().pixmap(32), dispPanel); + +#ifdef FC_USE_VTK_PYTHON + auto extrPanel = new TaskPostExtraction(this); + dlg->addTaskBox(extrPanel->windowIcon().pixmap(32), extrPanel); +#endif } void ViewProviderFemPostObject::unsetEdit(int ModNum) diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 3ae3219705..31f396fbe9 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -214,7 +214,11 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterDataAtPoint" << "FEM_PostFilterCalculator" << "Separator" - << "FEM_PostCreateFunctions"; + << "FEM_PostCreateFunctions" +#ifdef FC_USE_VTK_PYTHON + << "FEM_PostVisualization" +#endif + ; #endif Gui::ToolBarItem* utils = new Gui::ToolBarItem(root); @@ -366,7 +370,11 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "FEM_PostFilterDataAtPoint" << "FEM_PostFilterCalculator" << "Separator" - << "FEM_PostCreateFunctions"; + << "FEM_PostCreateFunctions" +#ifdef FC_USE_VTK_PYTHON + << "FEM_PostVisualization" +#endif + ; #endif Gui::MenuItem* utils = new Gui::MenuItem; diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 8ac271d379..e7d0a2ada7 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -81,9 +81,10 @@ class FemWorkbench(Workbench): False if femcommands.commands.__name__ else True # check vtk version to potentially find missmatchs - from femguiutils.vtk_module_handling import vtk_module_handling + if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + from femguiutils.vtk_module_handling import vtk_module_handling - vtk_module_handling() + vtk_module_handling() def GetClassName(self): # see https://forum.freecad.org/viewtopic.php?f=10&t=43300 diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c05ecc6108..2bd6e74056 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -686,6 +686,141 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): return obj +def makePostLineplot(doc, name="Lineplot"): + """makePostLineplot(document, [name]): + creates a FEM post processing line plot + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_lineplot + + post_lineplot.PostLineplot(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + + view_post_lineplot.VPPostLineplot(obj.ViewObject) + return obj + + +def makePostLineplotFieldData(doc, name="FieldData2D"): + """makePostLineplotFieldData(document, [name]): + creates a FEM post processing data extractor for 2D Field data + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_lineplot + + post_lineplot.PostLineplotFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + + view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject) + return obj + + +def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"): + """makePostLineplotIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 2D index data + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_lineplot + + post_lineplot.PostLineplotIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + + view_post_lineplot.VPPostLineplotIndexOverFrames(obj.ViewObject) + return obj + + +def makePostHistogram(doc, name="Histogram"): + """makePostHistogram(document, [name]): + creates a FEM post processing histogram plot + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogram(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + + view_post_histogram.VPPostHistogram(obj.ViewObject) + return obj + + +def makePostHistogramFieldData(doc, name="FieldData1D"): + """makePostHistogramFieldData(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogramFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + + view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) + return obj + + +def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): + """makePostHistogramIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogramIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + + view_post_histogram.VPPostHistogramIndexOverFrames(obj.ViewObject) + return obj + + +def makePostTable(doc, name="Table"): + """makePostTable(document, [name]): + creates a FEM post processing histogram plot + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_table + + post_table.PostTable(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + + view_post_table.VPPostTable(obj.ViewObject) + return obj + + +def makePostTableFieldData(doc, name="FieldData1D"): + """makePostTableFieldData(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_table + + post_table.PostTableFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + + view_post_table.VPPostTableFieldData(obj.ViewObject) + return obj + + +def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"): + """makePostTableIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_table + + post_table.PostTableIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + + view_post_table.VPPostTableIndexOverFrames(obj.ViewObject) + return obj + + # ********* solver objects *********************************************************************** def makeEquationDeformation(doc, base_solver=None, name="Deformation"): """makeEquationDeformation(document, [base_solver], [name]): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 0c61dcff2b..5d662074be 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1289,3 +1289,12 @@ FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88()) if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) + + # setup all visualization commands (register by importing) + import femobjects.post_lineplot + import femobjects.post_histogram + import femobjects.post_table + + from femguiutils import post_visualization + + post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index d57764e54b..bb2edc3e05 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -34,7 +34,6 @@ import FreeCAD from femtools.femutils import expandParentObject from femtools.femutils import is_of_type -from femguiutils.vtk_module_handling import vtk_compatibility_abort if FreeCAD.GuiUp: from PySide import QtCore @@ -381,6 +380,8 @@ class CommandManager: # and the selobj is expanded in the tree to see the added obj # check if we should use python filter + from femguiutils.vtk_module_handling import vtk_compatibility_abort + if vtk_compatibility_abort(True): return diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py new file mode 100644 index 0000000000..dfe0cea7f8 --- /dev/null +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -0,0 +1,163 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing ldata view and extraction widget" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package data_extraction +# \ingroup FEM +# \brief A widget for data extraction. Used in the PostObject task panel. + +from . import vtk_table_view + +from PySide import QtCore, QtGui + +from vtkmodules.vtkCommonCore import vtkVersion +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents + +if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3: + from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter +else: + from vtkmodules.vtkInfovisCore import vtkDataObjectToTable + + +import FreeCAD +import FreeCADGui + +import femobjects.base_fempostextractors as extr +from femtaskpanels.base_fempostpanel import _BasePostTaskPanel + +from . import extract_link_view + +ExtractLinkView = extract_link_view.ExtractLinkView + + +class DataExtraction(_BasePostTaskPanel): + # The class is not a widget itself, but provides a widget. It implements + # all required callbacks for the widget and the task dialog. + # Note: This object is created and used from c++! See PostTaskExtraction + + def __init__(self, vobj): + + super().__init__(vobj.Object) + + self.ViewObject = vobj + self.Object = vobj.Object + + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostExtraction.ui" + ) + + # connect all signals as required + self.widget.Data.clicked.connect(self.showData) + self.widget.Summary.clicked.connect(self.showSummary) + + # setup the data models + self.data_model = vtk_table_view.VtkTableModel() + self.summary_model = vtk_table_view.VtkTableSummaryModel() + + # generate the data + self.onPostDataChanged(self.Object) + + # setup the extraction widget + self._extraction_view = ExtractLinkView(self.Object, True, self) + self.widget.layout().addSpacing(self.widget.Data.size().height() / 3) + self.widget.layout().addWidget(self._extraction_view) + self._extraction_view.repopulate() + + @QtCore.Slot() + def showData(self): + + dialog = QtGui.QDialog(self.widget) + dialog.setWindowTitle(f"Data of {self.Object.Label}") + widget = vtk_table_view.VtkTableView(self.data_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0, 0, 0, 0) + + dialog.setLayout(layout) + dialog.resize(1500, 900) + dialog.show() + + @QtCore.Slot() + def showSummary(self): + + dialog = QtGui.QDialog(self.widget) + dialog.setWindowTitle(f"Data summary of {self.Object.Label}") + widget = vtk_table_view.VtkTableView(self.summary_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0, 0, 0, 0) + + dialog.setLayout(layout) + dialog.resize(600, 900) + dialog.show() + + def isGuiTaskOnly(self): + # If all panels return true it omits the Apply button in the dialog + return True + + def onPostDataChanged(self, obj): + + algo = obj.getOutputAlgorithm() + if not algo: + self.data_model.setTable(vtkTable()) + + if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3: + filter = vtkAttributeDataToTableFilter() + else: + filter = vtkDataObjectToTable() + filter.SetFieldType(vtkDataObjectToTable.POINT_DATA) + + filter.SetInputConnection(0, algo.GetOutputPort(0)) + filter.Update() + table = filter.GetOutputDataObject(0) + + # add the points + points = algo.GetOutputDataObject(0).GetPoints().GetData() + table.AddColumn(points) + + # split the components + splitter = vtkSplitColumnComponents() + splitter.SetNamingModeToNamesWithParens() + splitter.SetInputData(0, table) + + splitter.Update() + table = splitter.GetOutputDataObject(0) + + self.data_model.setTable(table) + self.summary_model.setTable(table) + + def apply(self): + pass + + def initiallyCollapsed(self): + # if we do not have any extractions to show we hide initially to remove clutter + + for obj in self.Object.InList: + if extr.is_extractor_object(obj): + return False + + return True diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py new file mode 100644 index 0000000000..eec8ba6927 --- /dev/null +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -0,0 +1,715 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing view for summarizing extractor links" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package data_extraction +# \ingroup FEM +# \brief A widget that shows summaries of all available links to extractors + +from PySide import QtGui, QtCore + +import femobjects.base_fempostextractors as extr +import femobjects.base_fempostvisualizations as vis + +import FreeCAD +import FreeCADGui + +from . import post_visualization as pv + +translate = FreeCAD.Qt.translate + +# a model showing available visualizations and possible extractions +# ################################################################# + + +def build_new_visualization_tree_model(): + # model that shows all options to create new visualizations + + model = QtGui.QStandardItemModel() + + visualizations = pv.get_registered_visualizations() + for vis_name in visualizations: + vis_icon = FreeCADGui.getIcon(visualizations[vis_name].icon) + vis_item = QtGui.QStandardItem(vis_icon, translate("FEM", "New {}").format(vis_name)) + vis_item.setFlags(QtGui.Qt.ItemIsEnabled) + vis_item.setData(visualizations[vis_name]) + + for ext in visualizations[vis_name].extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_name) + ext_item = QtGui.QStandardItem(icon, translate("FEM", "with {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) + ext_item.setData(ext) + vis_item.appendRow(ext_item) + model.appendRow(vis_item) + + return model + + +def build_add_to_visualization_tree_model(): + # model that shows all possible visualization objects to add data to + + visualizations = pv.get_registered_visualizations() + model = QtGui.QStandardItemModel() + + for obj in FreeCAD.ActiveDocument.Objects: + if obj.isDerivedFrom("Fem::FemAnalysis"): + ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label) + ana_item.setFlags(QtGui.Qt.ItemIsEnabled) + + # check all children it it is a visualization + for child in obj.Group: + if vis.is_visualization_object(child): + + vis_item = QtGui.QStandardItem(child.ViewObject.Icon, child.Label) + vis_type = vis.get_visualization_type(child) + vis_item.setFlags(QtGui.Qt.ItemIsEnabled) + vis_item.setData(child) + ana_item.appendRow(vis_item) + + # add extractor items + for ext in visualizations[vis_type].extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_type) + ext_item = QtGui.QStandardItem( + icon, translate("FEM", "Add {}").format(name) + ) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) + ext_item.setData(ext) + vis_item.appendRow(ext_item) + + if ana_item.rowCount(): + model.appendRow(ana_item) + + return model + + +def build_post_object_item(post_object, extractions, vis_type): + + # definitely build a item and add the extractions + post_item = QtGui.QStandardItem( + post_object.ViewObject.Icon, translate("FEM", "From {}").format(post_object.Label) + ) + post_item.setFlags(QtGui.Qt.ItemIsEnabled) + post_item.setData(post_object) + + # add extractor items + for ext in extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_type) + ext_item = QtGui.QStandardItem(icon, translate("FEM", "add {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) + ext_item.setData(ext) + post_item.appendRow(ext_item) + + # if we are a post group, we need to add the children + if post_object.hasExtension("Fem::FemPostGroupExtension"): + + for child in post_object.Group: + if child.isDerivedFrom("Fem::FemPostObject"): + item = build_post_object_item(child, extractions, vis_type) + post_item.appendRow(item) + + return post_item + + +def build_add_from_data_tree_model(vis_type): + # model that shows all Post data objects from which data can be extracted + extractions = pv.get_registered_visualizations()[vis_type].extractions + + model = QtGui.QStandardItemModel() + for obj in FreeCAD.ActiveDocument.Objects: + if obj.isDerivedFrom("Fem::FemAnalysis"): + ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label) + ana_item.setFlags(QtGui.Qt.ItemIsEnabled) + + # check all children if it is a post object + for child in obj.Group: + if child.isDerivedFrom("Fem::FemPostObject"): + item = build_post_object_item(child, extractions, vis_type) + ana_item.appendRow(item) + + if ana_item.rowCount(): + model.appendRow(ana_item) + + return model + + +# implementation of GUI and its functionality +# ########################################### + + +class _ElideToolButton(QtGui.QToolButton): + # tool button that elides its text, and left align icon and text + + def __init__(self, icon, text, parent): + super().__init__(parent) + + self._text = text + self._icon = icon + + def setCustomText(self, text): + self._text = text + self.repaint() + + def setCustomIcon(self, icon): + self._icon = icon + self.repaint() + + def sizeHint(self): + button_size = super().sizeHint() + icn_size = self.iconSize() + min_margin = max((button_size - icn_size).height(), 6) + return QtCore.QSize(self.iconSize().width() + 10, icn_size.height() + min_margin) + + def paintEvent(self, event): + + # draw notmal button, without text and icon + super().paintEvent(event) + + # add icon and elided text + painter = QtGui.QPainter() + painter.begin(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing, True) + painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True) + + margin = (self.height() - self.iconSize().height()) / 2 + icn_width = self.iconSize().width() + if self._icon.isNull(): + icn_width = 0 + + fm = self.fontMetrics() + txt_size = self.width() - icn_width - 2 * margin + if not self._icon.isNull(): + # we add the margin between icon and text + txt_size -= margin + + txt_min = fm.boundingRect("...").width() + + # should we center the icon? + xpos = margin + if not self._icon.isNull() and txt_size < txt_min: + # center icon + xpos = self.width() / 2 - self.iconSize().width() / 2 + + if not self._icon.isNull(): + match type(self._icon): + case QtGui.QPixmap: + painter.drawPixmap(xpos, margin, self._icon.scaled(self.iconSize())) + xpos += self.iconSize().width() + case QtGui.QIcon: + self._icon.paint( + painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize()) + ) + xpos += self.iconSize().width() + + xpos += margin # the margin to the text + + if txt_size >= txt_min: + text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, txt_size) + painter.drawText(xpos, margin + fm.ascent(), text) + + painter.end() + + +class _TreeChoiceButton(QtGui.QToolButton): + + selection = QtCore.Signal(object, object) + + def __init__(self, model): + super().__init__() + + self.model = model + self.setEnabled(bool(model.rowCount())) + + self.__skip_next_hide = False + + self.tree_view = QtGui.QTreeView(self) + self.tree_view.setModel(model) + + self.tree_view.setFrameShape(QtGui.QFrame.NoFrame) + self.tree_view.setHeaderHidden(True) + self.tree_view.setSelectionBehavior(QtGui.QTreeView.SelectionBehavior.SelectRows) + self.tree_view.expandAll() + self.tree_view.clicked.connect(self.selectIndex) + + style = self.style() + if not style.styleHint(QtGui.QStyle.SH_ItemView_ActivateItemOnSingleClick): + self.tree_view.activated.connect(self.selectIndex) + + # set a complex menu + self.popup = QtGui.QWidgetAction(self) + self.popup.setDefaultWidget(self.tree_view) + self.setPopupMode(QtGui.QToolButton.InstantPopup) + self.addAction(self.popup) + + QtCore.Slot(QtCore.QModelIndex) + + def selectIndex(self, index): + item = self.model.itemFromIndex(index) + + if item and not item.hasChildren(): + extraction = item.data() + parent = item.parent().data() + self.selection.emit(parent, extraction) + self.popup.trigger() + + def setModel(self, model): + self.model = model + self.tree_view.setModel(model) + self.tree_view.expandAll() + + # check if we should be disabled + self.setEnabled(bool(model.rowCount())) + + +class _SettingsPopup(QtGui.QMenu): + + close = QtCore.Signal() + + def __init__(self, setting, parent): + super().__init__(parent) + + self._setting = setting + self.setWindowFlags(QtGui.Qt.Popup) + self.setFocusPolicy(QtGui.Qt.ClickFocus) + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(setting) + + buttonBox = QtGui.QDialogButtonBox() + buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) + buttonBox.accepted.connect(self.hide) + vbox.addWidget(buttonBox) + + widget = QtGui.QFrame() + widget.setLayout(vbox) + + vbox2 = QtGui.QVBoxLayout() + vbox2.setContentsMargins(0, 0, 0, 0) + vbox2.addWidget(widget) + self.setLayout(vbox2) + + def size(self): + return self._setting.sizeHint() + + def showEvent(self, event): + # required to get keyboard events + self.setFocus() + + def hideEvent(self, event): + # emit on hide: this happens for OK button as well as + # "click away" closing of the popup + self.close.emit() + + def keyPressEvent(self, event): + # close on hitting enter + if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return: + self.hide() + + +class _SummaryWidget(QtGui.QWidget): + + delete = QtCore.Signal(object, object) # to delete: document object, summary widget + + def __init__(self, st_object, extractor, post_dialog): + super().__init__() + + self._st_object = st_object + self._extractor = extractor + self._post_dialog = post_dialog + + extr_label = extractor.Proxy.get_representive_fieldname(extractor) + extr_repr = extractor.ViewObject.Proxy.get_preview() + + # build the UI + hbox = QtGui.QHBoxLayout() + hbox.setContentsMargins(6, 0, 6, 0) + hbox.setSpacing(2) + + self.extrButton = self._button(extractor.ViewObject.Icon, extr_label) + self.viewButton = self._button(extr_repr[0], extr_repr[1], 1) + + size = self.viewButton.iconSize() + size.setWidth(size.width() * 2) + self.viewButton.setIconSize(size) + + if st_object: + self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label) + hbox.addWidget(self.stButton) + + else: + # that happens if the source of the extractor was deleted and now + # that property is set to None + self.extrButton.hide() + self.viewButton.hide() + + self.warning = QtGui.QLabel(self) + self.warning.full_text = translate("FEM", "{}: Data source not available").format( + extractor.Label + ) + hbox.addWidget(self.warning) + + self.rmButton = QtGui.QToolButton(self) + self.rmButton.setIcon(FreeCADGui.getIcon("delete.svg")) + self.rmButton.setAutoRaise(True) + + hbox.addWidget(self.extrButton) + hbox.addWidget(self.viewButton) + hbox.addSpacing(15) + hbox.addWidget(self.rmButton) + + # add the separation line + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0, 0, 0, 0) + vbox.setSpacing(5) + vbox.addItem(hbox) + self.frame = QtGui.QFrame(self) + self.frame.setFrameShape(QtGui.QFrame.HLine) + vbox.addWidget(self.frame) + + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + self.setSizePolicy(policy) + # self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) + self.setLayout(vbox) + + # connect actions. We add functions to widget, as well as the data we need, + # and use those as callback. This way every widget knows which objects to use + if st_object: + self.stButton.clicked.connect(self.showVisualization) + self.extrButton.clicked.connect(self.editApp) + self.viewButton.clicked.connect(self.editView) + + self.rmButton.clicked.connect(self.deleteTriggered) + + # make sure initial drawing happened + # self._redraw() + + def _button(self, icon, text, stretch=2): + + btn = _ElideToolButton(icon, text, self) + btn.setMinimumWidth(0) + btn.setAutoRaise(True) + btn.setToolTip(text) + + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + policy.setHorizontalStretch(stretch) + btn.setSizePolicy(policy) + return btn + + @QtCore.Slot() + def showVisualization(self): + if vis.is_visualization_object(self._st_object): + # show the visualization + self._st_object.ViewObject.Proxy.show_visualization() + else: + # for now just select the thing + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(self._st_object) + + def _position_dialog(self, dialog): + + # the scroll area does mess the mapping to global up, somehow + # the transformation from the widget ot the scroll area gives + # very weird values. Hence we build the coords of the widget + # ourself + + summary = dialog.parent() # == self + base_widget = summary.parent() + viewport = summary.parent() + scroll = viewport.parent() + + top_left = ( + summary.geometry().topLeft() + + base_widget.geometry().topLeft() + + viewport.geometry().topLeft() + ) + delta = (summary.width() - dialog.size().width()) / 2 + local_point = QtCore.QPoint(top_left.x() + delta, top_left.y() + summary.height()) + global_point = scroll.mapToGlobal(local_point) + + dialog.setGeometry(QtCore.QRect(global_point, dialog.sizeHint())) + + @QtCore.Slot() + def editApp(self): + if not hasattr(self, "appDialog"): + widget = self._extractor.ViewObject.Proxy.get_app_edit_widget(self._post_dialog) + self.appDialog = _SettingsPopup(widget, self) + self.appDialog.close.connect(self.appAccept) + + if not self.appDialog.isVisible(): + # position correctly and show + self._position_dialog(self.appDialog) + self.appDialog.show() + + @QtCore.Slot() + def editView(self): + + if not hasattr(self, "viewDialog"): + widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog) + self.viewDialog = _SettingsPopup(widget, self) + self.viewDialog.close.connect(self.viewAccept) + + if not self.viewDialog.isVisible(): + # position correctly and show + self._position_dialog(self.viewDialog) + self.viewDialog.show() + + @QtCore.Slot() + def deleteTriggered(self): + self.delete.emit(self._extractor, self) + + @QtCore.Slot() + def viewAccept(self): + + # update the preview + extr_repr = self._extractor.ViewObject.Proxy.get_preview() + self.viewButton.setCustomIcon(extr_repr[0]) + self.viewButton.setCustomText(extr_repr[1]) + self.viewButton.setToolTip(extr_repr[1]) + + @QtCore.Slot() + def appAccept(self): + + # update the preview + extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) + self.extrButton.setCustomText(extr_label) + self.extrButton.setToolTip(extr_label) + + +class ExtractLinkView(QtGui.QWidget): + + def __init__(self, obj, is_source, post_dialog): + # initializes the view. + # obj: The object for which the links should be shown / summarized + # is_source: Bool, if the object is the data source (e.g. postobject), or the target (e.g. plots) + + super().__init__() + + self._object = obj + self._is_source = is_source + self._post_dialog = post_dialog + self._widgets = [] + + # build the layout: + self._scroll_view = QtGui.QScrollArea(self) + self._scroll_view.setHorizontalScrollBarPolicy(QtGui.Qt.ScrollBarAlwaysOff) + self._scroll_view.setWidgetResizable(True) + self._scroll_widget = QtGui.QWidget(self._scroll_view) + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0, 6, 0, 0) + vbox.addStretch() + self._scroll_widget.setLayout(vbox) + self._scroll_view.setWidget(self._scroll_widget) + + hbox = QtGui.QHBoxLayout() + hbox.setSpacing(6) + label = QtGui.QLabel(translate("FEM", "Data used in:")) + if not self._is_source: + label.setText(translate("FEM", "Data used from:")) + + label.setAlignment(QtGui.Qt.AlignBottom) + hbox.addWidget(label) + hbox.addStretch() + + if self._is_source: + + self._add = _TreeChoiceButton(build_add_to_visualization_tree_model()) + self._add.setText(translate("FEM", "Add data to")) + self._add.selection.connect(self.addExtractionToVisualization) + hbox.addWidget(self._add) + + self._create = _TreeChoiceButton(build_new_visualization_tree_model()) + self._create.setText(translate("FEM", "New")) + self._create.selection.connect(self.newVisualization) + hbox.addWidget(self._create) + + else: + vis_type = vis.get_visualization_type(self._object) + self._add = _TreeChoiceButton(build_add_from_data_tree_model(vis_type)) + self._add.setText(translate("FEM", "Add data from")) + self._add.selection.connect(self.addExtractionToPostObject) + hbox.addWidget(self._add) + + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0, 0, 0, 0) + vbox.addItem(hbox) + vbox.addWidget(self._scroll_view) + + self.setLayout(vbox) + + # add the content + self.repopulate() + + def _build_summary_widget(self, extractor): + + if self._is_source: + st_object = extractor.getParentGroup() + else: + st_object = extractor.Source + + widget = _SummaryWidget(st_object, extractor, self._post_dialog) + widget.delete.connect(self._delete_extraction) + + return widget + + def _delete_extraction(self, extractor, widget): + # remove the document object + doc = extractor.Document + doc.removeObject(extractor.Name) + doc.recompute() + + # remove the widget + self._widgets.remove(widget) + widget.deleteLater() + + def repopulate(self): + # collect all links that are available and shows them + + # clear the view + for widget in self._widgets: + widget.hide() + widget.deleteLater() + + self._widgets = [] + + # rebuild the widgets + + if self._is_source: + candidates = self._object.InList + else: + candidates = self._object.OutList + + # get all widgets from the candidates + for candidate in candidates: + if extr.is_extractor_object(candidate): + summary = self._build_summary_widget(candidate) + self._widgets.append(summary) + + # fill the scroll area + vbox = self._scroll_widget.layout() + for widget in reversed(self._widgets): + vbox.insertWidget(0, widget) + + # also reset the add button model + if self._is_source: + self._add.setModel(build_add_to_visualization_tree_model()) + + def _find_parent_analysis(self, obj): + # iterate upwards, till we find a analysis + for parent in obj.InList: + if parent.isDerivedFrom("Fem::FemAnalysis"): + return parent + + analysis = self._find_parent_analysis(parent) + if analysis: + return analysis + + return None + + QtCore.Slot(object, object) # visualization data, extraction data + + def newVisualization(self, vis_data, ext_data): + + FreeCADGui.addModule(vis_data.module) + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create visualization + FreeCADGui.doCommand( + f"visualization = {vis_data.module}.{vis_data.factory}(FreeCAD.ActiveDocument)" + ) + + analysis = self._find_parent_analysis(self._object) + if analysis: + FreeCADGui.doCommand(f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)") + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}") + # default values: color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = visualization.ViewObject.Proxy.get_next_default_color()" + ) + + FreeCADGui.doCommand(f"visualization.addObject(extraction)") + + self._post_dialog._recompute() + self.repopulate() + + QtCore.Slot(object, object) # visualization object, extraction data + + def addExtractionToVisualization(self, vis_obj, ext_data): + + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}") + + # default values: color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = (Gui.ActiveDocument.{vis_obj.Name}.Proxy.get_next_default_color())" + ) + + FreeCADGui.doCommand(f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)") + + self._post_dialog._recompute() + self.repopulate() + + QtCore.Slot(object, object) # post object, extraction data + + def addExtractionToPostObject(self, post_obj, ext_data): + + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}") + + # default values for color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = Gui.ActiveDocument.{self._object.Name}.Proxy.get_next_default_color()" + ) + + FreeCADGui.doCommand(f"App.ActiveDocument.{self._object.Name}.addObject(extraction)") + + self._post_dialog._recompute() + self.repopulate() diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py new file mode 100644 index 0000000000..557c177cd3 --- /dev/null +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -0,0 +1,179 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD visualization registry" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_visualization +# \ingroup FEM +# \brief A registry to collect visualizations for use in menus + +# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui +# directly to support cmd line use. + +import copy +from dataclasses import dataclass + +from PySide import QtCore + +import FreeCAD + + +# Registry to handle visualization commands +# ######################################### + +_registry = {} + + +@dataclass +class _Extraction: + + name: str + icon: str + dimension: str + extracttype: str + module: str + factory: str + + +@dataclass +class _Visualization: + + name: str + icon: str + module: str + factory: str + extractions: list[_Extraction] + + +# Register a visualization by type, icon and factory function +def register_visualization(visualization_type, icon, module, factory): + if visualization_type in _registry: + raise ValueError("Visualization type already registered") + + _registry[visualization_type] = _Visualization(visualization_type, icon, module, factory, []) + + +def register_extractor( + visualization_type, extraction_type, icon, dimension, etype, module, factory +): + + if not visualization_type in _registry: + raise ValueError("visualization not registered yet") + + extraction = _Extraction(extraction_type, icon, dimension, etype, module, factory) + _registry[visualization_type].extractions.append(extraction) + + +def get_registered_visualizations(): + return copy.deepcopy(_registry) + + +def _to_command_name(name): + return "FEM_PostVisualization" + name + + +class _VisualizationGroupCommand: + + def GetCommands(self): + visus = _registry.keys() + cmds = [_to_command_name(v) for v in visus] + return cmds + + def GetDefaultCommand(self): + return 0 + + def GetResources(self): + return { + "MenuText": QtCore.QT_TRANSLATE_NOOP("FEM", "Data Visualizations"), + "ToolTip": QtCore.QT_TRANSLATE_NOOP( + "FEM", "Different visualizations to show post processing data in" + ), + } + + def IsActive(self): + if not FreeCAD.ActiveDocument: + return False + + import FemGui + + return bool(FemGui.getActiveAnalysis()) + + +class _VisualizationCommand: + + def __init__(self, visualization_type): + self._visualization_type = visualization_type + + def GetResources(self): + + cmd = _to_command_name(self._visualization_type) + vis = _registry[self._visualization_type] + tooltip = f"Create a {self._visualization_type} post processing data visualization" + + return { + "Pixmap": vis.icon, + "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)), + "Accel": "", + "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), + "CmdType": "AlterDoc", + } + + def IsActive(self): + # active analysis available + if not FreeCAD.ActiveDocument: + return False + + import FemGui + + return bool(FemGui.getActiveAnalysis()) + + def Activated(self): + import FreeCADGui + + vis = _registry[self._visualization_type] + FreeCAD.ActiveDocument.openTransaction(f"Create {vis.name}") + + FreeCADGui.addModule(vis.module) + FreeCADGui.addModule("FemGui") + + FreeCADGui.doCommand(f"obj = {vis.module}.{vis.factory}(FreeCAD.ActiveDocument)") + FreeCADGui.doCommand(f"FemGui.getActiveAnalysis().addObject(obj)") + + FreeCADGui.Selection.clearSelection() + FreeCADGui.doCommand("FreeCADGui.ActiveDocument.setEdit(obj)") + + +def setup_commands(toplevel_name): + # creates all visualization commands and registers them. The + # toplevel group command will have the name provided to this function. + + import FreeCADGui + + # first all visualization and extraction commands + for vis in _registry: + FreeCADGui.addCommand(_to_command_name(vis), _VisualizationCommand(vis)) + + # build the group command! + FreeCADGui.addCommand("FEM_PostVisualization", _VisualizationGroupCommand()) diff --git a/src/Mod/Fem/femguiutils/vtk_module_handling.py b/src/Mod/Fem/femguiutils/vtk_module_handling.py index 6c834b0820..9ae5cc2fb9 100644 --- a/src/Mod/Fem/femguiutils/vtk_module_handling.py +++ b/src/Mod/Fem/femguiutils/vtk_module_handling.py @@ -47,6 +47,10 @@ __title__ = "FEM GUI vtk python module check" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" + +# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui +# directly to support cmd line use. + __user_input_received = False diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py new file mode 100644 index 0000000000..b6e8c939b3 --- /dev/null +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -0,0 +1,253 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD table view widget to visualize vtkTable" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package vtk_table_view +# \ingroup FEM +# \brief A Qt widget to show a vtkTable + +from PySide import QtGui +from PySide import QtCore + +import FreeCAD +import FreeCADGui + +from vtkmodules.vtkIOCore import vtkDelimitedTextWriter + +translate = FreeCAD.Qt.translate + + +class VtkTableModel(QtCore.QAbstractTableModel): + # Simple table model. Only supports single component columns + # One can supply a header_names dict to replace the table column names + # in the header. It is a dict "column_idx (int)" to "new name"" or + # "orig_name (str)" to "new name" + + def __init__(self, header_names=None): + super().__init__() + self._table = None + if header_names: + self._header = header_names + else: + self._header = {} + + def setTable(self, table, header_names=None): + self.beginResetModel() + self._table = table + if header_names: + self._header = header_names + self.endResetModel() + + def rowCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfRows() + + def columnCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfColumns() + + def data(self, index, role): + + if not self._table: + return None + + if role == QtCore.Qt.DisplayRole: + col = self._table.GetColumn(index.column()) + return col.GetTuple(index.row())[0] + + return None + + def headerData(self, section, orientation, role): + + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + if section in self._header: + return self._header[section] + + name = self._table.GetColumnName(section) + if name in self._header: + return self._header[name] + + return name + + if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: + return section + + return None + + def getTable(self): + return self._table + + +class VtkTableSummaryModel(QtCore.QAbstractTableModel): + # Simple model showing a summary of the table. + # Only supports single component columns + + def __init__(self): + super().__init__() + self._table = None + + def setTable(self, table): + self.beginResetModel() + self._table = table + self.endResetModel() + + def rowCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfColumns() + + def columnCount(self, index): + return 2 # min, max + + def data(self, index, role): + + if not self._table: + return None + + if role == QtCore.Qt.DisplayRole: + col = self._table.GetColumn(index.row()) + range = col.GetRange() + return range[index.column()] + + return None + + def headerData(self, section, orientation, role): + + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return ["Min", "Max"][section] + + if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: + return self._table.GetColumnName(section) + + return None + + def getTable(self): + return self._table + + +class VtkTableView(QtGui.QWidget): + + def __init__(self, model): + super().__init__() + + self.model = model + + layout = QtGui.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + # start with the toolbar + self.toolbar = QtGui.QToolBar() + csv_action = QtGui.QAction(self) + csv_action.triggered.connect(self.exportCsv) + csv_action.setIcon(FreeCADGui.getIcon("Std_Export")) + csv_action.setToolTip(translate("FEM", "Export to CSV")) + self.toolbar.addAction(csv_action) + + copy_action = QtGui.QAction(self) + copy_action.triggered.connect(self.copyToClipboard) + copy_action.setIcon(FreeCADGui.getIcon("edit-copy")) + shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy) + copy_action.setToolTip( + translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString())) + ) + copy_action.setShortcut(shortcut) + self.toolbar.addAction(copy_action) + + layout.addWidget(self.toolbar) + + # now the table view + self.table_view = QtGui.QTableView() + self.table_view.setModel(model) + self.model.modelReset.connect(self.modelReset) + + # fast initial resize and manual resizing still allowed! + header = self.table_view.horizontalHeader() + header.setResizeContentsPrecision(10) + self.table_view.resizeColumnsToContents() + + layout.addWidget(self.table_view) + self.setLayout(layout) + + @QtCore.Slot() + def modelReset(self): + # The model is reset, make sure the header visibility is working + # This is needed in case new data was added + self.table_view.resizeColumnsToContents() + + @QtCore.Slot(bool) + def exportCsv(self, state): + + file_path, filter = QtGui.QFileDialog.getSaveFileName( + None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)" + ) + if not file_path: + FreeCAD.Console.PrintMessage( + translate("FEM", "CSV file export aborted: no filename selected") + ) + return + + writer = vtkDelimitedTextWriter() + writer.SetFileName(file_path) + writer.SetInputData(self.model.getTable()) + writer.Write() + + @QtCore.Slot() + def copyToClipboard(self): + + sel_model = self.table_view.selectionModel() + selection = sel_model.selectedIndexes() + + if len(selection) < 1: + return + + copy_table = "" + previous = selection.pop(0) + for current in selection: + + data = self.model.data(previous, QtCore.Qt.DisplayRole) + copy_table += str(data) + + if current.row() != previous.row(): + copy_table += "\n" + else: + copy_table += "\t" + + previous = current + + copy_table += str(self.model.data(selection[-1], QtCore.Qt.DisplayRole)) + copy_table += "\n" + + clipboard = QtGui.QApplication.instance().clipboard() + clipboard.setText(copy_table) diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py new file mode 100644 index 0000000000..41ba270c35 --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -0,0 +1,398 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing data exxtractor base objects" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostextractors +# \ingroup FEM +# \brief base objects for data extractors + +from vtkmodules.vtkCommonCore import vtkIntArray +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonDataModel import vtkTable + +from PySide.QtCore import QT_TRANSLATE_NOOP + +from . import base_fempythonobject + +_PropHelper = base_fempythonobject._PropHelper + +# helper functions +# ################ + + +def is_extractor_object(obj): + if not hasattr(obj, "Proxy"): + return False + + return hasattr(obj.Proxy, "ExtractionType") + + +def get_extraction_type(obj): + # returns the extractor type string, or throws exception if + # not a extractor + return obj.Proxy.ExtractionType + + +def get_extraction_dimension(obj): + # returns the extractor dimension string, or throws exception if + # not a extractor + return obj.Proxy.ExtractionDimension + + +# Base class for all extractors with common source and table handling functionality +# Note: Never use directly, always subclass! This class does not create a +# ExtractionType/Dimension variable, hence will not work correctly. +class Extractor(base_fempythonobject.BaseFemPythonObject): + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + + prop = [ + _PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc=QT_TRANSLATE_NOOP("FEM", "The data table that stores the extracted data"), + value=vtkTable(), + ), + _PropHelper( + type="App::PropertyLink", + name="Source", + group="Base", + doc=QT_TRANSLATE_NOOP("FEM", "The data source from which the data is extracted"), + value=None, + ), + ] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(obj) + + def onChanged(self, obj, prop): + + if prop == "Source": + # check if the source is a Post object + if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): + FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") + obj.Source = None + + def get_vtk_table(self, obj): + if not obj.DataTable: + obj.DataTable = vtkTable() + + return obj.DataTable + + def component_options(self, num): + + match num: + case 2: + return ["X", "Y"] + case 3: + return ["X", "Y", "Z"] + case 6: + return ["XX", "YY", "ZZ", "XY", "XZ", "YZ"] + case _: + return ["Not a vector"] + + def get_representive_fieldname(self, obj): + # should return the representative field name, e.g. Position (X) + return "" + + +class Extractor1D(Extractor): + + ExtractionDimension = "1D" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyEnumeration", + name="XField", + group="X Data", + doc=QT_TRANSLATE_NOOP("FEM", "The field to use as X data"), + value=[], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XComponent", + group="X Data", + doc=QT_TRANSLATE_NOOP( + "FEM", "Which part of the X field vector to use for the X axis" + ), + value=[], + ), + ] + + return super()._get_properties() + prop + + def onChanged(self, obj, prop): + + super().onChanged(obj, prop) + + if prop == "XField" and obj.Source and obj.Source.getDataSet(): + point_data = obj.Source.getDataSet().GetPointData() + self._setup_x_component_property(obj, point_data) + + if prop == "Source": + if obj.Source: + dset = obj.Source.getDataSet() + if dset: + self._setup_x_properties(obj, dset) + else: + self._clear_x_properties(obj) + else: + self._clear_x_properties(obj) + + def _setup_x_component_property(self, obj, point_data): + + if obj.XField == "Index": + obj.XComponent = self.component_options(1) + elif obj.XField == "Position": + obj.XComponent = self.component_options(3) + else: + array = point_data.GetAbstractArray(obj.XField) + obj.XComponent = self.component_options(array.GetNumberOfComponents()) + + def _clear_x_properties(self, obj): + if hasattr(obj, "XComponent"): + obj.XComponent = [] + if hasattr(obj, "XField"): + obj.XField = [] + + def _setup_x_properties(self, obj, dataset): + # Set all X Data properties correctly for the given dataset + fields = ["Index", "Position"] + point_data = dataset.GetPointData() + + for i in range(point_data.GetNumberOfArrays()): + fields.append(point_data.GetArrayName(i)) + + current_field = obj.XField + obj.XField = fields + if current_field in fields: + obj.XField = current_field + + self._setup_x_component_property(obj, point_data) + + def _x_array_component_to_table(self, obj, array, table): + # extracts the component out of the array according to XComponent setting + # Note: Uses the array name unchanged + + if array.GetNumberOfComponents() == 1: + table.AddColumn(array) + else: + component_array = vtkDoubleArray() + component_array.SetNumberOfComponents(1) + component_array.SetNumberOfTuples(array.GetNumberOfTuples()) + c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) + component_array.CopyComponent(0, array, c_idx) + component_array.SetName(array.GetName()) + table.AddColumn(component_array) + + def _x_array_from_dataset(self, obj, dataset, copy=True): + # extracts the relevant array from the dataset and returns a copy + # indices = None uses all indices, otherwise the values in this list + + match obj.XField: + case "Index": + # index needs always to be build, ignore copy argument + num = dataset.GetPoints().GetNumberOfPoints() + array = vtkIntArray() + array.SetNumberOfTuples(num) + array.SetNumberOfComponents(1) + for i in range(num): + array.SetValue(i, i) + + case "Position": + + orig_array = dataset.GetPoints().GetData() + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.XField) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array + + return array + + def get_representive_fieldname(self, obj): + # representative field is the x field + label = obj.XField + if not label: + return "" + + if len(obj.getEnumerationsOfProperty("XComponent")) > 1: + label += f" ({obj.XComponent})" + + return label + + +class Extractor2D(Extractor1D): + + ExtractionDimension = "2D" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyEnumeration", + name="YField", + group="Y Data", + doc=QT_TRANSLATE_NOOP("FEM", "The field to use as Y data"), + value=[], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YComponent", + group="Y Data", + doc=QT_TRANSLATE_NOOP( + "FEM", "Which part of the Y field vector to use for the Y axis" + ), + value=[], + ), + ] + + return super()._get_properties() + prop + + def onChanged(self, obj, prop): + + super().onChanged(obj, prop) + + if prop == "YField" and obj.Source and obj.Source.getDataSet(): + point_data = obj.Source.getDataSet().GetPointData() + self._setup_y_component_property(obj, point_data) + + if prop == "Source": + if obj.Source: + dset = obj.Source.getDataSet() + if dset: + self._setup_y_properties(obj, dset) + else: + self._clear_y_properties(obj) + else: + self._clear_y_properties(obj) + + def _setup_y_component_property(self, obj, point_data): + + if obj.YField == "Position": + obj.YComponent = self.component_options(3) + else: + array = point_data.GetAbstractArray(obj.YField) + obj.YComponent = self.component_options(array.GetNumberOfComponents()) + + def _clear_y_properties(self, obj): + if hasattr(obj, "YComponent"): + obj.YComponent = [] + if hasattr(obj, "YField"): + obj.YField = [] + + def _setup_y_properties(self, obj, dataset): + # Set all X Data properties correctly for the given dataset + fields = ["Position"] + point_data = dataset.GetPointData() + + for i in range(point_data.GetNumberOfArrays()): + fields.append(point_data.GetArrayName(i)) + + current_field = obj.YField + obj.YField = fields + if current_field in fields: + obj.YField = current_field + + self._setup_y_component_property(obj, point_data) + + def _y_array_component_to_table(self, obj, array, table): + # extracts the component out of the array according to XComponent setting + + if array.GetNumberOfComponents() == 1: + table.AddColumn(array) + else: + component_array = vtkDoubleArray() + component_array.SetNumberOfComponents(1) + component_array.SetNumberOfTuples(array.GetNumberOfTuples()) + c_idx = obj.getEnumerationsOfProperty("YComponent").index(obj.YComponent) + component_array.CopyComponent(0, array, c_idx) + component_array.SetName(array.GetName()) + table.AddColumn(component_array) + + def _y_array_from_dataset(self, obj, dataset, copy=True): + # extracts the relevant array from the dataset and returns a copy + # indices = None uses all indices, otherwise the values in this list + + match obj.YField: + case "Position": + + orig_array = dataset.GetPoints().GetData() + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.YField) + + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array + + return array + + def get_representive_fieldname(self, obj): + # representative field is the y field + label = obj.YField + if not label: + return "" + + if len(obj.getEnumerationsOfProperty("YComponent")) > 1: + label += f" ({obj.YComponent})" + + return label diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py new file mode 100644 index 0000000000..5c7465d5bc --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -0,0 +1,183 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing data visualization base object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostextractors +# \ingroup FEM +# \brief base objects for data visualizations + +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonCore import vtkDoubleArray + +from . import base_fempythonobject +from . import base_fempostextractors + +# helper functions +# ################ + + +def is_visualization_object(obj): + if not obj: + return False + + if not hasattr(obj, "Proxy"): + return False + + return hasattr(obj.Proxy, "VisualizationType") + + +def get_visualization_type(obj): + # returns the extractor type string, or throws exception if + # not a extractor + return obj.Proxy.VisualizationType + + +def is_visualization_extractor_type(obj, vistype): + + # must be extractor + if not base_fempostextractors.is_extractor_object(obj): + return False + + # must be visualization object + if not is_visualization_object(obj): + return False + + # must be correct type + if get_visualization_type(obj) != vistype: + return False + + return True + + +# Base class for all visualizations +# It collects all data from its extraction objects into a table. +# Note: Never use directly, always subclass! This class does not create a +# Visualization variable, hence will not work correctly. +class PostVisualization(base_fempythonobject.BaseFemPythonObject): + + def __init__(self, obj): + super().__init__(obj) + obj.addExtension("App::GroupExtensionPython") + self._setup_properties(obj) + + def _setup_properties(self, obj): + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + # override if subclass wants to add additional properties + + prop = [ + base_fempostextractors._PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the data for visualization", + value=vtkTable(), + ), + ] + return prop + + def onDocumentRestored(self, obj): + # if a new property was added we handle it by setup + # Override if subclass needs to handle changed property type + + self._setup_properties(obj) + + def onChanged(self, obj, prop): + # Ensure only correct child object types are in the group + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if not is_visualization_extractor_type(child, self.VisualizationType): + FreeCAD.Console.PrintWarning( + f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added" + ) + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + def execute(self, obj): + # Collect all extractor child data into our table + # Note: Each childs table can have different number of rows. We need + # to pad the date for our table in this case + + rows = self.getLongestColumnLength(obj) + table = vtkTable() + for child in obj.Group: + + # If child has no Source, its table should be empty. However, + # it would theoretical be possible that child source was set + # to none without recompute, and the visualization was manually + # recomputed afterwards + if not child.Source and (child.Table.GetNumberOfColumns() > 0): + FreeCAD.Console.PrintWarning( + f"{child.Label} has data, but no Source object. Will be ignored" + ) + continue + + c_table = child.Table + for i in range(c_table.GetNumberOfColumns()): + c_array = c_table.GetColumn(i) + array = vtkDoubleArray() + + if c_array.GetNumberOfTuples() == rows: + # simple deep copy is enough + array.DeepCopy(c_array) + + else: + array.SetNumberOfComponents(c_array.GetNumberOfComponents()) + array.SetNumberOfTuples(rows) + array.Fill(0) # so that all non-used entries are set to 0 + for j in range(c_array.GetNumberOfTuples()): + array.SetTuple(j, c_array.GetTuple(j)) + + array.SetName(f"{child.Source.Name}: {c_array.GetName()}") + table.AddColumn(array) + + obj.Table = table + return False + + def getLongestColumnLength(self, obj): + # iterate all extractor children and get the column lengths + + length = 0 + for child in obj.Group: + if base_fempostextractors.is_extractor_object(child): + table = child.Table + if table.GetNumberOfColumns() > 0: + # we assume all columns of an extractor have same length + num = table.GetColumn(0).GetNumberOfTuples() + if num > length: + length = num + + return length diff --git a/src/Mod/Fem/femobjects/base_fempythonobject.py b/src/Mod/Fem/femobjects/base_fempythonobject.py index 45b8de4e7d..48003443c2 100644 --- a/src/Mod/Fem/femobjects/base_fempythonobject.py +++ b/src/Mod/Fem/femobjects/base_fempythonobject.py @@ -54,6 +54,7 @@ class _PropHelper: Helper class to manage property data inside proxy objects. Initialization keywords are the same used with PropertyContainer to add dynamics properties plus "value" for the initial value. + Note: Is used as base for a GUI version, be aware when refactoring """ def __init__(self, **kwds): diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py new file mode 100644 index 0000000000..f70c6c65c9 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -0,0 +1,213 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post line plot" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +import FreeCAD + +from . import base_fempostextractors +from . import base_fempythonobject + +_PropHelper = base_fempythonobject._PropHelper + +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline + +from PySide.QtCore import QT_TRANSLATE_NOOP + + +class PostFieldData1D(base_fempostextractors.Extractor1D): + """ + A post processing extraction of one dimensional field data + """ + + ExtractionType = "Field" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify if the field shall be extracted for every available frame" + ), + value=False, + ), + ] + return super()._get_properties() + prop + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + timesteps = [] + if obj.ExtractFrames: + # check if we have timesteps + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + else: + FreeCAD.Console.PrintWarning( + 'No frames available in data, ignoring "ExtractFrames" property' + ) + + if not timesteps: + # get the dataset and extract the correct array + array = self._x_array_from_dataset(obj, dataset) + if array.GetNumberOfComponents() > 1: + array.SetName(obj.XField + " (" + obj.XComponent + ")") + else: + array.SetName(obj.XField) + + self._x_array_component_to_table(obj, array, table) + + else: + algo = obj.Source.getOutputAlgorithm() + for timestep in timesteps: + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._x_array_from_dataset(obj, dataset) + + if array.GetNumberOfComponents() > 1: + array.SetName(f"{obj.XField} ({obj.XComponent}) - {timestep}") + else: + array.SetName(f"{obj.XField} - {timestep}") + self._x_array_component_to_table(obj, array, table) + + # set the final table + obj.Table = table + + +class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): + """ + A post processing extraction of one dimensional index data + """ + + ExtractionType = "Index" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyInteger", + name="Index", + group="X Data", + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify for which index the data should be extracted" + ), + value=0, + ), + ] + return super()._get_properties() + prop + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + # check if we have timesteps + timesteps = [] + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + + algo = obj.Source.getOutputAlgorithm() + frame_array = vtkDoubleArray() + idx = obj.Index + + if timesteps: + setup = False + for i, timestep in enumerate(timesteps): + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._x_array_from_dataset(obj, dataset, copy=False) + + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) + + if not setup: + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_array.SetTuple(i, idx, array) + else: + algo.Update() + dataset = algo.GetOutputDataObject(0) + array = self._x_array_from_dataset(obj, dataset, copy=False) + + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) + + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(1) + frame_array.SetTuple(0, idx, array) + + if frame_array.GetNumberOfComponents() > 1: + frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") + else: + frame_array.SetName(f"{obj.XField} @Idx {obj.Index}") + + self._x_array_component_to_table(obj, frame_array, table) + + # set the final table + obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py new file mode 100644 index 0000000000..64cba2d5c7 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -0,0 +1,249 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post extractors 2D" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +import FreeCAD + +from . import base_fempostextractors +from . import base_fempythonobject + +_PropHelper = base_fempythonobject._PropHelper + +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline + +from PySide.QtCore import QT_TRANSLATE_NOOP + + +class PostFieldData2D(base_fempostextractors.Extractor2D): + """ + A post processing extraction of two dimensional field data + """ + + ExtractionType = "Field" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify if the field shall be extracted for every available frame" + ), + value=False, + ), + ] + return super()._get_properties() + prop + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + timesteps = [] + if obj.ExtractFrames: + # check if we have timesteps + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + else: + FreeCAD.Console.PrintWarning( + 'No frames available in data, ignoring "ExtractFrames" property' + ) + + if not timesteps: + # get the dataset and extract the correct array + xarray = self._x_array_from_dataset(obj, dataset) + if xarray.GetNumberOfComponents() > 1: + xarray.SetName(obj.XField + " (" + obj.XComponent + ")") + else: + xarray.SetName(obj.XField) + + self._x_array_component_to_table(obj, xarray, table) + + yarray = self._y_array_from_dataset(obj, dataset) + if yarray.GetNumberOfComponents() > 1: + yarray.SetName(obj.YField + " (" + obj.YComponent + ")") + else: + yarray.SetName(obj.YField) + + self._y_array_component_to_table(obj, yarray, table) + + else: + algo = obj.Source.getOutputAlgorithm() + for timestep in timesteps: + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + + xarray = self._x_array_from_dataset(obj, dataset) + if xarray.GetNumberOfComponents() > 1: + xarray.SetName(f"X - {obj.XField} ({obj.XComponent}) - {timestep}") + else: + xarray.SetName(f"X - {obj.XField} - {timestep}") + self._x_array_component_to_table(obj, xarray, table) + + yarray = self._y_array_from_dataset(obj, dataset) + if yarray.GetNumberOfComponents() > 1: + yarray.SetName(f"{obj.YField} ({obj.YComponent}) - {timestep}") + else: + yarray.SetName(f"{obj.YField} - {timestep}") + self._y_array_component_to_table(obj, yarray, table) + + # set the final table + obj.Table = table + + +class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): + """ + A post processing extraction for two dimensional data with X always being the frames + """ + + ExtractionType = "Index" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyInteger", + name="Index", + group="Data", + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify for which point index the data should be extracted" + ), + value=0, + ), + ] + return super()._get_properties() + prop + + def _setup_x_component_property(self, obj, point_data): + # override to only allow "Frames" as X data + obj.XComponent = ["Not a vector"] + + def _setup_x_properties(self, obj, dataset): + # override to only allow "Frames" as X data + obj.XField = ["Frames"] + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + # check if we have timesteps (required!) + timesteps = [] + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + + algo = obj.Source.getOutputAlgorithm() + + frame_x_array = vtkDoubleArray() + frame_y_array = vtkDoubleArray() + idx = obj.Index + + if timesteps: + setup = False + frame_x_array.SetNumberOfTuples(len(timesteps)) + frame_x_array.SetNumberOfComponents(1) + for i, timestep in enumerate(timesteps): + + frame_x_array.SetTuple1(i, timestep) + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._y_array_from_dataset(obj, dataset, copy=False) + + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) + + if not setup: + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_y_array.SetTuple(i, idx, array) + + else: + frame_x_array.SetNumberOfTuples(1) + frame_x_array.SetNumberOfComponents(1) + frame_x_array.SetTuple1(0, 0) + + algo.Update() + dataset = algo.GetOutputDataObject(0) + array = self._y_array_from_dataset(obj, dataset, copy=False) + + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) + + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(1) + frame_y_array.SetTuple(0, idx, array) + + frame_x_array.SetName("Frames") + if frame_y_array.GetNumberOfComponents() > 1: + frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}") + else: + frame_y_array.SetName(f"{obj.YField} @Idx {obj.Index}") + + table.AddColumn(frame_x_array) + self._y_array_component_to_table(obj, frame_y_array, table) + + # set the final table + obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py new file mode 100644 index 0000000000..fb0b1343cc --- /dev/null +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -0,0 +1,105 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post histogram" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \ingroup FEM +# \brief Post processing plot displaying histograms + +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling + +vtk_module_handling() + +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract1D + +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization( + "Histogram", ":/icons/FEM_PostHistogram.svg", "ObjectsFem", "makePostHistogram" +) + +post_visualization.register_extractor( + "Histogram", + "HistogramFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostHistogramFieldData", +) + + +post_visualization.register_extractor( + "Histogram", + "HistogramIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostHistogramIndexOverFrames", +) + +# Implementation +# ############## + + +def is_histogram_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Histogram" + + +class PostHistogramFieldData(post_extract1D.PostFieldData1D): + """ + A 1D Field extraction for histograms. + """ + + VisualizationType = "Histogram" + + +class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for histogram. + """ + + VisualizationType = "Histogram" + + +class PostHistogram(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as histograms + """ + + VisualizationType = "Histogram" diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py new file mode 100644 index 0000000000..3216400415 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -0,0 +1,105 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post line plot" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_lineplot +# \ingroup FEM +# \brief Post processing plot displaying lines + +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling + +vtk_module_handling() + +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract2D + +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization( + "Lineplot", ":/icons/FEM_PostLineplot.svg", "ObjectsFem", "makePostLineplot" +) + +post_visualization.register_extractor( + "Lineplot", + "LineplotFieldData", + ":/icons/FEM_PostField.svg", + "2D", + "Field", + "ObjectsFem", + "makePostLineplotFieldData", +) + +post_visualization.register_extractor( + "Lineplot", + "LineplotIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "2D", + "Index", + "ObjectsFem", + "makePostLineplotIndexOverFrames", +) + + +# Implementation +# ############## + + +def is_lineplot_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Lineplot" + + +class PostLineplotFieldData(post_extract2D.PostFieldData2D): + """ + A 2D Field extraction for lineplot. + """ + + VisualizationType = "Lineplot" + + +class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): + """ + A 2D index extraction for lineplot. + """ + + VisualizationType = "Lineplot" + + +class PostLineplot(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as line plots + """ + + VisualizationType = "Lineplot" diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py new file mode 100644 index 0000000000..a12398ab9e --- /dev/null +++ b/src/Mod/Fem/femobjects/post_table.py @@ -0,0 +1,105 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post table" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_table +# \ingroup FEM +# \brief Post processing plot displaying tables + +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling + +vtk_module_handling() + +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract1D + +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization( + "Table", ":/icons/FEM_PostSpreadsheet.svg", "ObjectsFem", "makePostTable" +) + +post_visualization.register_extractor( + "Table", + "TableFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostTableFieldData", +) + + +post_visualization.register_extractor( + "Table", + "TableIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostTableIndexOverFrames", +) + +# Implementation +# ############## + + +def is_table_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Table" + + +class PostTableFieldData(post_extract1D.PostFieldData1D): + """ + A 1D Field extraction for tables. + """ + + VisualizationType = "Table" + + +class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for table. + """ + + VisualizationType = "Table" + + +class PostTable(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as tables + """ + + VisualizationType = "Table" diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py new file mode 100644 index 0000000000..c972921dac --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -0,0 +1,88 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD task panel base for post object task panels" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostpanel +# \ingroup FEM +# \brief task panel base for post objects + +from PySide import QtCore, QtGui + +import FreeCAD + +from . import base_femtaskpanel + +translate = FreeCAD.Qt.translate + + +class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): + """ + The TaskPanel for post objects, mimicking the c++ functionality + """ + + def __init__(self, obj): + super().__init__(obj) + + # get the settings group + self.__settings_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem") + + # Implement parent functions + # ########################## + + def getStandardButtons(self): + return ( + QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel + ) + + def clicked(self, button): + # apply button hit? + if button == QtGui.QDialogButtonBox.Apply: + self.obj.Document.recompute() + + def open(self): + # open a new transaction if non is open + if not FreeCAD.getActiveTransaction(): + FreeCAD.ActiveDocument.openTransaction( + translate("FEM", "Edit {}").format(self.obj.Label) + ) + + # Helper functions + # ################ + + def _recompute(self): + # only recompute if the user wants automatic recompute + if self.__settings_grp.GetBool("PostAutoRecompute", True): + self.obj.Document.recompute() + + def _enumPropertyToCombobox(self, obj, prop, cbox): + cbox.blockSignals(True) + cbox.clear() + entries = obj.getEnumerationsOfProperty(prop) + for entry in entries: + cbox.addItem(entry) + + cbox.setCurrentText(getattr(obj, prop)) + cbox.blockSignals(False) diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py new file mode 100644 index 0000000000..9c54352f10 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -0,0 +1,54 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM post extractor object task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_extractor +# \ingroup FEM +# \brief universal task dialog for extractor objects. + +from PySide import QtCore, QtGui + +from . import base_fempostpanel + + +class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties extractor objects. The actual UI is + provided by the viewproviders. This allows using a universal task panel + """ + + def __init__(self, obj): + super().__init__(obj) + + # form is used to display individual task panels + app = obj.ViewObject.Proxy.get_app_edit_widget(self) + app.setWindowTitle("Data extraction") + app.setWindowIcon(obj.ViewObject.Icon) + view = obj.ViewObject.Proxy.get_view_edit_widget(self) + view.setWindowTitle("Visualization settings") + view.setWindowIcon(obj.ViewObject.Icon) + + self.form = [app, view] diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index a0658812e6..570ca63b66 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -35,10 +35,10 @@ import FreeCAD import FreeCADGui from femguiutils import selection_widgets -from . import base_femtaskpanel +from . import base_fempostpanel -class _TaskPanel(base_femtaskpanel._BaseTaskPanel): +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter """ @@ -56,50 +56,6 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): # form made from param and selection widget self.form = [self.widget, vobj.createDisplayTaskWidget()] - # get the settings group - self.__settings_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem") - - # Implement parent functions - # ########################## - - def getStandardButtons(self): - return ( - QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel - ) - - def clicked(self, button): - # apply button hit? - if button == QtGui.QDialogButtonBox.Apply: - self.obj.Document.recompute() - - def accept(self): - # self.obj.CharacteristicLength = self.elelen - # self.obj.References = self.selection_widget.references - # self.selection_widget.finish_selection() - return super().accept() - - def reject(self): - # self.selection_widget.finish_selection() - return super().reject() - - # Helper functions - # ################## - - def _recompute(self): - # only recompute if the user wants automatic recompute - if self.__settings_grp.GetBool("PostAutoRecompute", True): - self.obj.Document.recompute() - - def _enumPropertyToCombobox(self, obj, prop, cbox): - cbox.blockSignals(True) - cbox.clear() - entries = obj.getEnumerationsOfProperty(prop) - for entry in entries: - cbox.addItem(entry) - - cbox.setCurrentText(getattr(obj, prop)) - cbox.blockSignals(False) - # Setup functions # ############### diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py new file mode 100644 index 0000000000..df70e2f18d --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -0,0 +1,194 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM histogram plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_histogram +# \ingroup FEM +# \brief task panel for post histogram plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +translate = FreeCAD.Qt.translate + + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + hbox = QtGui.QHBoxLayout() + self.data_widget.show_plot = QtGui.QPushButton() + self.data_widget.show_plot.setText(translate("FEM", "Show plot")) + hbox.addWidget(self.data_widget.show_plot) + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText(translate("FEM", "Show data")) + hbox.addWidget(self.data_widget.show_table) + vbox = QtGui.QVBoxLayout() + vbox.addItem(hbox) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle(translate("FEM", "Histogram data")) + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) + + # histogram parameter widget + self.view_widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" + ) + self.view_widget.setWindowTitle(translate("FEM", "Histogram view settings")) + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) + + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget, self.view_widget] + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_plot.clicked.connect(self.showPlot) + self.data_widget.show_table.clicked.connect(self.showTable) + + # set current values to view widget + viewObj = self.obj.ViewObject + + self.view_widget.Bins.setValue(viewObj.Bins) + self._enumPropertyToCombobox(viewObj, "Type", self.view_widget.Type) + self.view_widget.Cumulative.setChecked(viewObj.Cumulative) + + self.view_widget.Title.setText(viewObj.Title) + self.view_widget.XLabel.setText(viewObj.XLabel) + self.view_widget.YLabel.setText(viewObj.YLabel) + + self.view_widget.LegendShow.setChecked(viewObj.Legend) + self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos) + self.view_widget.BarWidth.setValue(viewObj.BarWidth) + self.view_widget.HatchWidth.setValue(viewObj.HatchLineWidth) + + # connect callbacks + self.view_widget.Bins.valueChanged.connect(self.binsChanged) + self.view_widget.Type.activated.connect(self.typeChanged) + self.view_widget.Cumulative.toggled.connect(self.comulativeChanged) + + self.view_widget.Title.editingFinished.connect(self.titleChanged) + self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged) + self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged) + + self.view_widget.LegendShow.toggled.connect(self.legendShowChanged) + self.view_widget.LegendPos.activated.connect(self.legendPosChanged) + self.view_widget.BarWidth.valueChanged.connect(self.barWidthChanged) + self.view_widget.HatchWidth.valueChanged.connect(self.hatchWidthChanged) + + QtCore.Slot() + + def showPlot(self): + self.obj.ViewObject.Proxy.show_visualization() + + QtCore.Slot() + + def showTable(self): + + # TODO: make data model update when object is recomputed + data_model = vtk_table_view.VtkTableModel() + data_model.setTable(self.obj.Table) + + dialog = QtGui.QDialog(self.data_widget) + widget = vtk_table_view.VtkTableView(data_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0, 0, 0, 0) + + dialog.setLayout(layout) + dialog.resize(1500, 900) + dialog.show() + + QtCore.Slot(int) + + def binsChanged(self, bins): + self.obj.ViewObject.Bins = bins + + QtCore.Slot(int) + + def typeChanged(self, idx): + self.obj.ViewObject.Type = idx + + QtCore.Slot(bool) + + def comulativeChanged(self, state): + self.obj.ViewObject.Cumulative = state + + QtCore.Slot() + + def titleChanged(self): + self.obj.ViewObject.Title = self.view_widget.Title.text() + + QtCore.Slot() + + def xLabelChanged(self): + self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() + + QtCore.Slot() + + def yLabelChanged(self): + self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() + + QtCore.Slot(int) + + def legendPosChanged(self, idx): + self.obj.ViewObject.LegendLocation = idx + + QtCore.Slot(bool) + + def legendShowChanged(self, state): + self.obj.ViewObject.Legend = state + + QtCore.Slot(float) + + def barWidthChanged(self, value): + self.obj.ViewObject.BarWidth = value + + QtCore.Slot(float) + + def hatchWidthChanged(self, value): + self.obj.ViewObject.HatchLineWidth = value diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py new file mode 100644 index 0000000000..f5598e1874 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -0,0 +1,173 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM lineplot plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_lineplot +# \ingroup FEM +# \brief task panel for post lineplot plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +translate = FreeCAD.Qt.translate + + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + hbox = QtGui.QHBoxLayout() + self.data_widget.show_plot = QtGui.QPushButton() + self.data_widget.show_plot.setText(translate("FEM", "Show plot")) + hbox.addWidget(self.data_widget.show_plot) + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText(translate("FEM", "Show data")) + hbox.addWidget(self.data_widget.show_table) + vbox = QtGui.QVBoxLayout() + vbox.addItem(hbox) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle(translate("FEM", "Lineplot data")) + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) + + # lineplot parameter widget + self.view_widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" + ) + self.view_widget.setWindowTitle(translate("FEM", "Lineplot view settings")) + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) + + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget, self.view_widget] + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_plot.clicked.connect(self.showPlot) + self.data_widget.show_table.clicked.connect(self.showTable) + + # set current values to view widget + viewObj = self.obj.ViewObject + + self._enumPropertyToCombobox(viewObj, "Scale", self.view_widget.Scale) + self.view_widget.Grid.setChecked(viewObj.Grid) + + self.view_widget.Title.setText(viewObj.Title) + self.view_widget.XLabel.setText(viewObj.XLabel) + self.view_widget.YLabel.setText(viewObj.YLabel) + + self.view_widget.LegendShow.setChecked(viewObj.Legend) + self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos) + + # connect callbacks + self.view_widget.Scale.activated.connect(self.scaleChanged) + self.view_widget.Grid.toggled.connect(self.gridChanged) + + self.view_widget.Title.editingFinished.connect(self.titleChanged) + self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged) + self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged) + + self.view_widget.LegendShow.toggled.connect(self.legendShowChanged) + self.view_widget.LegendPos.activated.connect(self.legendPosChanged) + + QtCore.Slot() + + def showPlot(self): + self.obj.ViewObject.Proxy.show_visualization() + + QtCore.Slot() + + def showTable(self): + + # TODO: make data model update when object is recomputed + data_model = vtk_table_view.VtkTableModel() + data_model.setTable(self.obj.Table) + + dialog = QtGui.QDialog(self.data_widget) + widget = vtk_table_view.VtkTableView(data_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0, 0, 0, 0) + + dialog.setLayout(layout) + dialog.resize(1500, 900) + dialog.show() + + QtCore.Slot(int) + + def scaleChanged(self, idx): + self.obj.ViewObject.Scale = idx + + QtCore.Slot(bool) + + def gridChanged(self, state): + self.obj.ViewObject.Grid = state + + QtCore.Slot() + + def titleChanged(self): + self.obj.ViewObject.Title = self.view_widget.Title.text() + + QtCore.Slot() + + def xLabelChanged(self): + self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() + + QtCore.Slot() + + def yLabelChanged(self): + self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() + + QtCore.Slot(int) + + def legendPosChanged(self, idx): + self.obj.ViewObject.LegendLocation = idx + + QtCore.Slot(bool) + + def legendShowChanged(self, state): + self.obj.ViewObject.Legend = state diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py new file mode 100644 index 0000000000..98fd1686d6 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -0,0 +1,82 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM histogram plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_histogram +# \ingroup FEM +# \brief task panel for post histogram plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv + +translate = FreeCAD.Qt.translate + + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText(translate("FEM", "Show table")) + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(self.data_widget.show_table) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle(translate("FEM", "Table data")) + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.svg")) + + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget] + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_table.clicked.connect(self.showTable) + + @QtCore.Slot() + def showTable(self): + self.obj.ViewObject.Proxy.show_visualization() diff --git a/src/Mod/Fem/femtest/app/test_object.py b/src/Mod/Fem/femtest/app/test_object.py index 93d331ea94..7b34db84e6 100644 --- a/src/Mod/Fem/femtest/app/test_object.py +++ b/src/Mod/Fem/femtest/app/test_object.py @@ -79,12 +79,16 @@ class TestObjectCreate(unittest.TestCase): # gmsh mesh children: group, region, boundary layer --> 3 # result children: mesh result --> 1 # analysis itself is not in analysis group --> 1 - # vtk post pipeline children: region, scalar, cut, wrap, glyph --> 5 - # vtk python post objects: glyph --> 1 + # vtk post pipeline children: region, scalar, cut, wrap, contour --> 5 + # vtk python post objects: glyph, 6x data extraction --> 7 subtraction = 15 if vtk_objects_used: - subtraction += 6 + subtraction += 12 + if not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__): + # remove the 3 data visualization objects that would be in the Analysis + # if they would be available (Lineplot, histogram, table) + subtraction += 3 self.assertEqual(len(doc.Analysis.Group), count_defmake - subtraction) @@ -92,7 +96,9 @@ class TestObjectCreate(unittest.TestCase): # have been counted, but will not be executed to create objects failed = 0 if vtk_objects_used and not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__): - failed += 1 + # the 7 objects also counted in subtraction, +3 additional objects that are + # added directly to the analysis + failed += 10 self.assertEqual(len(doc.Objects), count_defmake - failed) @@ -1167,6 +1173,19 @@ def create_all_fem_objects_doc(doc): if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: ObjectsFem.makePostFilterGlyph(doc, vres) + # data extraction objects + lp = analysis.addObject(ObjectsFem.makePostLineplot(doc))[0] + lp.addObject(ObjectsFem.makePostLineplotFieldData(doc)) + lp.addObject(ObjectsFem.makePostLineplotIndexOverFrames(doc)) + + hp = analysis.addObject(ObjectsFem.makePostHistogram(doc))[0] + hp.addObject(ObjectsFem.makePostHistogramFieldData(doc)) + hp.addObject(ObjectsFem.makePostHistogramIndexOverFrames(doc)) + + tb = analysis.addObject(ObjectsFem.makePostTable(doc))[0] + tb.addObject(ObjectsFem.makePostTableFieldData(doc)) + tb.addObject(ObjectsFem.makePostTableIndexOverFrames(doc)) + analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc)) analysis.addObject(ObjectsFem.makeSolverCalculiX(doc)) sol = analysis.addObject(ObjectsFem.makeSolverElmer(doc))[0] diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py index dc7e6ba8ba..d170f2d6f2 100644 --- a/src/Mod/Fem/femviewprovider/view_base_femobject.py +++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py @@ -36,9 +36,27 @@ import FreeCADGui import FemGui # needed to display the icons in TreeView +from femobjects.base_fempythonobject import _PropHelper + False if FemGui.__name__ else True # flake8, dummy FemGui usage +class _GuiPropHelper(_PropHelper): + """ + Helper class to manage property data inside proxy objects. + Based on the App version, but viewprovider addProperty does + not take keyword args, hence we use positional arguments here + """ + + def __init__(self, **kwds): + super().__init__(**kwds) + + def add_to_object(self, obj): + obj.addProperty(self.info["type"], self.info["name"], self.info["group"], self.info["doc"]) + obj.setPropertyStatus(self.name, "LockDynamic") + setattr(obj, self.name, self.value) + + class VPBaseFemObject: """Proxy View Provider for FEM FeaturePythons base constraint.""" diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py new file mode 100644 index 0000000000..37ea937910 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -0,0 +1,148 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +from PySide import QtGui + +from femtaskpanels import task_post_extractor + + +class VPPostExtractor: + """ + A View Provider for extraction of data + """ + + def __init__(self, vobj): + vobj.Proxy = self + self._setup_properties(vobj) + + def _setup_properties(self, vobj): + pl = vobj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(vobj) + + def _get_properties(self): + return [] + + def attach(self, vobj): + self.Object = vobj.Object # used on various places, claim childreens, get icon, etc. + self.ViewObject = vobj + + def isShow(self): + return True + + def onChanged(self, vobj, prop): + + # one of our view properties was changed. Lets inform our parent visualization + # that this happened, as this is the one that needs to redraw + + if prop == "Proxy": + return + + group = vobj.Object.getParentGroup() + if not group: + return + + if hasattr(group.ViewObject, "Proxy") and hasattr( + group.ViewObject.Proxy, "childViewPropertyChanged" + ): + + group.ViewObject.Proxy.childViewPropertyChanged(vobj, prop) + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_extractor._ExtractorTaskPanel(vobj.Object) + + # show it + FreeCADGui.Control.showDialog(taskd) + + return True + + def unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + return True + + def doubleClicked(self, vobj): + guidoc = FreeCADGui.getDocument(vobj.Object.Document) + + # check if another VP is in edit mode and close it then + if guidoc.getInEdit(): + FreeCADGui.Control.closeDialog() + guidoc.resetEdit() + + guidoc.setEdit(vobj.Object.Name) + + return True + + def dumps(self): + return None + + def loads(self, state): + return None + + # To be implemented by subclasses: + # ################################ + + def get_default_color_property(self): + # Returns the property name to set the default color to. + # Return None if no such property + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_default_field_properties(self): + # Returns the property name to which the default field name should be set + # ret: [FieldProperty, ComponentProperty] + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_kw_args(self): + # Returns the matplotlib plot keyword arguments that represent the + # properties of the object. + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_app_edit_widget(self, post_dialog): + # Returns a widgets for editing the object (not viewprovider!) + # The widget will be part of the provided post_dialog, and + # should use its functionality to inform of changes. + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_view_edit_widget(self, post_dialog): + # Returns a widgets for editing the viewprovider (not object!) + # The widget will be part of the provided post_dialog, and + # should use its functionality to inform of changes. + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_preview(self): + # Returns the preview tuple of icon and label: (QPixmap, str) + # Note: QPixmap in ratio 2:1 + raise FreeCAD.Base.FreeCADError("Not implemented") diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py new file mode 100644 index 0000000000..d66b8ac738 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -0,0 +1,124 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing visualization base ViewProvider" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_base_fempostvisualizations +# \ingroup FEM +# \brief view provider for post visualization object + +import FreeCAD +import FreeCADGui + + +class VPPostVisualization: + """ + A View Provider for visualization objects + """ + + def __init__(self, vobj): + vobj.Proxy = self + self._setup_properties(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + + def _setup_properties(self, vobj): + pl = vobj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(vobj) + + def _get_properties(self): + return [] + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def isShow(self): + # Mark ourself as visible in the tree + return True + + def getDisplayModes(self, obj): + return ["Dialog"] + + def doubleClicked(self, vobj): + + guidoc = FreeCADGui.getDocument(vobj.Object.Document) + + # check if another VP is in edit mode and close it then + if guidoc.getInEdit(): + FreeCADGui.Control.closeDialog() + guidoc.resetEdit() + + # open task dialog + guidoc.setEdit(vobj.Object.Name) + + # show visualization + self.show_visualization() + + return True + + def unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + return True + + def updateData(self, obj, prop): + # If the data changed we need to update the visualization + if prop == "Table": + self.update_visualization() + + def onChanged(self, vobj, prop): + # for all property changes we need to update the visualization + self.update_visualization() + + def childViewPropertyChanged(self, vobj, prop): + # One of the extractors view properties has changed, we need to + # update the visualization + self.update_visualization() + + def dumps(self): + return None + + def loads(self, state): + return None + + # To be implemented by subclasses: + # ################################ + + def update_visualization(self): + # The visualization data or any relevant view property has changed, + # and the visualization itself needs to update to reflect that + raise FreeCAD.Base.FreeCADError("Not implemented") + + def show_visualization(self): + # Shows the visualization without going into edit mode + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_next_default_color(self): + # Returns the next default color a new object should use + # Returns color in FreeCAD property notation (r,g,b,a) + # If the relevant extractors do not have color properties, this + # can stay unimplemented + raise FreeCAD.Base.FreeCADError("Not implemented") diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py new file mode 100644 index 0000000000..9f88ee2b57 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -0,0 +1,603 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import Plot +from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP + +import io +import numpy as np +import matplotlib as mpl +from packaging.version import Version + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_base_fempostextractors +from . import view_base_fempostvisualization +from femtaskpanels import task_post_histogram + +from . import view_base_femobject + +_GuiPropHelper = view_base_femobject._GuiPropHelper + + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + vobj = self._object.ViewObject + + self.widget.Legend.setText(vobj.Legend) + self._post_dialog._enumPropertyToCombobox(vobj, "Hatch", self.widget.Hatch) + self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle) + self.widget.LineWidth.setValue(vobj.LineWidth) + self.widget.HatchDensity.setValue(vobj.HatchDensity) + + # setup the color buttons (don't use FreeCADs color button, as this does not work in popups!) + self._setup_color_button(self.widget.BarColor, vobj.BarColor, self.barColorChanged) + self._setup_color_button(self.widget.LineColor, vobj.LineColor, self.lineColorChanged) + + self.widget.Legend.editingFinished.connect(self.legendChanged) + self.widget.Hatch.activated.connect(self.hatchPatternChanged) + self.widget.LineStyle.activated.connect(self.lineStyleChanged) + self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged) + self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) + + # sometimes weird sizes occur with spinboxes + self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height()) + self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) + + def _setup_color_button(self, button, fcColor, callback): + + barColor = QtGui.QColor(*[v * 255 for v in fcColor]) + icon_size = button.iconSize() + icon_size.setWidth(icon_size.width() * 2) + button.setIconSize(icon_size) + pixmap = QtGui.QPixmap(icon_size) + pixmap.fill(barColor) + button.setIcon(pixmap) + + action = QtGui.QWidgetAction(button) + diag = QtGui.QColorDialog(barColor, parent=button) + diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) + diag.accepted.connect(action.trigger) + diag.rejected.connect(action.trigger) + diag.colorSelected.connect(callback) + + action.setDefaultWidget(diag) + button.addAction(action) + button.setPopupMode(QtGui.QToolButton.InstantPopup) + + @QtCore.Slot(QtGui.QColor) + def lineColorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.LineColor.iconSize()) + pixmap.fill(color) + self.widget.LineColor.setIcon(pixmap) + + self._object.ViewObject.LineColor = color.getRgb() + + @QtCore.Slot(QtGui.QColor) + def barColorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.BarColor.iconSize()) + pixmap.fill(color) + self.widget.BarColor.setIcon(pixmap) + + self._object.ViewObject.BarColor = color.getRgb() + + @QtCore.Slot(float) + def lineWidthChanged(self, value): + self._object.ViewObject.LineWidth = value + + @QtCore.Slot(float) + def hatchDensityChanged(self, value): + self._object.ViewObject.HatchDensity = value + + @QtCore.Slot(int) + def hatchPatternChanged(self, index): + self._object.ViewObject.Hatch = index + + @QtCore.Slot(int) + def lineStyleChanged(self, index): + self._object.ViewObject.LineStyle = index + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Legend = self.widget.Legend.text() + + +class EditFieldAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + + +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + + # sometimes weird sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + + +class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): + """ + A View Provider for extraction of 1D field data specially for histograms + """ + + def __init__(self, vobj): + super().__init__(vobj) + vobj.Proxy = self + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Legend", + group="HistogramPlot", + doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"), + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="BarColor", + group="HistogramBar", + doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"), + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Hatch", + group="HistogramBar", + doc=QT_TRANSLATE_NOOP("FEM", "The hatch pattern drawn in the bar"), + value=["None", "/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"], + ), + _GuiPropHelper( + type="App::PropertyIntegerConstraint", + name="HatchDensity", + group="HistogramBar", + doc=QT_TRANSLATE_NOOP("FEM", "The line width of the hatch)"), + value=(1, 1, 99, 1), + ), + _GuiPropHelper( + type="App::PropertyColor", + name="LineColor", + group="HistogramLine", + doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"), + value=(0, 0, 0, 1), # black + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="LineWidth", + group="HistogramLine", + doc=QT_TRANSLATE_NOOP( + "FEM", "The width of the bar, between 0 and 1 (1 being without gaps)" + ), + value=(1, 0, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LineStyle", + group="HistogramLine", + doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), + value=["None", "-", "--", "-.", ":"], + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_app_edit_widget(self, post_dialog): + return EditFieldAppWidget(self.Object, post_dialog) + + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + + fig = mpl.pyplot.figure(figsize=(0.4, 0.2), dpi=500) + ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 2, 1]) + ax.set_axis_off() + fig.add_axes(ax) + + kwargs = self.get_kw_args() + patch = mpl.patches.Rectangle(xy=(0, 0), width=2, height=1, **kwargs) + ax.add_patch(patch) + + data = io.BytesIO() + mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) + mpl.pyplot.close() + + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data.getvalue()) + + return (pixmap, self.ViewObject.Legend) + + def get_kw_args(self): + # builds kw args from the properties + kwargs = {} + + # colors need a workaround, some error occurs with rgba tuple + kwargs["edgecolor"] = self.ViewObject.LineColor + kwargs["facecolor"] = self.ViewObject.BarColor + kwargs["linestyle"] = self.ViewObject.LineStyle + kwargs["linewidth"] = self.ViewObject.LineWidth + if self.ViewObject.Hatch != "None": + kwargs["hatch"] = self.ViewObject.Hatch * self.ViewObject.HatchDensity + + return kwargs + + def get_default_color_property(self): + return "BarColor" + + +class VPPostHistogramIndexOverFrames(VPPostHistogramFieldData): + """ + A View Provider for extraction of 1D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + +class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Histogram plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Cumulative", + group="Histogram", + doc=QT_TRANSLATE_NOOP( + "FEM", "If be the bars should show the cumulative sum left to right" + ), + value=False, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Type", + group="Histogram", + doc=QT_TRANSLATE_NOOP("FEM", "The type of histogram plotted"), + value=["bar", "barstacked", "step", "stepfilled"], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="BarWidth", + group="Histogram", + doc=QT_TRANSLATE_NOOP( + "FEM", "The width of the bar, between 0 and 1 (1 being without gaps)" + ), + value=(0.9, 0, 1, 0.05), + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="HatchLineWidth", + group="Histogram", + doc=QT_TRANSLATE_NOOP("FEM", "The line width of all drawn hatch patterns"), + value=(1, 0, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyInteger", + name="Bins", + group="Histogram", + doc=QT_TRANSLATE_NOOP("FEM", "The number of bins the data is split into"), + value=10, + ), + _GuiPropHelper( + type="App::PropertyString", + name="Title", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="XLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="YLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyBool", + name="Legend", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LegendLocation", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), + value=[ + "best", + "upper right", + "upper left", + "lower left", + "lower right", + "right", + "center left", + "center right", + "lower center", + "upper center", + "center", + ], + ), + ] + return prop + + def getIcon(self): + return ":/icons/FEM_PostHistogram.svg" + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_histogram._TaskPanel(vobj) + + # show it + FreeCADGui.Control.showDialog(taskd) + + return True + + def show_visualization(self): + + if not hasattr(self, "_plot") or not self._plot: + main = FreeCADGui.getMainWindow() + self._plot = Plot.Plot() + self._plot.setWindowTitle(self.Object.Label) + self._plot.setParent(main) + self._plot.setWindowFlags(QtGui.Qt.Dialog) + self._plot.resize(main.size().height() / 2, main.size().height() / 3) # keep it square + self.update_visualization() + + self._plot.show() + + def get_kw_args(self, obj): + view = obj.ViewObject + if not view or not hasattr(view, "Proxy"): + return {} + if not hasattr(view.Proxy, "get_kw_args"): + return {} + return view.Proxy.get_kw_args() + + def update_visualization(self): + + if not hasattr(self, "_plot") or not self._plot: + return + + self._plot.axes.clear() + bins = self.ViewObject.Bins + + # we do not iterate the table, but iterate the children. This makes it possible + # to attribute the correct styles + full_args = [] + full_data = [] + labels = [] + for child in self.Object.Group: + + table = child.Table + kwargs = self.get_kw_args(child) + + # iterate over the table and plot all + color_factor = np.linspace(1, 0.5, table.GetNumberOfColumns()) + legend_multiframe = table.GetNumberOfColumns() > 1 + for i in range(table.GetNumberOfColumns()): + + # add the kw args, with some slide change over color for multiple frames + args = kwargs.copy() + for key in kwargs: + + if "color" in key: + value = np.array(kwargs[key]) * color_factor[i] + args[key] = mpl.colors.to_hex(value) + + full_args.append(args) + + data = VTKArray(table.GetColumn(i)) + full_data.append(data) + + # legend labels + if child.ViewObject.Legend: + if not legend_multiframe: + labels.append(child.ViewObject.Legend) + else: + postfix = table.GetColumnName(i).split("-")[-1] + labels.append(child.ViewObject.Legend + " - " + postfix) + else: + legend_prefix = "" + if len(self.Object.Group) > 1: + legend_prefix = child.Source.Label + ": " + labels.append(legend_prefix + table.GetColumnName(i)) + + args = {} + args["rwidth"] = self.ViewObject.BarWidth + args["cumulative"] = self.ViewObject.Cumulative + args["histtype"] = self.ViewObject.Type + args["label"] = labels + if Version(mpl.__version__) >= Version("3.10.0"): + args["hatch_linewidth"] = self.ViewObject.HatchLineWidth + + n, b, patches = self._plot.axes.hist(full_data, bins, **args) + + # set the patches view properties. + if len(full_args) == 1: + for patch in patches: + patch.set(**full_args[0]) + elif len(full_args) > 1: + for i, args in enumerate(full_args): + for patch in patches[i]: + patch.set(**full_args[i]) + + # axes decoration + if self.ViewObject.Title: + self._plot.axes.set_title(self.ViewObject.Title) + if self.ViewObject.XLabel: + self._plot.axes.set_xlabel(self.ViewObject.XLabel) + if self.ViewObject.YLabel: + self._plot.axes.set_ylabel(self.ViewObject.YLabel) + + if self.ViewObject.Legend and labels: + self._plot.axes.legend(loc=self.ViewObject.LegendLocation) + + self._plot.update() + + def get_next_default_color(self): + # we use the next color in order. We do not check (yet) if this + # color is already taken + i = len(self.Object.Group) + cmap = mpl.pyplot.get_cmap("tab10") + return cmap(i) diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py new file mode 100644 index 0000000000..dd58066604 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -0,0 +1,583 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import Plot +from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP + + +import io +import numpy as np +import matplotlib as mpl + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_base_fempostextractors +from . import view_base_fempostvisualization +from femtaskpanels import task_post_lineplot + +from . import view_base_femobject + +_GuiPropHelper = view_base_femobject._GuiPropHelper + + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + vobj = self._object.ViewObject + + self.widget.Legend.setText(vobj.Legend) + self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle) + self._post_dialog._enumPropertyToCombobox(vobj, "MarkerStyle", self.widget.MarkerStyle) + self.widget.LineWidth.setValue(vobj.LineWidth) + self.widget.MarkerSize.setValue(vobj.MarkerSize) + + self._setup_color_button(self.widget.Color, vobj.Color, self.colorChanged) + + self.widget.Legend.editingFinished.connect(self.legendChanged) + self.widget.MarkerStyle.activated.connect(self.markerStyleChanged) + self.widget.LineStyle.activated.connect(self.lineStyleChanged) + self.widget.MarkerSize.valueChanged.connect(self.markerSizeChanged) + self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) + + # sometimes weird sizes occur with spinboxes + self.widget.MarkerSize.setMaximumHeight(self.widget.MarkerStyle.sizeHint().height()) + self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) + + def _setup_color_button(self, button, fcColor, callback): + + barColor = QtGui.QColor(*[v * 255 for v in fcColor]) + icon_size = button.iconSize() + icon_size.setWidth(icon_size.width() * 2) + button.setIconSize(icon_size) + pixmap = QtGui.QPixmap(icon_size) + pixmap.fill(barColor) + button.setIcon(pixmap) + + action = QtGui.QWidgetAction(button) + diag = QtGui.QColorDialog(barColor, parent=button) + diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) + diag.accepted.connect(action.trigger) + diag.rejected.connect(action.trigger) + diag.colorSelected.connect(callback) + + action.setDefaultWidget(diag) + button.addAction(action) + button.setPopupMode(QtGui.QToolButton.InstantPopup) + + @QtCore.Slot(QtGui.QColor) + def colorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.Color.iconSize()) + pixmap.fill(color) + self.widget.Color.setIcon(pixmap) + + self._object.ViewObject.Color = color.getRgb() + + @QtCore.Slot(float) + def lineWidthChanged(self, value): + self._object.ViewObject.LineWidth = value + + @QtCore.Slot(float) + def markerSizeChanged(self, value): + self._object.ViewObject.MarkerSize = value + + @QtCore.Slot(int) + def markerStyleChanged(self, index): + self._object.ViewObject.MarkerStyle = index + + @QtCore.Slot(int) + def lineStyleChanged(self, index): + self._object.ViewObject.LineStyle = index + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Legend = self.widget.Legend.text() + + +class EditFieldAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.XField) + self._post_dialog._enumPropertyToCombobox( + self._object, "XComponent", self.widget.XComponent + ) + self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.XField.activated.connect(self.xFieldChanged) + self.widget.XComponent.activated.connect(self.xComponentChanged) + self.widget.YField.activated.connect(self.yFieldChanged) + self.widget.YComponent.activated.connect(self.yComponentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def xFieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox( + self._object, "XComponent", self.widget.XComponent + ) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def xComponentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yFieldChanged(self, index): + self._object.YField = index + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yComponentChanged(self, index): + self._object.YComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + + +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.YField.activated.connect(self.yFieldChanged) + self.widget.YComponent.activated.connect(self.yComponentChanged) + + # sometimes weird sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.YField.sizeHint().height()) + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yFieldChanged(self, index): + self._object.YField = index + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yComponentChanged(self, index): + self._object.YComponent = index + self._post_dialog._recompute() + + +class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): + """ + A View Provider for extraction of 2D field data specially for histograms + """ + + def __init__(self, vobj): + super().__init__(vobj) + vobj.Proxy = self + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Legend", + group="Lineplot", + doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"), + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="Color", + group="Lineplot", + doc=QT_TRANSLATE_NOOP("FEM", "The color the line and the markers are drawn with"), + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LineStyle", + group="Lineplot", + doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), + value=["-", "--", "-.", ":", "None"], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="LineWidth", + group="Lineplot", + doc=QT_TRANSLATE_NOOP("FEM", "The width the line is drawn with"), + value=(1, 0.1, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="MarkerStyle", + group="Lineplot", + doc=QT_TRANSLATE_NOOP("FEM", "The style the data markers are drawn with"), + value=["None", "*", "+", "s", ".", "o", "x"], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="MarkerSize", + group="Lineplot", + doc=QT_TRANSLATE_NOOP("FEM", "The size the data markers are drawn in"), + value=(5, 0.1, 99, 0.1), + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_app_edit_widget(self, post_dialog): + return EditFieldAppWidget(self.Object, post_dialog) + + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + # Returns the preview tuple of icon and label: (QPixmap, str) + # Note: QPixmap in ratio 2:1 + + fig = mpl.pyplot.figure(figsize=(0.2, 0.1), dpi=1000) + ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 1.0, 1.0]) + ax.set_axis_off() + fig.add_axes(ax) + kwargs = self.get_kw_args() + kwargs["markevery"] = [1] + ax.plot([0, 0.5, 1], [0.5, 0.5, 0.5], **kwargs) + data = io.BytesIO() + mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) + mpl.pyplot.close() + + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data.getvalue()) + + return (pixmap, self.ViewObject.Legend) + + def get_kw_args(self): + # builds kw args from the properties + kwargs = {} + + # colors need a workaround, some error occurs with rgba tuple + kwargs["color"] = self.ViewObject.Color + kwargs["markeredgecolor"] = self.ViewObject.Color + kwargs["markerfacecolor"] = self.ViewObject.Color + kwargs["linestyle"] = self.ViewObject.LineStyle + kwargs["linewidth"] = self.ViewObject.LineWidth + kwargs["marker"] = self.ViewObject.MarkerStyle + kwargs["markersize"] = self.ViewObject.MarkerSize + return kwargs + + def get_default_color_property(self): + return "Color" + + +class VPPostLineplotIndexOverFrames(VPPostLineplotFieldData): + """ + A View Provider for extraction of 2D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + +class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Lineplot plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Grid", + group="Lineplot", + doc=QT_TRANSLATE_NOOP( + "FEM", "If be the bars should show the cumulative sum left to right" + ), + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Scale", + group="Lineplot", + doc=QT_TRANSLATE_NOOP("FEM", "The scale the axis are drawn in"), + value=["linear", "semi-log x", "semi-log y", "log"], + ), + _GuiPropHelper( + type="App::PropertyString", + name="Title", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="XLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="YLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyBool", + name="Legend", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LegendLocation", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), + value=[ + "best", + "upper right", + "upper left", + "lower left", + "lower right", + "right", + "center left", + "center right", + "lower center", + "upper center", + "center", + ], + ), + ] + return prop + + def getIcon(self): + return ":/icons/FEM_PostLineplot.svg" + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_lineplot._TaskPanel(vobj) + + # show it + FreeCADGui.Control.showDialog(taskd) + + return True + + def show_visualization(self): + + if not hasattr(self, "_plot") or not self._plot: + main = FreeCADGui.getMainWindow() + self._plot = Plot.Plot() + self._plot.setWindowTitle(self.Object.Label) + self._plot.setParent(main) + self._plot.setWindowFlags(QtGui.Qt.Dialog) + self._plot.resize( + main.size().height() / 2, main.size().height() / 3 + ) # keep the aspect ratio + self.update_visualization() + + self._plot.show() + + def get_kw_args(self, obj): + view = obj.ViewObject + if not view or not hasattr(view, "Proxy"): + return {} + if not hasattr(view.Proxy, "get_kw_args"): + return {} + return view.Proxy.get_kw_args() + + def update_visualization(self): + + if not hasattr(self, "_plot") or not self._plot: + return + + self._plot.axes.clear() + + # we do not iterate the table, but iterate the children. This makes it possible + # to attribute the correct styles + plotted = False + for child in self.Object.Group: + + table = child.Table + kwargs = self.get_kw_args(child) + + # iterate over the table and plot all (note: column 0 is always X!) + color_factor = np.linspace(1, 0.5, int(table.GetNumberOfColumns() / 2)) + legend_multiframe = table.GetNumberOfColumns() > 2 + + for i in range(0, table.GetNumberOfColumns(), 2): + + plotted = True + + # add the kw args, with some slide change over color for multiple frames + tmp_args = {} + for key in kwargs: + if "color" in key: + value = np.array(kwargs[key]) * color_factor[int(i / 2)] + tmp_args[key] = mpl.colors.to_hex(value) + else: + tmp_args[key] = kwargs[key] + + xdata = VTKArray(table.GetColumn(i)) + ydata = VTKArray(table.GetColumn(i + 1)) + + # ensure points are visible if it is a single datapoint + if len(xdata) == 1 and tmp_args["marker"] == "None": + tmp_args["marker"] = "o" + + # legend labels + if child.ViewObject.Legend: + if not legend_multiframe: + label = child.ViewObject.Legend + else: + postfix = table.GetColumnName(i + 1).split("-")[-1] + label = child.ViewObject.Legend + " - " + postfix + else: + legend_prefix = "" + if len(self.Object.Group) > 1: + legend_prefix = child.Source.Label + ": " + label = legend_prefix + table.GetColumnName(i + 1) + + match self.ViewObject.Scale: + case "log": + self._plot.axes.loglog(xdata, ydata, **tmp_args, label=label) + case "semi-log x": + self._plot.axes.semilogx(xdata, ydata, **tmp_args, label=label) + case "semi-log y": + self._plot.axes.semilogy(xdata, ydata, **tmp_args, label=label) + case _: + self._plot.axes.plot(xdata, ydata, **tmp_args, label=label) + + if self.ViewObject.Title: + self._plot.axes.set_title(self.ViewObject.Title) + if self.ViewObject.XLabel: + self._plot.axes.set_xlabel(self.ViewObject.XLabel) + if self.ViewObject.YLabel: + self._plot.axes.set_ylabel(self.ViewObject.YLabel) + + if self.ViewObject.Legend and plotted: + self._plot.axes.legend(loc=self.ViewObject.LegendLocation) + + self._plot.axes.grid(self.ViewObject.Grid) + self._plot.update() + + def get_next_default_color(self): + # we use the next color in order. We do not check (yet) if this + # color is already taken + i = len(self.Object.Group) + cmap = mpl.pyplot.get_cmap("tab10") + return cmap(i) diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py new file mode 100644 index 0000000000..8d00e9db4b --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -0,0 +1,291 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * 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. * +# * * +# * 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 * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing table ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_table +# \ingroup FEM +# \brief view provider for post table object + +import FreeCAD +import FreeCADGui + +from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP + +from . import view_base_fempostextractors +from . import view_base_fempostvisualization +from femtaskpanels import task_post_table +from femguiutils import vtk_table_view as vtv + +from . import view_base_femobject + +_GuiPropHelper = view_base_femobject._GuiPropHelper + + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostTableFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + self.widget.Name.setText(self._object.ViewObject.Name) + self.widget.Name.editingFinished.connect(self.legendChanged) + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Name = self.widget.Name.text() + + +class EditFieldAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up (we reuse histogram, as we need the exact same) + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + + +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up (we reuse histogram, as we need the exact same) + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + + # sometimes weird sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + + +class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): + """ + A View Provider for extraction of 1D field data specially for tables + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Name", + group="Table", + doc=QT_TRANSLATE_NOOP( + "FEM", "The name used in the table header. Default name is used if empty" + ), + value="", + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_app_edit_widget(self, post_dialog): + return EditFieldAppWidget(self.Object, post_dialog) + + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + name = QT_TRANSLATE_NOOP("FEM", "default") + if self.ViewObject.Name: + name = self.ViewObject.Name + return (QtGui.QPixmap(), name) + + def get_default_color_property(self): + return None + + +class VPPostTableIndexOverFrames(VPPostTableFieldData): + """ + A View Provider for extraction of 1D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + +class VPPostTable(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Table plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostSpreadsheet.svg" + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_table._TaskPanel(vobj) + + # show it + FreeCADGui.Control.showDialog(taskd) + + return True + + def show_visualization(self): + + if not hasattr(self, "_tableview") or not self._tableview: + main = FreeCADGui.getMainWindow() + self._tableModel = vtv.VtkTableModel() + self._tableview = vtv.VtkTableView(self._tableModel) + self._tableview.setWindowTitle(self.Object.Label) + self._tableview.setParent(main) + self._tableview.setWindowFlags(QtGui.Qt.Dialog) + self._tableview.resize( + main.size().height() / 2, main.size().height() / 3 + ) # keep the aspect ratio + + self.update_visualization() + + self._tableview.show() + + def update_visualization(self): + + if not hasattr(self, "_tableModel") or not self._tableModel: + return + + # we collect the header names from the viewproviders + table = self.Object.Table + header = {} + for child in self.Object.Group: + + if not child.Source: + continue + + new_name = child.ViewObject.Name + if new_name: + # this child uses a custom name. We try to find all + # columns that are from this child and use custom header for it + for i in range(table.GetNumberOfColumns()): + if child.Source.Name in table.GetColumnName(i): + header[table.GetColumnName(i)] = new_name + + self._tableModel.setTable(self.Object.Table, header) diff --git a/src/Mod/Help/Help.py b/src/Mod/Help/Help.py index 344e83b734..291f0aa4e2 100644 --- a/src/Mod/Help/Help.py +++ b/src/Mod/Help/Help.py @@ -169,7 +169,7 @@ def location_url(url_localized: str, url_english: str) -> tuple: req = urllib.request.Request(url_localized) with urllib.request.urlopen(req) as response: html = response.read().decode("utf-8") - if re.search(MD_RAW_URL, url_localized): + if url_localized.startswith(MD_RAW_URL): pagename_match = re.search(r"Name/.*?:\s*(.+)", html) else: # Pages from FreeCAD Wiki fall here diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 115f9a1125..6b9279deaf 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -207,6 +207,10 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, const Base::Vector3d& end, bool /*hidden*/) { + if (shouldSkipEntity()) { + return; + } + gp_Pnt p0 = makePoint(start); gp_Pnt p1 = makePoint(end); if (p0.IsEqual(p1, 0.00000001)) { @@ -219,6 +223,10 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, void ImpExpDxfRead::OnReadPoint(const Base::Vector3d& start) { + if (shouldSkipEntity()) { + return; + } + Collector->AddObject(BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(), "Point"); } @@ -229,6 +237,10 @@ void ImpExpDxfRead::OnReadArc(const Base::Vector3d& start, bool dir, bool /*hidden*/) { + if (shouldSkipEntity()) { + return; + } + gp_Pnt p0 = makePoint(start); gp_Pnt p1 = makePoint(end); gp_Dir up(0, 0, 1); @@ -251,6 +263,10 @@ void ImpExpDxfRead::OnReadCircle(const Base::Vector3d& start, bool dir, bool /*hidden*/) { + if (shouldSkipEntity()) { + return; + } + gp_Pnt p0 = makePoint(start); gp_Dir up(0, 0, 1); if (!dir) { @@ -393,6 +409,10 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center, bool dir) // NOLINTEND(bugprone-easily-swappable-parameters) { + if (shouldSkipEntity()) { + return; + } + gp_Dir up(0, 0, 1); if (!dir) { up = -up; @@ -414,6 +434,10 @@ void ImpExpDxfRead::OnReadText(const Base::Vector3d& point, const std::string& text, const double rotation) { + if (shouldSkipEntity()) { + return; + } + // Note that our parameters do not contain all the information needed to properly orient the // text. As a result the text will always appear on the XY plane if (m_importAnnotations) { @@ -454,6 +478,10 @@ void ImpExpDxfRead::OnReadInsert(const Base::Vector3d& point, const std::string& name, double rotation) { + if (shouldSkipEntity()) { + return; + } + Collector->AddInsert(point, scale, name, rotation); } void ImpExpDxfRead::ExpandInsert(const std::string& name, @@ -534,6 +562,10 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start, const Base::Vector3d& point, double /*rotation*/) { + if (shouldSkipEntity()) { + return; + } + if (m_importAnnotations) { auto makeDimension = [this, start, end, point](const Base::Matrix4D& transform) -> App::FeaturePython* { @@ -576,6 +608,10 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start, } void ImpExpDxfRead::OnReadPolyline(std::list& vertices, int flags) { + if (shouldSkipEntity()) { + return; + } + std::map> ShapesToCombine; { // TODO: Currently ExpandPolyline calls OnReadArc etc to generate the pieces, and these diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index c7f86715d5..c486e6eb7d 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -108,6 +108,11 @@ public: void setOptions(); private: + bool shouldSkipEntity() const + { + // This entity is in paper space, and the user setting says to ignore it. + return !m_importPaperSpaceEntities && m_entityAttributes.m_paperSpace; + } static gp_Pnt makePoint(const Base::Vector3d& point3d) { return {point3d.x, point3d.y, point3d.z}; diff --git a/src/Mod/Measure/App/MeasurePosition.cpp b/src/Mod/Measure/App/MeasurePosition.cpp index aa3d3160a2..8c871c29fa 100644 --- a/src/Mod/Measure/App/MeasurePosition.cpp +++ b/src/Mod/Measure/App/MeasurePosition.cpp @@ -95,7 +95,9 @@ App::DocumentObjectExecReturn* MeasurePosition::execute() { const App::DocumentObject* object = Element.getValue(); const std::vector& subElements = Element.getSubValues(); - + if (subElements.empty()) { + return {}; + } App::SubObjectT subject {object, subElements.front().c_str()}; auto info = getMeasureInfo(subject); diff --git a/src/Mod/Measure/App/Measurement.cpp b/src/Mod/Measure/App/Measurement.cpp index 73ae0344cc..6ab6c72dec 100644 --- a/src/Mod/Measure/App/Measurement.cpp +++ b/src/Mod/Measure/App/Measurement.cpp @@ -239,7 +239,7 @@ MeasureType Measurement::findType() } else if (edges > 0) { if (verts > 0) { - if (verts > 1 && edges > 0) { + if (verts > 1) { mode = MeasureType::Invalid; } else { diff --git a/src/Mod/Measure/Gui/AppMeasureGui.cpp b/src/Mod/Measure/Gui/AppMeasureGui.cpp index 2897c1591b..4da57110ef 100644 --- a/src/Mod/Measure/Gui/AppMeasureGui.cpp +++ b/src/Mod/Measure/Gui/AppMeasureGui.cpp @@ -37,7 +37,6 @@ #include "ViewProviderMeasureAngle.h" #include "ViewProviderMeasureDistance.h" #include "ViewProviderMeasureBase.h" -#include "WorkbenchManipulator.h" // use a different name to CreateCommand() @@ -87,9 +86,6 @@ PyMOD_INIT_FUNC(MeasureGui) PyObject* mod = MeasureGui::initModule(); Base::Console().log("Loading GUI of Measure module... done\n"); - auto manip = std::make_shared(); - Gui::WorkbenchManipulator::installManipulator(manip); - // instantiating the commands CreateMeasureCommands(); diff --git a/src/Mod/Measure/Gui/CMakeLists.txt b/src/Mod/Measure/Gui/CMakeLists.txt index ef1909afc1..68acddfbbe 100644 --- a/src/Mod/Measure/Gui/CMakeLists.txt +++ b/src/Mod/Measure/Gui/CMakeLists.txt @@ -50,8 +50,6 @@ SET(MeasureGui_SRCS ViewProviderMeasureAngle.h ViewProviderMeasureDistance.cpp ViewProviderMeasureDistance.h - WorkbenchManipulator.cpp - WorkbenchManipulator.h DlgPrefsMeasureAppearanceImp.ui DlgPrefsMeasureAppearanceImp.cpp DlgPrefsMeasureAppearanceImp.h diff --git a/src/Mod/Measure/Gui/WorkbenchManipulator.cpp b/src/Mod/Measure/Gui/WorkbenchManipulator.cpp deleted file mode 100644 index 0041db16d7..0000000000 --- a/src/Mod/Measure/Gui/WorkbenchManipulator.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/*************************************************************************** - * Copyright (c) 2024 David Friedli * - * * - * This file is part of FreeCAD. * - * * - * FreeCAD is free software: you can redistribute it and/or modify it * - * under the terms of the GNU Lesser General Public License as * - * published by the Free Software Foundation, either version 2.1 of the * - * License, or (at your option) any later version. * - * * - * FreeCAD is distributed in the hope that it will be useful, but * - * WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * - * Lesser General Public License for more details. * - * * - * You should have received a copy of the GNU Lesser General Public * - * License along with FreeCAD. If not, see * - * . * - * * - **************************************************************************/ - - -#include "PreCompiled.h" -#include "WorkbenchManipulator.h" -#include -#include - -using namespace MeasureGui; - -void WorkbenchManipulator::modifyMenuBar([[maybe_unused]] Gui::MenuItem* menuBar) -{ - auto menuTools = menuBar->findItem("&Tools"); - if (!menuTools) { - return; - } - auto itemMeasure = new Gui::MenuItem(); - itemMeasure->setCommand("Std_Measure"); - menuTools->appendItem(itemMeasure); -} - -void WorkbenchManipulator::modifyToolBars(Gui::ToolBarItem* toolBar) -{ - auto tbView = toolBar->findItem("View"); - if (!tbView) { - return; - } - - auto itemMeasure = new Gui::ToolBarItem(); - itemMeasure->setCommand("Std_Measure"); - tbView->appendItem(itemMeasure); -} diff --git a/src/Mod/Measure/Gui/WorkbenchManipulator.h b/src/Mod/Measure/Gui/WorkbenchManipulator.h deleted file mode 100644 index 767f1e058c..0000000000 --- a/src/Mod/Measure/Gui/WorkbenchManipulator.h +++ /dev/null @@ -1,41 +0,0 @@ -/*************************************************************************** - * Copyright (c) 2024 David Friedli * - * * - * This file is part of FreeCAD. * - * * - * FreeCAD is free software: you can redistribute it and/or modify it * - * under the terms of the GNU Lesser General Public License as * - * published by the Free Software Foundation, either version 2.1 of the * - * License, or (at your option) any later version. * - * * - * FreeCAD is distributed in the hope that it will be useful, but * - * WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * - * Lesser General Public License for more details. * - * * - * You should have received a copy of the GNU Lesser General Public * - * License along with FreeCAD. If not, see * - * . * - * * - **************************************************************************/ - - -#ifndef MEASUREGUI_WORKBENCHMANIPULATOR_H -#define MEASUREGUI_WORKBENCHMANIPULATOR_H - -#include - -namespace MeasureGui -{ - -class WorkbenchManipulator: public Gui::WorkbenchManipulator -{ -protected: - void modifyMenuBar(Gui::MenuItem* menuBar) override; - void modifyToolBars(Gui::ToolBarItem* toolBar) override; -}; - -} // namespace MeasureGui - - -#endif // MEASUREGUI_WORKBENCHMANIPULATOR_H diff --git a/src/Mod/Part/App/PartFeature.cpp b/src/Mod/Part/App/PartFeature.cpp index fbaa479fe7..5ec7a459fa 100644 --- a/src/Mod/Part/App/PartFeature.cpp +++ b/src/Mod/Part/App/PartFeature.cpp @@ -1776,11 +1776,19 @@ bool Feature::getCameraAlignmentDirection(Base::Vector3d& directionZ, Base::Vect // Edge direction const size_t edgeCount = topoShape.countSubShapes(TopAbs_EDGE); - if (edgeCount == 1 && topoShape.isLinearEdge()) { - if (const std::unique_ptr geometry = Geometry::fromShape(topoShape.getSubShape(TopAbs_EDGE, 1), true)) { - const std::unique_ptr geomLine(static_cast(geometry.get())->toLine()); - if (geomLine) { - directionZ = geomLine->getDir().Normalize(); + if (edgeCount == 1) { + if (topoShape.isLinearEdge()) { + if (const std::unique_ptr geometry = Geometry::fromShape(topoShape.getSubShape(TopAbs_EDGE, 1), true)) { + const std::unique_ptr geomLine(static_cast(geometry.get())->toLine()); + if (geomLine) { + directionZ = geomLine->getDir().Normalize(); + return true; + } + } + } else { + // Planar curves + if (gp_Pln plane; topoShape.findPlane(plane)) { + directionZ = Base::Vector3d(plane.Axis().Direction().X(), plane.Axis().Direction().Y(), plane.Axis().Direction().Z()).Normalize(); return true; } } diff --git a/src/Mod/Part/App/PartFeatures.cpp b/src/Mod/Part/App/PartFeatures.cpp index 4a8fe0d37a..dab9d9219d 100644 --- a/src/Mod/Part/App/PartFeatures.cpp +++ b/src/Mod/Part/App/PartFeatures.cpp @@ -243,12 +243,6 @@ App::DocumentObjectExecReturn* Loft::execute() } } -void Part::Loft::setupObject() -{ - Feature::setupObject(); -// Linearize.setValue(PartParams::getLinearizeExtrusionDraft()); // TODO: Resolve after PartParams -} - // ---------------------------------------------------------------------------- const char* Part::Sweep::TransitionEnums[] = {"Transformed", @@ -349,12 +343,6 @@ App::DocumentObjectExecReturn* Sweep::execute() } } -void Part::Sweep::setupObject() -{ - Feature::setupObject(); -// Linearize.setValue(PartParams::getLinearizeExtrusionDraft()); // TODO: Resolve after PartParams -} - // ---------------------------------------------------------------------------- const char* Part::Thickness::ModeEnums[] = {"Skin", "Pipe", "RectoVerso", nullptr}; diff --git a/src/Mod/Part/App/PartFeatures.h b/src/Mod/Part/App/PartFeatures.h index dacb2cd5b9..f0b408200c 100644 --- a/src/Mod/Part/App/PartFeatures.h +++ b/src/Mod/Part/App/PartFeatures.h @@ -50,7 +50,6 @@ public: short mustExecute() const override; const char* getViewProviderName() const override { return "PartGui::ViewProviderRuledSurface"; - void setupObject(); } //@} @@ -86,7 +85,6 @@ public: const char* getViewProviderName() const override { return "PartGui::ViewProviderLoft"; } - void setupObject() override; //@} protected: @@ -118,7 +116,6 @@ public: const char* getViewProviderName() const override { return "PartGui::ViewProviderSweep"; } - void setupObject() override; //@} protected: diff --git a/src/Mod/PartDesign/App/FeatureHole.cpp b/src/Mod/PartDesign/App/FeatureHole.cpp index d259e1145c..eeaac1f634 100644 --- a/src/Mod/PartDesign/App/FeatureHole.cpp +++ b/src/Mod/PartDesign/App/FeatureHole.cpp @@ -376,6 +376,7 @@ const std::vector Hole::threadDescription[] = { "7/8", 22.225, 1.814, 20.40 }, { "1", 25.400, 2.117, 23.25 }, { "1 1/8", 28.575, 2.117, 26.50 }, + { "1 3/16", 30.163, 1.588, 28.58 }, { "1 1/4", 31.750, 2.117, 29.50 }, { "1 3/8", 34.925, 2.117, 32.75 }, { "1 1/2", 38.100, 2.117, 36.00 }, @@ -613,7 +614,7 @@ const double Hole::metricHoleDiameters[51][4] = { 150.0, 155.0, 158.0, 165.0} }; -const Hole::UTSClearanceDefinition Hole::UTSHoleDiameters[22] = +const Hole::UTSClearanceDefinition Hole::UTSHoleDiameters[23] = { /* 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). @@ -641,6 +642,7 @@ const Hole::UTSClearanceDefinition Hole::UTSHoleDiameters[22] = { "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 3/16", 31.0, 32.5, 34.9 }, { "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 } diff --git a/src/Mod/PartDesign/App/FeatureHole.h b/src/Mod/PartDesign/App/FeatureHole.h index 96b8a91d34..d1ecf74371 100644 --- a/src/Mod/PartDesign/App/FeatureHole.h +++ b/src/Mod/PartDesign/App/FeatureHole.h @@ -116,7 +116,7 @@ public: double normal; double loose; }; - static const UTSClearanceDefinition UTSHoleDiameters[22]; + static const UTSClearanceDefinition UTSHoleDiameters[23]; void Restore(Base::XMLReader & reader) override; diff --git a/src/Mod/PartDesign/PartDesignTests/TestHole.py b/src/Mod/PartDesign/PartDesignTests/TestHole.py index a7af5d0194..37b0aa443b 100644 --- a/src/Mod/PartDesign/PartDesignTests/TestHole.py +++ b/src/Mod/PartDesign/PartDesignTests/TestHole.py @@ -269,7 +269,7 @@ class TestHole(unittest.TestCase): "#0", "#1", "#2", "#3", "#4", "#5", "#6", "#8", "#10", "#12", "1/4", "5/16", "3/8", "7/16", "1/2", "9/16", - "5/8", "3/4", "7/8", "1", "1 1/8", "1 1/4", + "5/8", "3/4", "7/8", "1", "1 1/8", "1 3/16", "1 1/4", "1 3/8", "1 1/2", ], 'UNEF': [ diff --git a/src/Mod/Plot/Plot.py b/src/Mod/Plot/Plot.py index 4f5a360745..a3423ae93d 100644 --- a/src/Mod/Plot/Plot.py +++ b/src/Mod/Plot/Plot.py @@ -28,7 +28,7 @@ import sys try: import matplotlib - matplotlib.use("Qt5Agg") + matplotlib.use("QtAgg") # Force matplotlib to use PySide backend by temporarily unloading PyQt if "PyQt5.QtCore" in sys.modules: @@ -36,10 +36,11 @@ try: import matplotlib.pyplot as plt import PyQt5.QtCore else: + print("default matplotlib import") import matplotlib.pyplot as plt - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas - from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar + from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas + from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure except ImportError: diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 192aae8a6c..1102153654 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -616,13 +616,6 @@ int SketchObject::solve(bool updateGeoAfterSolving /*=true*/) } } } - else if (err < 0) { - // if solver failed, invalid constraints were likely added before solving - // (see solve in addConstraint), so solver information is definitely invalid. - // - // Update: ViewProviderSketch shall now rely on the signalSolverUpdate below for update - // this->Constraints.touch(); - } signalSolverUpdate(); @@ -1791,8 +1784,9 @@ int SketchObject::delGeometry(int GeoId, bool deleteinternalgeo) Base::StateLocker lock(managedoperation, true); const std::vector& vals = getInternalGeometry(); - if (GeoId < 0 || GeoId >= int(vals.size())) + if (GeoId >= int(vals.size())) { return -1; + } if (deleteinternalgeo && hasInternalGeometry(getGeometry(GeoId))) { // Only for supported types @@ -7575,8 +7569,9 @@ const Part::Geometry* SketchObject::_getGeometry(int GeoId) const if (GeoId < int(geomlist.size())) return geomlist[GeoId]; } - else if (GeoId < 0 && -GeoId-1 < ExternalGeo.getSize()) + else if (-GeoId-1 < ExternalGeo.getSize()) { return ExternalGeo[-GeoId-1]; + } return nullptr; } diff --git a/src/Mod/Sketcher/App/planegcs/Constraints.cpp b/src/Mod/Sketcher/App/planegcs/Constraints.cpp index 336d20e3d2..b6f564ca2b 100644 --- a/src/Mod/Sketcher/App/planegcs/Constraints.cpp +++ b/src/Mod/Sketcher/App/planegcs/Constraints.cpp @@ -83,16 +83,6 @@ void Constraint::rescale(double coef) scale = coef * 1.0; } -double Constraint::error() -{ - return 0.0; -} - -double Constraint::grad(double* /*param*/) -{ - return 0.0; -} - double Constraint::maxStep(MAP_pD_D& /*dir*/, double lim) { return lim; @@ -114,8 +104,8 @@ int Constraint::findParamInPvec(double* param) // -------------------------------------------------------- // Equal ConstraintEqual::ConstraintEqual(double* p1, double* p2, double p1p2ratio) + : ratio(p1p2ratio) { - ratio = p1p2ratio; pvec.push_back(p1); pvec.push_back(p2); origpvec = pvec; @@ -127,11 +117,6 @@ ConstraintType ConstraintEqual::getTypeId() return Equal; } -void ConstraintEqual::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintEqual::error() { return scale * (*param1() - ratio * (*param2())); @@ -171,11 +156,6 @@ ConstraintType ConstraintWeightedLinearCombination::getTypeId() return WeightedLinearCombination; } -void ConstraintWeightedLinearCombination::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintWeightedLinearCombination::error() { // Explanation of the math here: @@ -232,9 +212,10 @@ double ConstraintWeightedLinearCombination::grad(double* param) ConstraintCenterOfGravity::ConstraintCenterOfGravity(const std::vector& givenpvec, const std::vector& givenweights) : weights(givenweights) + , numpoints(givenpvec.size() - 1) { pvec = givenpvec; - numpoints = pvec.size() - 1; + assert(pvec.size() > 1); assert(weights.size() == numpoints); origpvec = pvec; @@ -246,11 +227,6 @@ ConstraintType ConstraintCenterOfGravity::getTypeId() return CenterOfGravity; } -void ConstraintCenterOfGravity::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintCenterOfGravity::error() { double sum = 0; @@ -332,7 +308,7 @@ ConstraintSlopeAtBSplineKnot::ConstraintSlopeAtBSplineKnot(BSpline& b, Line& l, } origpvec = pvec; - rescale(); + ConstraintSlopeAtBSplineKnot::rescale(); } ConstraintType ConstraintSlopeAtBSplineKnot::getTypeId() @@ -555,11 +531,6 @@ void ConstraintPointOnBSpline::setStartPole(double u) } } -void ConstraintPointOnBSpline::rescale(double coef) -{ - scale = coef * 1.0; -} - double ConstraintPointOnBSpline::error() { if (*theparam() < bsp.flattenedknots[startpole + bsp.degree] @@ -666,11 +637,6 @@ ConstraintType ConstraintDifference::getTypeId() return Difference; } -void ConstraintDifference::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintDifference::error() { return scale * (*param2() - *param1() - *difference()); @@ -710,11 +676,6 @@ ConstraintType ConstraintP2PDistance::getTypeId() return P2PDistance; } -void ConstraintP2PDistance::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintP2PDistance::error() { double dx = (*p1x() - *p2x()); @@ -811,11 +772,6 @@ ConstraintType ConstraintP2PAngle::getTypeId() return P2PAngle; } -void ConstraintP2PAngle::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintP2PAngle::error() { double dx = (*p2x() - *p1x()); @@ -897,11 +853,6 @@ ConstraintType ConstraintP2LDistance::getTypeId() return P2LDistance; } -void ConstraintP2LDistance::rescale(double coef) -{ - scale = coef; -} - double ConstraintP2LDistance::error() { double x0 = *p0x(), x1 = *p1x(), x2 = *p2x(); @@ -1041,11 +992,6 @@ ConstraintType ConstraintPointOnLine::getTypeId() return PointOnLine; } -void ConstraintPointOnLine::rescale(double coef) -{ - scale = coef; -} - double ConstraintPointOnLine::error() { double x0 = *p0x(), x1 = *p1x(), x2 = *p2x(); @@ -1123,11 +1069,6 @@ ConstraintType ConstraintPointOnPerpBisector::getTypeId() return PointOnPerpBisector; } -void ConstraintPointOnPerpBisector::rescale(double coef) -{ - scale = coef; -} - void ConstraintPointOnPerpBisector::errorgrad(double* err, double* grad, double* param) { DeriVector2 p0(Point(p0x(), p0y()), param); @@ -1152,26 +1093,6 @@ void ConstraintPointOnPerpBisector::errorgrad(double* err, double* grad, double* } } -double ConstraintPointOnPerpBisector::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintPointOnPerpBisector::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // Parallel @@ -1186,7 +1107,7 @@ ConstraintParallel::ConstraintParallel(Line& l1, Line& l2) pvec.push_back(l2.p2.x); pvec.push_back(l2.p2.y); origpvec = pvec; - rescale(); + ConstraintParallel::rescale(); } ConstraintType ConstraintParallel::getTypeId() @@ -1258,7 +1179,7 @@ ConstraintPerpendicular::ConstraintPerpendicular(Line& l1, Line& l2) pvec.push_back(l2.p2.x); pvec.push_back(l2.p2.y); origpvec = pvec; - rescale(); + ConstraintPerpendicular::rescale(); } ConstraintPerpendicular::ConstraintPerpendicular(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2) @@ -1272,7 +1193,7 @@ ConstraintPerpendicular::ConstraintPerpendicular(Point& l1p1, Point& l1p2, Point pvec.push_back(l2p2.x); pvec.push_back(l2p2.y); origpvec = pvec; - rescale(); + ConstraintPerpendicular::rescale(); } ConstraintType ConstraintPerpendicular::getTypeId() @@ -1372,11 +1293,6 @@ ConstraintType ConstraintL2LAngle::getTypeId() return L2LAngle; } -void ConstraintL2LAngle::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintL2LAngle::error() { double dx1 = (*l1p2x() - *l1p1x()); @@ -1497,11 +1413,6 @@ ConstraintType ConstraintMidpointOnLine::getTypeId() return MidpointOnLine; } -void ConstraintMidpointOnLine::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintMidpointOnLine::error() { double x0 = ((*l1p1x()) + (*l1p2x())) / 2; @@ -1565,8 +1476,9 @@ ConstraintTangentCircumf::ConstraintTangentCircumf(Point& p1, double* rad1, double* rad2, bool internal_) + : internal(internal_) { - internal = internal_; + pvec.push_back(p1.x); pvec.push_back(p1.y); pvec.push_back(p2.x); @@ -1582,11 +1494,6 @@ ConstraintType ConstraintTangentCircumf::getTypeId() return TangentCircumf; } -void ConstraintTangentCircumf::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintTangentCircumf::error() { double dx = (*c1x() - *c2x()); @@ -1659,11 +1566,6 @@ ConstraintType ConstraintPointOnEllipse::getTypeId() return PointOnEllipse; } -void ConstraintPointOnEllipse::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintPointOnEllipse::error() { double X_0 = *p1x(); @@ -1737,12 +1639,13 @@ double ConstraintPointOnEllipse::grad(double* param) // -------------------------------------------------------- // ConstraintEllipseTangentLine ConstraintEllipseTangentLine::ConstraintEllipseTangentLine(Line& l, Ellipse& e) + : l(l) + , e(e) { - this->l = l; - this->l.PushOwnParams(pvec); - this->e = e; + this->l.PushOwnParams(pvec); this->e.PushOwnParams(pvec); // DeepSOIC: hopefully, this won't push arc's parameters + origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -1761,11 +1664,6 @@ ConstraintType ConstraintEllipseTangentLine::getTypeId() return TangentEllipseLine; } -void ConstraintEllipseTangentLine::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEllipseTangentLine::errorgrad(double* err, double* grad, double* param) { // DeepSOIC equation @@ -1803,26 +1701,6 @@ void ConstraintEllipseTangentLine::errorgrad(double* err, double* grad, double* } } -double ConstraintEllipseTangentLine::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEllipseTangentLine::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintInternalAlignmentPoint2Ellipse @@ -1830,13 +1708,13 @@ ConstraintInternalAlignmentPoint2Ellipse::ConstraintInternalAlignmentPoint2Ellip Ellipse& e, Point& p1, InternalAlignmentType alignmentType) + : e(e) + , p(p1) + , AlignmentType(alignmentType) { - this->p = p1; pvec.push_back(p.x); pvec.push_back(p.y); - this->e = e; this->e.PushOwnParams(pvec); - this->AlignmentType = alignmentType; origpvec = pvec; rescale(); } @@ -1857,11 +1735,6 @@ ConstraintType ConstraintInternalAlignmentPoint2Ellipse::getTypeId() return InternalAlignmentPoint2Ellipse; } -void ConstraintInternalAlignmentPoint2Ellipse::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintInternalAlignmentPoint2Ellipse::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -1925,26 +1798,6 @@ void ConstraintInternalAlignmentPoint2Ellipse::errorgrad(double* err, double* gr } } -double ConstraintInternalAlignmentPoint2Ellipse::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintInternalAlignmentPoint2Ellipse::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintInternalAlignmentPoint2Hyperbola @@ -1952,13 +1805,13 @@ ConstraintInternalAlignmentPoint2Hyperbola::ConstraintInternalAlignmentPoint2Hyp Hyperbola& e, Point& p1, InternalAlignmentType alignmentType) + : e(e) + , p(p1) + , AlignmentType(alignmentType) { - this->p = p1; pvec.push_back(p.x); pvec.push_back(p.y); - this->e = e; this->e.PushOwnParams(pvec); - this->AlignmentType = alignmentType; origpvec = pvec; rescale(); } @@ -1979,11 +1832,6 @@ ConstraintType ConstraintInternalAlignmentPoint2Hyperbola::getTypeId() return InternalAlignmentPoint2Hyperbola; } -void ConstraintInternalAlignmentPoint2Hyperbola::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintInternalAlignmentPoint2Hyperbola::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2052,35 +1900,15 @@ void ConstraintInternalAlignmentPoint2Hyperbola::errorgrad(double* err, double* } } -double ConstraintInternalAlignmentPoint2Hyperbola::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintInternalAlignmentPoint2Hyperbola::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintEqualMajorAxesEllipse ConstraintEqualMajorAxesConic::ConstraintEqualMajorAxesConic(MajorRadiusConic* a1, MajorRadiusConic* a2) + : e1(a1) + , e2(a2) { - this->e1 = a1; this->e1->PushOwnParams(pvec); - this->e2 = a2; this->e2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; @@ -2100,11 +1928,6 @@ ConstraintType ConstraintEqualMajorAxesConic::getTypeId() return EqualMajorAxesConic; } -void ConstraintEqualMajorAxesConic::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEqualMajorAxesConic::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2122,26 +1945,6 @@ void ConstraintEqualMajorAxesConic::errorgrad(double* err, double* grad, double* } } -double ConstraintEqualMajorAxesConic::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEqualMajorAxesConic::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // ConstraintEqualFocalDistance ConstraintEqualFocalDistance::ConstraintEqualFocalDistance(ArcOfParabola* a1, ArcOfParabola* a2) { @@ -2167,11 +1970,6 @@ ConstraintType ConstraintEqualFocalDistance::getTypeId() return EqualFocalDistance; } -void ConstraintEqualFocalDistance::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEqualFocalDistance::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2204,37 +2002,17 @@ void ConstraintEqualFocalDistance::errorgrad(double* err, double* grad, double* } } -double ConstraintEqualFocalDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEqualFocalDistance::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintCurveValue -ConstraintCurveValue::ConstraintCurveValue(Point& p, double* pcoord, Curve& crv, double* u) +ConstraintCurveValue::ConstraintCurveValue(Point& p, double* pcoord, Curve& c, double* u) + : crv(c.Copy()) { pvec.push_back(p.x); pvec.push_back(p.y); pvec.push_back(pcoord); pvec.push_back(u); - crv.PushOwnParams(pvec); - this->crv = crv.Copy(); + crv->PushOwnParams(pvec); pvecChangedFlag = true; origpvec = pvec; rescale(); @@ -2264,11 +2042,6 @@ ConstraintType ConstraintCurveValue::getTypeId() return CurveValue; } -void ConstraintCurveValue::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintCurveValue::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2307,26 +2080,6 @@ void ConstraintCurveValue::errorgrad(double* err, double* grad, double* param) } } -double ConstraintCurveValue::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintCurveValue::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - double ConstraintCurveValue::maxStep(MAP_pD_D& /*dir*/, double lim) { return lim; @@ -2366,11 +2119,6 @@ ConstraintType ConstraintPointOnHyperbola::getTypeId() return PointOnHyperbola; } -void ConstraintPointOnHyperbola::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintPointOnHyperbola::error() { double X_0 = *p1x(); @@ -2457,22 +2205,22 @@ double ConstraintPointOnHyperbola::grad(double* param) // -------------------------------------------------------- // ConstraintPointOnParabola ConstraintPointOnParabola::ConstraintPointOnParabola(Point& p, Parabola& e) + : parab(e.Copy()) { pvec.push_back(p.x); pvec.push_back(p.y); - e.PushOwnParams(pvec); - this->parab = e.Copy(); + parab->PushOwnParams(pvec); pvecChangedFlag = true; origpvec = pvec; rescale(); } ConstraintPointOnParabola::ConstraintPointOnParabola(Point& p, ArcOfParabola& e) + : parab(e.Copy()) { pvec.push_back(p.x); pvec.push_back(p.y); - e.PushOwnParams(pvec); - this->parab = e.Copy(); + parab->PushOwnParams(pvec); pvecChangedFlag = true; origpvec = pvec; rescale(); @@ -2500,11 +2248,6 @@ ConstraintType ConstraintPointOnParabola::getTypeId() return PointOnParabola; } -void ConstraintPointOnParabola::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintPointOnParabola::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2542,38 +2285,18 @@ void ConstraintPointOnParabola::errorgrad(double* err, double* grad, double* par } } -double ConstraintPointOnParabola::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintPointOnParabola::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintAngleViaPoint ConstraintAngleViaPoint::ConstraintAngleViaPoint(Curve& acrv1, Curve& acrv2, Point p, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p.x); pvec.push_back(p.y); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2605,11 +2328,6 @@ ConstraintType ConstraintAngleViaPoint::getTypeId() return AngleViaPoint; } -void ConstraintAngleViaPoint::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaPoint::error() { if (pvecChangedFlag) { @@ -2662,16 +2380,16 @@ ConstraintAngleViaTwoPoints::ConstraintAngleViaTwoPoints(Curve& acrv1, Point p1, Point p2, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p1.x); pvec.push_back(p1.y); pvec.push_back(p2.x); pvec.push_back(p2.y); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2707,11 +2425,6 @@ ConstraintType ConstraintAngleViaTwoPoints::getTypeId() return AngleViaTwoPoints; } -void ConstraintAngleViaTwoPoints::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaTwoPoints::error() { if (pvecChangedFlag) { @@ -2764,15 +2477,15 @@ ConstraintAngleViaPointAndParam::ConstraintAngleViaPointAndParam(Curve& acrv1, Point p, double* cparam, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p.x); pvec.push_back(p.y); pvec.push_back(cparam); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2805,11 +2518,6 @@ ConstraintType ConstraintAngleViaPointAndParam::getTypeId() return AngleViaPointAndParam; } -void ConstraintAngleViaPointAndParam::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaPointAndParam::error() { if (pvecChangedFlag) { @@ -2863,16 +2571,16 @@ ConstraintAngleViaPointAndTwoParams::ConstraintAngleViaPointAndTwoParams(Curve& double* cparam1, double* cparam2, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p.x); pvec.push_back(p.y); pvec.push_back(cparam1); pvec.push_back(cparam2); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2906,11 +2614,6 @@ ConstraintType ConstraintAngleViaPointAndTwoParams::getTypeId() return AngleViaPointAndTwoParams; } -void ConstraintAngleViaPointAndTwoParams::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaPointAndTwoParams::error() { if (pvecChangedFlag) { @@ -2959,31 +2662,30 @@ double ConstraintAngleViaPointAndTwoParams::grad(double* param) // -------------------------------------------------------- // ConstraintSnell -ConstraintSnell::ConstraintSnell(Curve& ray1, - Curve& ray2, - Curve& boundary, +ConstraintSnell::ConstraintSnell(Curve& r1, + Curve& r2, + Curve& b, Point p, double* n1, double* n2, bool flipn1, bool flipn2) + : ray1(r1.Copy()) + , ray2(r2.Copy()) + , boundary(b.Copy()) + , flipn1(flipn1) + , flipn2(flipn2) { pvec.push_back(n1); pvec.push_back(n2); pvec.push_back(p.x); pvec.push_back(p.y); - ray1.PushOwnParams(pvec); - ray2.PushOwnParams(pvec); - boundary.PushOwnParams(pvec); - this->ray1 = ray1.Copy(); - this->ray2 = ray2.Copy(); - this->boundary = boundary.Copy(); + ray1->PushOwnParams(pvec); + ray2->PushOwnParams(pvec); + boundary->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; - this->flipn1 = flipn1; - this->flipn2 = flipn2; - rescale(); } @@ -3017,11 +2719,6 @@ ConstraintType ConstraintSnell::getTypeId() return Snell; } -void ConstraintSnell::rescale(double coef) -{ - scale = coef * 1.; -} - // error and gradient combined. Values are returned through pointers. void ConstraintSnell::errorgrad(double* err, double* grad, double* param) { @@ -3053,35 +2750,14 @@ void ConstraintSnell::errorgrad(double* err, double* grad, double* param) } } -double ConstraintSnell::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintSnell::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return scale * deriv; -} - // -------------------------------------------------------- // ConstraintEqualLineLength ConstraintEqualLineLength::ConstraintEqualLineLength(Line& l1, Line& l2) + : l1(l1) + , l2(l2) { - this->l1 = l1; this->l1.PushOwnParams(pvec); - - this->l2 = l2; this->l2.PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; @@ -3101,11 +2777,6 @@ ConstraintType ConstraintEqualLineLength::getTypeId() return EqualLineLength; } -void ConstraintEqualLineLength::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEqualLineLength::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -3169,37 +2840,14 @@ void ConstraintEqualLineLength::errorgrad(double* err, double* grad, double* par } } -double ConstraintEqualLineLength::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEqualLineLength::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - - // -------------------------------------------------------- // ConstraintC2CDistance ConstraintC2CDistance::ConstraintC2CDistance(Circle& c1, Circle& c2, double* d) + : c1(c1) + , c2(c2) { - this->d = d; pvec.push_back(d); - - this->c1 = c1; this->c1.PushOwnParams(pvec); - - this->c2 = c2; this->c2.PushOwnParams(pvec); origpvec = pvec; @@ -3221,11 +2869,6 @@ ConstraintType ConstraintC2CDistance::getTypeId() return C2CDistance; } -void ConstraintC2CDistance::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintC2CDistance::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -3282,36 +2925,14 @@ void ConstraintC2CDistance::errorgrad(double* err, double* grad, double* param) } } -double ConstraintC2CDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintC2CDistance::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintC2LDistance ConstraintC2LDistance::ConstraintC2LDistance(Circle& c, Line& l, double* d) + : circle(c) + , line(l) { - this->d = d; pvec.push_back(d); - - this->circle = c; this->circle.PushOwnParams(pvec); - - this->line = l; this->line.PushOwnParams(pvec); origpvec = pvec; @@ -3324,11 +2945,6 @@ ConstraintType ConstraintC2LDistance::getTypeId() return C2LDistance; } -void ConstraintC2LDistance::rescale(double coef) -{ - scale = coef; -} - void ConstraintC2LDistance::ReconstructGeomPointers() { int i = 0; @@ -3374,11 +2990,21 @@ void ConstraintC2LDistance::errorgrad(double* err, double* grad, double* param) double dh = (darea - h * dlength) / length; if (err) { - *err = *distance() + *circle.rad - h; + if (h < *circle.rad) { + *err = *circle.rad - *distance() - h; + } + else { + *err = *circle.rad + *distance() - h; + } } else if (grad) { if (param == distance() || param == circle.rad) { - *grad = 1.0; + if (h < *circle.rad) { + *grad = -1.0; + } + else { + *grad = 1.0; + } } else { *grad = -dh; @@ -3386,36 +3012,14 @@ void ConstraintC2LDistance::errorgrad(double* err, double* grad, double* param) } } -double ConstraintC2LDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintC2LDistance::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintP2CDistance ConstraintP2CDistance::ConstraintP2CDistance(Point& p, Circle& c, double* d) + : circle(c) + , pt(p) { - this->d = d; pvec.push_back(d); - - this->circle = c; this->circle.PushOwnParams(pvec); - - this->pt = p; this->pt.PushOwnParams(pvec); origpvec = pvec; @@ -3428,11 +3032,6 @@ ConstraintType ConstraintP2CDistance::getTypeId() return P2CDistance; } -void ConstraintP2CDistance::rescale(double coef) -{ - scale = coef; -} - void ConstraintP2CDistance::ReconstructGeomPointers() { int i = 0; @@ -3477,33 +3076,12 @@ void ConstraintP2CDistance::errorgrad(double* err, double* grad, double* param) } } -double ConstraintP2CDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintP2CDistance::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintArcLength ConstraintArcLength::ConstraintArcLength(Arc& a, double* d) + : arc(a) { - this->d = d; pvec.push_back(d); - - this->arc = a; this->arc.PushOwnParams(pvec); origpvec = pvec; @@ -3524,11 +3102,6 @@ ConstraintType ConstraintArcLength::getTypeId() return ArcLength; } -void ConstraintArcLength::rescale(double coef) -{ - scale = coef; -} - void ConstraintArcLength::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -3562,24 +3135,4 @@ void ConstraintArcLength::errorgrad(double* err, double* grad, double* param) } } -double ConstraintArcLength::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintArcLength::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - - } // namespace GCS diff --git a/src/Mod/Sketcher/App/planegcs/Constraints.h b/src/Mod/Sketcher/App/planegcs/Constraints.h index 52d8d0ac36..e9512e51e9 100644 --- a/src/Mod/Sketcher/App/planegcs/Constraints.h +++ b/src/Mod/Sketcher/App/planegcs/Constraints.h @@ -133,7 +133,7 @@ public: virtual ~Constraint() {} - inline VEC_pD params() + VEC_pD params() { return pvec; } @@ -167,10 +167,38 @@ public: return internalAlignment; } + virtual ConstraintType getTypeId(); virtual void rescale(double coef = 1.); - virtual double error(); - virtual double grad(double*); + + // error and gradient combined. Values are returned through pointers. + virtual void errorgrad(double* err, double* grad, double* param) + { + (void)param; + if (err) { + *err = 0.; + } + if (grad) { + *grad = 0.; + } + }; + virtual double error() + { + double err; + errorgrad(&err, nullptr, nullptr); + return scale * err; + }; + virtual double grad(double* param) + { + if (findParamInPvec(param) == -1) { + return 0.0; + } + + double deriv; + errorgrad(nullptr, &deriv, param); + + return deriv * scale; + }; // virtual void grad(MAP_pD_D &deriv); --> TODO: vectorized grad version virtual double maxStep(MAP_pD_D& dir, double lim = 1.); // Finds first occurrence of param in pvec. This is useful to test if a constraint depends @@ -185,11 +213,11 @@ class ConstraintEqual: public Constraint { private: double ratio; - inline double* param1() + double* param1() { return pvec[0]; } - inline double* param2() + double* param2() { return pvec[1]; } @@ -197,7 +225,6 @@ private: public: ConstraintEqual(double* p1, double* p2, double p1p2ratio = 1.0); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -205,11 +232,11 @@ public: // Center of Gravity class ConstraintCenterOfGravity: public Constraint { - inline double* thecenter() + double* thecenter() { return pvec[0]; } - inline double* pointat(size_t i) + double* pointat(size_t i) { return pvec[1 + i]; } @@ -222,7 +249,6 @@ public: ConstraintCenterOfGravity(const std::vector& givenpvec, const std::vector& givenweights); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; @@ -234,15 +260,15 @@ private: // Weighted Linear Combination class ConstraintWeightedLinearCombination: public Constraint { - inline double* thepoint() + double* thepoint() { return pvec[0]; } - inline double* poleat(size_t i) + double* poleat(size_t i) { return pvec[1 + i]; } - inline double* weightat(size_t i) + double* weightat(size_t i) { return pvec[1 + numpoles + i]; } @@ -264,7 +290,6 @@ public: const std::vector& givenpvec, const std::vector& givenfactors); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; @@ -277,31 +302,31 @@ private: class ConstraintSlopeAtBSplineKnot: public Constraint { private: - inline double* polexat(size_t i) + double* polexat(size_t i) { return pvec[i]; } - inline double* poleyat(size_t i) + double* poleyat(size_t i) { return pvec[numpoles + i]; } - inline double* weightat(size_t i) + double* weightat(size_t i) { return pvec[2 * numpoles + i]; } - inline double* linep1x() + double* linep1x() { return pvec[3 * numpoles + 0]; } - inline double* linep1y() + double* linep1y() { return pvec[3 * numpoles + 1]; } - inline double* linep2x() + double* linep2x() { return pvec[3 * numpoles + 2]; } - inline double* linep2y() + double* linep2y() { return pvec[3 * numpoles + 3]; } @@ -325,20 +350,20 @@ private: class ConstraintPointOnBSpline: public Constraint { private: - inline double* thepoint() + double* thepoint() { return pvec[0]; } // TODO: better name because param has a different meaning here? - inline double* theparam() + double* theparam() { return pvec[1]; } - inline double* poleat(size_t i) + double* poleat(size_t i) { return pvec[2 + (startpole + i) % bsp.poles.size()]; } - inline double* weightat(size_t i) + double* weightat(size_t i) { return pvec[2 + bsp.poles.size() + (startpole + i) % bsp.weights.size()]; } @@ -349,7 +374,6 @@ public: /// coordidx = 0 if x, 1 if y ConstraintPointOnBSpline(double* point, double* initparam, int coordidx, BSpline& b); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; size_t numpoints; @@ -361,15 +385,15 @@ public: class ConstraintDifference: public Constraint { private: - inline double* param1() + double* param1() { return pvec[0]; } - inline double* param2() + double* param2() { return pvec[1]; } - inline double* difference() + double* difference() { return pvec[2]; } @@ -377,7 +401,6 @@ private: public: ConstraintDifference(double* p1, double* p2, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -386,23 +409,23 @@ public: class ConstraintP2PDistance: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* p2x() + double* p2x() { return pvec[2]; } - inline double* p2y() + double* p2y() { return pvec[3]; } - inline double* distance() + double* distance() { return pvec[4]; } @@ -410,11 +433,10 @@ private: public: ConstraintP2PDistance(Point& p1, Point& p2, double* d); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintP2PDistance() + ConstraintP2PDistance() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -424,23 +446,23 @@ public: class ConstraintP2PAngle: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* p2x() + double* p2x() { return pvec[2]; } - inline double* p2y() + double* p2y() { return pvec[3]; } - inline double* angle() + double* angle() { return pvec[4]; } @@ -449,11 +471,10 @@ private: public: ConstraintP2PAngle(Point& p1, Point& p2, double* a, double da_ = 0.); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintP2PAngle() + ConstraintP2PAngle() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -463,31 +484,31 @@ public: class ConstraintP2LDistance: public Constraint { private: - inline double* p0x() + double* p0x() { return pvec[0]; } - inline double* p0y() + double* p0y() { return pvec[1]; } - inline double* p1x() + double* p1x() { return pvec[2]; } - inline double* p1y() + double* p1y() { return pvec[3]; } - inline double* p2x() + double* p2x() { return pvec[4]; } - inline double* p2y() + double* p2y() { return pvec[5]; } - inline double* distance() + double* distance() { return pvec[6]; } @@ -495,11 +516,10 @@ private: public: ConstraintP2LDistance(Point& p, Line& l, double* d); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintP2LDistance() + ConstraintP2LDistance() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -510,27 +530,27 @@ public: class ConstraintPointOnLine: public Constraint { private: - inline double* p0x() + double* p0x() { return pvec[0]; } - inline double* p0y() + double* p0y() { return pvec[1]; } - inline double* p1x() + double* p1x() { return pvec[2]; } - inline double* p1y() + double* p1y() { return pvec[3]; } - inline double* p2x() + double* p2x() { return pvec[4]; } - inline double* p2y() + double* p2y() { return pvec[5]; } @@ -539,11 +559,10 @@ public: ConstraintPointOnLine(Point& p, Line& l); ConstraintPointOnLine(Point& p, Point& lp1, Point& lp2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnLine() + ConstraintPointOnLine() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -552,78 +571,74 @@ public: class ConstraintPointOnPerpBisector: public Constraint { private: - inline double* p0x() + double* p0x() { return pvec[0]; } - inline double* p0y() + double* p0y() { return pvec[1]; } - inline double* p1x() + double* p1x() { return pvec[2]; } - inline double* p1y() + double* p1y() { return pvec[3]; } - inline double* p2x() + double* p2x() { return pvec[4]; } - inline double* p2y() + double* p2y() { return pvec[5]; } - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintPointOnPerpBisector(Point& p, Line& l); ConstraintPointOnPerpBisector(Point& p, Point& lp1, Point& lp2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnPerpBisector() {}; + ConstraintPointOnPerpBisector() {}; #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - - double error() override; - double grad(double*) override; }; // Parallel class ConstraintParallel: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } @@ -631,7 +646,7 @@ private: public: ConstraintParallel(Line& l1, Line& l2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintParallel() + ConstraintParallel() {} #endif ConstraintType getTypeId() override; @@ -644,35 +659,35 @@ public: class ConstraintPerpendicular: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } @@ -681,7 +696,7 @@ public: ConstraintPerpendicular(Line& l1, Line& l2); ConstraintPerpendicular(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPerpendicular() + ConstraintPerpendicular() {} #endif ConstraintType getTypeId() override; @@ -694,39 +709,39 @@ public: class ConstraintL2LAngle: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } - inline double* angle() + double* angle() { return pvec[8]; } @@ -735,11 +750,10 @@ public: ConstraintL2LAngle(Line& l1, Line& l2, double* a); ConstraintL2LAngle(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2, double* a); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintL2LAngle() + ConstraintL2LAngle() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -749,35 +763,35 @@ public: class ConstraintMidpointOnLine: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } @@ -786,11 +800,10 @@ public: ConstraintMidpointOnLine(Line& l1, Line& l2); ConstraintMidpointOnLine(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintMidpointOnLine() + ConstraintMidpointOnLine() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -799,27 +812,27 @@ public: class ConstraintTangentCircumf: public Constraint { private: - inline double* c1x() + double* c1x() { return pvec[0]; } - inline double* c1y() + double* c1y() { return pvec[1]; } - inline double* c2x() + double* c2x() { return pvec[2]; } - inline double* c2y() + double* c2y() { return pvec[3]; } - inline double* r1() + double* r1() { return pvec[4]; } - inline double* r2() + double* r2() { return pvec[5]; } @@ -832,17 +845,16 @@ public: double* rd2, bool internal_ = false); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintTangentCircumf(bool internal_) + ConstraintTangentCircumf(bool internal_) { internal = internal_; } #endif - inline bool getInternal() + bool getInternal() { return internal; }; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -850,31 +862,31 @@ public: class ConstraintPointOnEllipse: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* cx() + double* cx() { return pvec[2]; } - inline double* cy() + double* cy() { return pvec[3]; } - inline double* f1x() + double* f1x() { return pvec[4]; } - inline double* f1y() + double* f1y() { return pvec[5]; } - inline double* rmin() + double* rmin() { return pvec[6]; } @@ -883,11 +895,10 @@ public: ConstraintPointOnEllipse(Point& p, Ellipse& e); ConstraintPointOnEllipse(Point& p, ArcOfEllipse& a); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnEllipse() + ConstraintPointOnEllipse() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -899,15 +910,11 @@ private: Ellipse e; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEllipseTangentLine(Line& l, Ellipse& e); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintInternalAlignmentPoint2Ellipse: public Constraint @@ -917,13 +924,9 @@ public: Point& p1, InternalAlignmentType alignmentType); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; private: - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Ellipse e; @@ -938,13 +941,9 @@ public: Point& p1, InternalAlignmentType alignmentType); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; private: - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Hyperbola e; @@ -959,15 +958,11 @@ private: MajorRadiusConic* e2; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEqualMajorAxesConic(MajorRadiusConic* a1, MajorRadiusConic* a2); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintEqualFocalDistance: public Constraint @@ -977,31 +972,26 @@ private: ArcOfParabola* e2; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEqualFocalDistance(ArcOfParabola* a1, ArcOfParabola* a2); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintCurveValue: public Constraint { private: // defines, which coordinate of point is being constrained by this constraint - inline double* pcoord() + double* pcoord() { return pvec[2]; } - inline double* u() + double* u() { return pvec[3]; } - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Curve* crv; @@ -1019,9 +1009,6 @@ public: ConstraintCurveValue(Point& p, double* pcoord, Curve& crv, double* u); ~ConstraintCurveValue() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; }; @@ -1029,31 +1016,31 @@ public: class ConstraintPointOnHyperbola: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* cx() + double* cx() { return pvec[2]; } - inline double* cy() + double* cy() { return pvec[3]; } - inline double* f1x() + double* f1x() { return pvec[4]; } - inline double* f1y() + double* f1y() { return pvec[5]; } - inline double* rmin() + double* rmin() { return pvec[6]; } @@ -1062,11 +1049,10 @@ public: ConstraintPointOnHyperbola(Point& p, Hyperbola& e); ConstraintPointOnHyperbola(Point& p, ArcOfHyperbola& a); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnHyperbola() + ConstraintPointOnHyperbola() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1075,8 +1061,7 @@ public: class ConstraintPointOnParabola: public Constraint { private: - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Parabola* parab; @@ -1087,19 +1072,16 @@ public: ConstraintPointOnParabola(Point& p, ArcOfParabola& a); ~ConstraintPointOnParabola() override; #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnParabola() + ConstraintPointOnParabola() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintAngleViaPoint: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; @@ -1123,7 +1105,6 @@ public: ConstraintAngleViaPoint(Curve& acrv1, Curve& acrv2, Point p, double* angle); ~ConstraintAngleViaPoint() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1131,7 +1112,7 @@ public: class ConstraintAngleViaTwoPoints: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; @@ -1158,7 +1139,6 @@ public: ConstraintAngleViaTwoPoints(Curve& acrv1, Curve& acrv2, Point p1, Point p2, double* angle); ~ConstraintAngleViaTwoPoints() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1167,11 +1147,11 @@ public: class ConstraintSnell: public Constraint { private: - inline double* n1() + double* n1() { return pvec[0]; }; - inline double* n2() + double* n2() { return pvec[1]; }; @@ -1192,8 +1172,7 @@ private: bool flipn1, flipn2; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: // n1dn2 = n1 divided by n2. from n1 to n2. flipn1 = true instructs to flip ray1's tangent @@ -1207,19 +1186,16 @@ public: bool flipn2); ~ConstraintSnell() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintAngleViaPointAndParam: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; - inline double* cparam() + double* cparam() { return pvec[3]; }; @@ -1247,7 +1223,6 @@ public: double* angle); ~ConstraintAngleViaPointAndParam() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1256,15 +1231,15 @@ public: class ConstraintAngleViaPointAndTwoParams: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; - inline double* cparam1() + double* cparam1() { return pvec[3]; }; - inline double* cparam2() + double* cparam2() { return pvec[4]; }; @@ -1292,7 +1267,6 @@ public: double* angle); ~ConstraintAngleViaPointAndTwoParams() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1304,15 +1278,11 @@ private: Line l2; // writes pointers in pvec to the parameters of line1, line2 void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEqualLineLength(Line& l1, Line& l2); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintC2CDistance: public Constraint @@ -1320,22 +1290,17 @@ class ConstraintC2CDistance: public Constraint private: Circle c1; Circle c2; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } // writes pointers in pvec to the parameters of c1, c2 void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintC2CDistance(Circle& c1, Circle& c2, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; // C2LDistance @@ -1344,22 +1309,17 @@ class ConstraintC2LDistance: public Constraint private: Circle circle; Line line; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } // writes pointers in pvec to the parameters of c, l void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintC2LDistance(Circle& c, Line& l, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; // P2CDistance @@ -1368,22 +1328,16 @@ class ConstraintP2CDistance: public Constraint private: Circle circle; Point pt; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } void ReconstructGeomPointers(); // writes pointers in pvec to the parameters of c - void - errorgrad(double* err, - double* grad, - double* param); // error and gradient combined. Values are returned through pointers. + void errorgrad(double* err, double* grad, double* param) override; + public: ConstraintP2CDistance(Point& p, Circle& c, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; // ArcLength @@ -1391,22 +1345,16 @@ class ConstraintArcLength: public Constraint { private: Arc arc; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } void ReconstructGeomPointers(); // writes pointers in pvec to the parameters of a - void - errorgrad(double* err, - double* grad, - double* param); // error and gradient combined. Values are returned through pointers. + void errorgrad(double* err, double* grad, double* param) override; + public: ConstraintArcLength(Arc& a, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; } // namespace GCS diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.cpp b/src/Mod/Sketcher/Gui/CommandConstraints.cpp index 664cf85191..0d43068a92 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.cpp +++ b/src/Mod/Sketcher/Gui/CommandConstraints.cpp @@ -892,12 +892,12 @@ enum SelType { SelUnknown = 0, SelVertex = 1, - SelVertexOrRoot = 64, SelRoot = 2, + SelVertexOrRoot = SelVertex | SelRoot, SelEdge = 4, - SelEdgeOrAxis = 128, SelHAxis = 8, SelVAxis = 16, + SelEdgeOrAxis = SelEdge | SelHAxis | SelVAxis, SelExternalEdge = 32 }; @@ -928,11 +928,11 @@ public: return false; } std::string element(sSubName); - if ((allowedSelTypes & (SelRoot | SelVertexOrRoot) && element.substr(0, 9) == "RootPoint") - || (allowedSelTypes & (SelVertex | SelVertexOrRoot) && element.substr(0, 6) == "Vertex") - || (allowedSelTypes & (SelEdge | SelEdgeOrAxis) && element.substr(0, 4) == "Edge") - || (allowedSelTypes & (SelHAxis | SelEdgeOrAxis) && element.substr(0, 6) == "H_Axis") - || (allowedSelTypes & (SelVAxis | SelEdgeOrAxis) && element.substr(0, 6) == "V_Axis") + if ((allowedSelTypes & SelRoot && element.substr(0, 9) == "RootPoint") + || (allowedSelTypes & SelVertex && element.substr(0, 6) == "Vertex") + || (allowedSelTypes & SelEdge && element.substr(0, 4) == "Edge") + || (allowedSelTypes & SelHAxis && element.substr(0, 6) == "H_Axis") + || (allowedSelTypes & SelVAxis && element.substr(0, 6) == "V_Axis") || (allowedSelTypes & SelExternalEdge && element.substr(0, 12) == "ExternalEdge")) { return true; } @@ -982,10 +982,6 @@ protected: * generate sequences to be passed to applyConstraint(). * Whenever any sequence is completed, applyConstraint() is called, so it's * best to keep them prefix-free. - * Be mindful that when SelVertex and SelRoot are given preference over - * SelVertexOrRoot, and similar for edges/axes. Thus if a vertex is selected - * when SelVertex and SelVertexOrRoot are both applicable, only sequences with - * SelVertex will be continue. * * TODO: Introduce structs to allow keeping first selection */ @@ -1032,30 +1028,30 @@ public: int VtId = getPreselectPoint(); int CrvId = getPreselectCurve(); int CrsId = getPreselectCross(); - if (allowedSelTypes & (SelRoot | SelVertexOrRoot) && CrsId == 0) { + if (allowedSelTypes & SelRoot && CrsId == 0) { selIdPair.GeoId = Sketcher::GeoEnum::RtPnt; selIdPair.PosId = Sketcher::PointPos::start; - newSelType = (allowedSelTypes & SelRoot) ? SelRoot : SelVertexOrRoot; + newSelType = SelRoot; ss << "RootPoint"; } - else if (allowedSelTypes & (SelVertex | SelVertexOrRoot) && VtId >= 0) { + else if (allowedSelTypes & SelVertex && VtId >= 0) { sketchgui->getSketchObject()->getGeoVertexIndex(VtId, selIdPair.GeoId, selIdPair.PosId); - newSelType = (allowedSelTypes & SelVertex) ? SelVertex : SelVertexOrRoot; + newSelType = SelVertex; ss << "Vertex" << VtId + 1; } - else if (allowedSelTypes & (SelEdge | SelEdgeOrAxis) && CrvId >= 0) { + else if (allowedSelTypes & SelEdge && CrvId >= 0) { selIdPair.GeoId = CrvId; - newSelType = (allowedSelTypes & SelEdge) ? SelEdge : SelEdgeOrAxis; + newSelType = SelEdge; ss << "Edge" << CrvId + 1; } - else if (allowedSelTypes & (SelHAxis | SelEdgeOrAxis) && CrsId == 1) { + else if (allowedSelTypes & SelHAxis && CrsId == 1) { selIdPair.GeoId = Sketcher::GeoEnum::HAxis; - newSelType = (allowedSelTypes & SelHAxis) ? SelHAxis : SelEdgeOrAxis; + newSelType = SelHAxis; ss << "H_Axis"; } - else if (allowedSelTypes & (SelVAxis | SelEdgeOrAxis) && CrsId == 2) { + else if (allowedSelTypes & SelVAxis && CrsId == 2) { selIdPair.GeoId = Sketcher::GeoEnum::VAxis; - newSelType = (allowedSelTypes & SelVAxis) ? SelVAxis : SelEdgeOrAxis; + newSelType = SelVAxis; ss << "V_Axis"; } else if (allowedSelTypes & SelExternalEdge && CrvId <= Sketcher::GeoEnum::RefExt) { @@ -1085,7 +1081,7 @@ public: for (std::set::iterator token = ongoingSequences.begin(); token != ongoingSequences.end(); ++token) { - if ((cmd->allowedSelSequences).at(*token).at(seqIndex) == newSelType) { + if ((cmd->allowedSelSequences).at(*token).at(seqIndex) & newSelType) { if (seqIndex == (cmd->allowedSelSequences).at(*token).size() - 1) { // One of the sequences is completed. Pass to cmd->applyConstraint cmd->applyConstraint(selSeq, *token);// replace arg 2 by ongoingToken @@ -1093,6 +1089,9 @@ public: selSeq.clear(); resetOngoingSequences(); + // Re-arm hint for next operation + updateHint(); + return true; } _tempOnSequences.insert(*token); @@ -1106,11 +1105,211 @@ public: seqIndex++; selFilterGate->setAllowedSelTypes(allowedSelTypes); } + updateHint(); return true; } + std::list getToolHints() const override { + const std::string commandName = cmd->getName(); + const int selectionStep = seqIndex; + + // Special case for Sketcher_ConstrainPointOnObject to generate dynamic step hint + if (commandName == "Sketcher_ConstrainPointOnObject") { + if (selectionStep == 0) { + return {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else if (selectionStep == 1 && !selSeq.empty()) { + if (isVertex(selSeq[0].GeoId, selSeq[0].PosId)) { + return {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else { + return {{QObject::tr("%1 pick point"), {Gui::InputHint::UserInput::MouseLeft}}}; + } + } + } + + // For everything else, use the static table + return lookupConstraintHints(commandName, selectionStep); +} + private: + struct ConstraintHintEntry { + std::string commandName; // FreeCAD command name (e.g., "Sketcher_ConstrainSymmetric") + int selectionStep; // 0-indexed step in the selection sequence + std::list hints; // Hint text and input types for this step + }; + + using ConstraintHintTable = std::vector; + + // Constraint hint lookup table + // Format: {command_name, selection_step, {hint_text, input_types}} + // Steps are 0-indexed and correspond to DrawSketchHandlerGenConstraint::seqIndex + // Each step provides contextual guidance for what the user should select next + static ConstraintHintTable getConstraintHintTable() { + return { + // Coincident + {.commandName = "Sketcher_ConstrainCoincidentUnified", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainCoincidentUnified", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Distance X/Y + {.commandName = "Sketcher_ConstrainDistanceX", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistanceX", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistanceY", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistanceY", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Horizontal/Vertical + {.commandName = "Sketcher_ConstrainHorizontal", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge or first point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainHorizontal", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainVertical", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge or first point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainVertical", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainHorVer", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge or first point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainHorVer", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Block/Lock + {.commandName = "Sketcher_ConstrainBlock", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge to block"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainLock", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point to lock"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Coincident (individual) + {.commandName = "Sketcher_ConstrainCoincident", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or curve"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainCoincident", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or curve"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainEqual", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainEqual", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Radius/Diameter + {.commandName = "Sketcher_ConstrainRadius", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick circle or arc"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDiameter", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick circle or arc"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Angle + {.commandName = "Sketcher_ConstrainAngle", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainAngle", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Symmetry + {.commandName = "Sketcher_ConstrainSymmetric", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainSymmetric", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainSymmetric", + .selectionStep = 2, + .hints = {{QObject::tr("%1 pick symmetry line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Tangent + {.commandName = "Sketcher_ConstrainTangent", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainTangent", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainTangent", + .selectionStep = 2, + .hints = {{QObject::tr("%1 pick optional tangent point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Perpendicular + {.commandName = "Sketcher_ConstrainPerpendicular", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainPerpendicular", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainPerpendicular", + .selectionStep = 2, + .hints = {{QObject::tr("%1 pick optional perpendicular point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Parallel + {.commandName = "Sketcher_ConstrainParallel", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainParallel", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Distance + {.commandName = "Sketcher_ConstrainDistance", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistance", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + }; + } + + static std::list lookupConstraintHints(const std::string& commandName, int selectionStep) { + const auto constraintHintTable = getConstraintHintTable(); + auto it = std::ranges::find_if(constraintHintTable, + [&commandName, selectionStep](const ConstraintHintEntry& entry) { + return entry.commandName == commandName && entry.selectionStep == selectionStep; + }); + + return (it != constraintHintTable.end()) ? it->hints : std::list{}; + } + void activated() override { selFilterGate = new GenericConstraintSelection(sketchgui->getObject()); @@ -1606,6 +1805,8 @@ public: ss.str().c_str()); sketchgui->draw(false, false); // Redraw } + + updateHint(); return true; } @@ -1620,6 +1821,17 @@ public: DrawSketchHandler::quit(); } } + +std::list getToolHints() const override { + if (selectionEmpty()) { + return {{QObject::tr("%1 pick geometry"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else if (selPoints.size() == 1 && selLine.empty() && selCircleArc.empty()) { + return {{QObject::tr("%1 pick second point or geometry"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else { + return {{QObject::tr("%1 place dimension"), {Gui::InputHint::UserInput::MouseLeft}}}; + } +} + protected: SpecialConstraint specialConstraint; AvailableConstraint availableConstraint; @@ -1763,7 +1975,7 @@ protected: && !contains(selEllipseAndCo, elem); } - bool selectionEmpty() + bool selectionEmpty() const { return selPoints.empty() && selLine.empty() && selCircleArc.empty() && selEllipseAndCo.empty(); } @@ -2263,10 +2475,10 @@ protected: Base::Vector3d pnt1 = lineSeg->getStartPoint(); Base::Vector3d pnt2 = lineSeg->getEndPoint(); Base::Vector3d d = pnt2 - pnt1; - double ActDist = - std::abs(-center1.x * d.y + center1.y * d.x + pnt1.x * pnt2.y - pnt2.x * pnt1.y) - / d.Length() - - radius1; + double ActDist = std::abs( + std::abs(-center1.x * d.y + center1.y * d.x + pnt1.x * pnt2.y - pnt2.x * pnt1.y) + / d.Length() + - radius1); Gui::cmdAppObjectArgs(Obj, "addConstraint(Sketcher.Constraint('Distance',%d,%d,%f))", @@ -3154,7 +3366,7 @@ CmdSketcherConstrainHorVer::CmdSketcherConstrainHorVer() sAccel = "A"; eType = ForEdit; - allowedSelSequences = { {SelEdge}, {SelVertex, SelVertexOrRoot}, {SelRoot, SelVertex} }; + allowedSelSequences = { {SelEdge}, {SelVertexOrRoot, SelVertexOrRoot} }; } void CmdSketcherConstrainHorVer::activated(int iMsg) @@ -3200,7 +3412,7 @@ CmdSketcherConstrainHorizontal::CmdSketcherConstrainHorizontal() sAccel = "H"; eType = ForEdit; - allowedSelSequences = {{SelEdge}, {SelVertex, SelVertexOrRoot}, {SelRoot, SelVertex}}; + allowedSelSequences = {{SelEdge}, {SelVertexOrRoot, SelVertexOrRoot}}; } void CmdSketcherConstrainHorizontal::activated(int iMsg) @@ -3245,7 +3457,7 @@ CmdSketcherConstrainVertical::CmdSketcherConstrainVertical() sAccel = "V"; eType = ForEdit; - allowedSelSequences = {{SelEdge}, {SelVertex, SelVertexOrRoot}, {SelRoot, SelVertex}}; + allowedSelSequences = {{SelEdge}, {SelVertexOrRoot, SelVertexOrRoot}}; } void CmdSketcherConstrainVertical::activated(int iMsg) @@ -3783,15 +3995,14 @@ CmdSketcherConstrainCoincidentUnified::CmdSketcherConstrainCoincidentUnified(con eType = ForEdit; - allowedSelSequences = { {SelVertex, SelEdgeOrAxis}, + allowedSelSequences = {{SelVertex, SelEdgeOrAxis}, {SelRoot, SelEdge}, {SelVertex, SelExternalEdge}, {SelEdge, SelVertexOrRoot}, {SelEdgeOrAxis, SelVertex}, {SelExternalEdge, SelVertex}, - {SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + {SelVertexOrRoot, SelVertexOrRoot}, {SelEdge, SelEdge}, {SelEdge, SelExternalEdge}, {SelExternalEdge, SelEdge} }; @@ -4112,11 +4323,10 @@ void CmdSketcherConstrainCoincidentUnified::applyConstraint(std::vectorgetGeometry(GeoId1)) || !isGeoConcentricCompatible(Obj->getGeometry(GeoId2))) { @@ -4297,8 +4506,7 @@ CmdSketcherConstrainCoincident::CmdSketcherConstrainCoincident() sAccel = hGrp->GetBool("UnifiedCoincident", true) ? "C,C" : "C"; eType = ForEdit; - allowedSelSequences = {{SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge, SelEdge}, {SelEdge, SelExternalEdge}, {SelExternalEdge, SelEdge}}; @@ -4399,15 +4607,13 @@ CmdSketcherConstrainDistance::CmdSketcherConstrainDistance() sAccel = "K, D"; eType = ForEdit; - allowedSelSequences = { {SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge}, {SelExternalEdge}, {SelVertex, SelEdgeOrAxis}, {SelRoot, SelEdge}, - {SelVertex, SelExternalEdge}, - {SelRoot, SelExternalEdge}, - {SelEdge, SelEdge} }; + {SelVertexOrRoot, SelExternalEdge}, + {SelEdge, SelEdge}}; } void CmdSketcherConstrainDistance::activated(int iMsg) @@ -4783,8 +4989,7 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe bool arebothpointsorsegmentsfixed = areBothPointsOrSegmentsFixed(Obj, GeoId1, GeoId2); switch (seqIndex) { - case 0:// {SelVertex, SelVertexOrRoot} - case 1:// {SelRoot, SelVertex} + case 0:// {SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -4848,8 +5053,8 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe return; } - case 2:// {SelEdge} - case 3:// {SelExternalEdge} + case 1:// {SelEdge} + case 2:// {SelExternalEdge} { GeoId1 = selSeq.at(0).GeoId; @@ -4891,10 +5096,9 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe return; } - case 4:// {SelVertex, SelEdgeOrAxis} - case 5:// {SelRoot, SelEdge} - case 6:// {SelVertex, SelExternalEdge} - case 7:// {SelRoot, SelExternalEdge} + case 3:// {SelVertex, SelEdgeOrAxis} + case 4:// {SelRoot, SelEdge} + case 5:// {SelVertexOrRoot, SelExternalEdge} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -4934,7 +5138,7 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe return; } - case 8:// {SelEdge, SelEdge} + case 6:// {SelEdge, SelEdge} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -5052,8 +5256,7 @@ CmdSketcherConstrainDistanceX::CmdSketcherConstrainDistanceX() eType = ForEdit; // Can't do single vertex because its a prefix for 2 vertices - allowedSelSequences = {{SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge}, {SelExternalEdge}}; } @@ -5237,8 +5440,7 @@ void CmdSketcherConstrainDistanceX::applyConstraint(std::vector& selS Sketcher::PointPos PosId1 = Sketcher::PointPos::none, PosId2 = Sketcher::PointPos::none; switch (seqIndex) { - case 0:// {SelVertex, SelVertexOrRoot} - case 1:// {SelRoot, SelVertex} + case 0:// {SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -5246,8 +5448,8 @@ void CmdSketcherConstrainDistanceX::applyConstraint(std::vector& selS PosId2 = selSeq.at(1).PosId; break; } - case 2:// {SelEdge} - case 3:// {SelExternalEdge} + case 1:// {SelEdge} + case 2:// {SelExternalEdge} { GeoId1 = GeoId2 = selSeq.at(0).GeoId; PosId1 = Sketcher::PointPos::start; @@ -5355,8 +5557,7 @@ CmdSketcherConstrainDistanceY::CmdSketcherConstrainDistanceY() eType = ForEdit; // Can't do single vertex because its a prefix for 2 vertices - allowedSelSequences = {{SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge}, {SelExternalEdge}}; } @@ -5536,8 +5737,7 @@ void CmdSketcherConstrainDistanceY::applyConstraint(std::vector& selS Sketcher::PointPos PosId1 = Sketcher::PointPos::none, PosId2 = Sketcher::PointPos::none; switch (seqIndex) { - case 0:// {SelVertex, SelVertexOrRoot} - case 1:// {SelRoot, SelVertex} + case 0:// {SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -5545,8 +5745,8 @@ void CmdSketcherConstrainDistanceY::applyConstraint(std::vector& selS PosId2 = selSeq.at(1).PosId; break; } - case 2:// {SelEdge} - case 3:// {SelExternalEdge} + case 1:// {SelEdge} + case 2:// {SelExternalEdge} { GeoId1 = GeoId2 = selSeq.at(0).GeoId; PosId1 = Sketcher::PointPos::start; @@ -9364,19 +9564,13 @@ CmdSketcherConstrainSymmetric::CmdSketcherConstrainSymmetric() allowedSelSequences = {{SelEdge, SelVertexOrRoot}, {SelExternalEdge, SelVertex}, - {SelVertex, SelEdge, SelVertexOrRoot}, - {SelRoot, SelEdge, SelVertex}, - {SelVertex, SelExternalEdge, SelVertexOrRoot}, - {SelRoot, SelExternalEdge, SelVertex}, + {SelVertexOrRoot, SelEdge, SelVertexOrRoot}, + {SelVertexOrRoot, SelExternalEdge, SelVertexOrRoot}, {SelVertex, SelEdgeOrAxis, SelVertex}, - {SelVertex, SelVertexOrRoot, SelEdge}, - {SelRoot, SelVertex, SelEdge}, - {SelVertex, SelVertexOrRoot, SelExternalEdge}, - {SelRoot, SelVertex, SelExternalEdge}, + {SelVertexOrRoot, SelVertexOrRoot, SelEdge}, + {SelVertexOrRoot, SelVertexOrRoot, SelExternalEdge}, {SelVertex, SelVertex, SelEdgeOrAxis}, - {SelVertex, SelVertexOrRoot, SelVertex}, - {SelVertex, SelVertex, SelVertexOrRoot}, - {SelVertexOrRoot, SelVertex, SelVertex}}; + {SelVertexOrRoot, SelVertexOrRoot, SelVertexOrRoot}}; } void CmdSketcherConstrainSymmetric::activated(int iMsg) @@ -9590,16 +9784,12 @@ void CmdSketcherConstrainSymmetric::applyConstraint(std::vector& selS } break; } - case 2: // {SelVertex, SelEdge, SelVertexOrRoot} - case 3: // {SelRoot, SelEdge, SelVertex} - case 4: // {SelVertex, SelExternalEdge, SelVertexOrRoot} - case 5: // {SelRoot, SelExternalEdge, SelVertex} - case 6: // {SelVertex, SelEdgeOrAxis, SelVertex} - case 7: // {SelVertex, SelVertexOrRoot,SelEdge} - case 8: // {SelRoot, SelVertex, SelEdge} - case 9: // {SelVertex, SelVertexOrRoot, SelExternalEdge} - case 10:// {SelRoot, SelVertex, SelExternalEdge} - case 11:// {SelVertex, SelVertex, SelEdgeOrAxis} + case 2:// {SelVertexOrRoot, SelEdge, SelVertexOrRoot} + case 3:// {SelVertexOrRoot, SelExternalEdge, SelVertexOrRoot} + case 4:// {SelVertex, SelEdgeOrAxis, SelVertex} + case 5:// {SelVertexOrRoot, SelVertexOrRoot, SelEdge} + case 6:// {SelVertexOrRoot, SelVertexOrRoot, SelExternalEdge} + case 7:// {SelVertex, SelVertex, SelEdgeOrAxis} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(2).GeoId; @@ -9658,9 +9848,7 @@ void CmdSketcherConstrainSymmetric::applyConstraint(std::vector& selS } return; } - case 12:// {SelVertex, SelVertexOrRoot, SelVertex} - case 13:// {SelVertex, SelVertex, SelVertexOrRoot} - case 14:// {SelVertexOrRoot, SelVertex, SelVertex} + case 8:// {SelVertexOrRoot, SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; diff --git a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp index 6c007b8761..13ccbea6cd 100644 --- a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp +++ b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp @@ -375,7 +375,7 @@ void CmdSketcherSelectConstraints::activated(int iMsg) // get the needed lists and objects const std::vector& SubNames = selection[0].getSubNames(); Sketcher::SketchObject* Obj = static_cast(selection[0].getObject()); - const std::vector& vals = Obj->Constraints.getValues(); + const std::vector& constraints = Obj->Constraints.getValues(); std::string doc_name = Obj->getDocument()->getName(); std::string obj_name = Obj->getNameInDocument(); @@ -384,23 +384,26 @@ void CmdSketcherSelectConstraints::activated(int iMsg) std::vector constraintSubNames; // go through the selected subelements - for (std::vector::const_iterator it = SubNames.begin(); it != SubNames.end(); - ++it) { - // only handle edges - if (it->size() > 4 && it->substr(0, 4) == "Edge") { - int GeoId = std::atoi(it->substr(4, 4000).c_str()) - 1; - - // push all the constraints - int i = 0; - for (std::vector::const_iterator it = vals.begin(); - it != vals.end(); - ++it, ++i) { - if ((*it)->First == GeoId || (*it)->Second == GeoId || (*it)->Third == GeoId) { - constraintSubNames.push_back( - Sketcher::PropertyConstraintList::getConstraintName(i)); - } + int i = 0; + for (auto const& constraint : constraints) { + auto isRelated = [&] (const std::string& subName){ + int geoId; + PointPos pointPos; + Data::IndexedName name = Obj->checkSubName(subName.c_str()); + if (!Obj->geoIdFromShapeType(name, geoId, pointPos)) { + return false; } + if (pointPos != PointPos::none) { + return constraint->involvesGeoIdAndPosId(geoId, pointPos); + } else { + return constraint->involvesGeoId(geoId); + } + }; + + if (std::ranges::any_of(SubNames, isRelated)) { + constraintSubNames.push_back(PropertyConstraintList::getConstraintName(i)); } + ++i; } if (!constraintSubNames.empty()) diff --git a/src/Mod/Sketcher/Gui/DrawSketchController.h b/src/Mod/Sketcher/Gui/DrawSketchController.h index cf59ba0efb..3feed1dd3a 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchController.h +++ b/src/Mod/Sketcher/Gui/DrawSketchController.h @@ -365,8 +365,56 @@ public: } } + void finishEditingOnAllOVPs() + { + // we call this on a current OnViewParameter when pressed CTRL+ENTER to accept + // input on all visible ovps of current mode + + // we check for initial state, since `onViewValueChanged` can process to next mode + // if we set hasFinishedEditing on current mode + auto initialState = handler->state(); + for (size_t i = 0; i < onViewParameters.size(); i++) { + if (isOnViewParameterOfCurrentMode(i) && isOnViewParameterVisible(i) + && initialState == getState(static_cast(i))) { + onViewParameters[i]->isSet = true; + onViewParameters[i]->hasFinishedEditing = true; + + double currentValue = onViewParameters[i]->getValue(); + onViewValueChanged(static_cast(i), currentValue); + } + } + } + void tryViewValueChanged(int onviewparameterindex, double value) { + // go to next label in circular manner if user has currently pressed enter on current one + if (onViewParameters[onviewparameterindex]->hasFinishedEditing) { + // find the first parameter of the current mode that is not locked to start the cycle + auto findNextUnlockedParameter = [this](size_t startIndex) -> int { + for (size_t i = startIndex; i < onViewParameters.size(); i++) { + if (isOnViewParameterOfCurrentMode(i) + && !onViewParameters[i]->hasFinishedEditing) { + return static_cast(i); + } + } + return -1; + }; + + // find first unlocked parameter (for cycling back) + int firstOfCurrentMode = findNextUnlockedParameter(0); + + // find next unlocked parameter after current one + int nextUnlockedIndex = findNextUnlockedParameter(onviewparameterindex + 1); + + // if no next parameter found, cycle back to first of current mode + if (nextUnlockedIndex != -1) { + setFocusToOnViewParameter(nextUnlockedIndex); + } + else if (firstOfCurrentMode != -1) { + setFocusToOnViewParameter(firstOfCurrentMode); + } + } + /* That is not supported with on-view parameters. // -> A machine does not forward to a next state when adapting the parameter (though it // may forward to @@ -628,6 +676,11 @@ protected: unsetOnViewParameter(parameter); finishControlsChanged(); }); + + // Connect Ctrl+Enter signal to apply values to all visible OVPs in current stage + QObject::connect(parameter, &Gui::EditableDatumLabel::finishEditingOnAllOVPs, [this]() { + finishEditingOnAllOVPs(); + }); } } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandler.h b/src/Mod/Sketcher/Gui/DrawSketchHandler.h index 607f4ee50a..ca7ad4e700 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandler.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandler.h @@ -32,6 +32,8 @@ #include #include #include +#include + #include #include @@ -160,6 +162,10 @@ public: return false; } + virtual std::list getToolHints() const + { + return {}; + } void quit() override; friend class ViewProviderSketch; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h index f33f3106e1..81c7faa2ea 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h @@ -721,7 +721,7 @@ void DSHArcController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -729,7 +729,7 @@ void DSHArcController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; @@ -743,7 +743,7 @@ void DSHArcController::doChangeDrawSketchHandlerMode() else { auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h index b5a1e74391..382cf17c04 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h @@ -784,7 +784,7 @@ void DSHArcSlotController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -792,7 +792,7 @@ void DSHArcSlotController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h index 46903efbe6..0d12a1f76e 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h @@ -1096,7 +1096,7 @@ void DSHBSplineController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->canGoToNextMode(); // its not going to next mode unsetOnViewParameter(thirdParam.get()); diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h index 5e93a86cf7..7ebb975b04 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h @@ -603,7 +603,7 @@ void DSHCircleController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -630,7 +630,7 @@ void DSHCircleController::doChangeDrawSketchHandlerMode() auto& fifthParam = onViewParameters[OnViewParameter::Fifth]; auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h index 033d2597c2..90413ffd07 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h @@ -697,7 +697,7 @@ void DSHEllipseController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -705,7 +705,7 @@ void DSHEllipseController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; @@ -720,7 +720,7 @@ void DSHEllipseController::doChangeDrawSketchHandlerMode() } else { auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h index 96e61a22b9..a31a3f7e65 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h @@ -563,7 +563,7 @@ void DSHLineController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -571,7 +571,7 @@ void DSHLineController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h index c143c0223e..30ea54eb0e 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h @@ -191,6 +191,15 @@ private: generateSourceWires(); } +public: + std::list getToolHints() const override + { + using enum Gui::InputHint::UserInput; + + return {{QObject::tr("%1 set offset direction and distance", "Sketcher Offset: hint"), + {MouseLeft}}}; + } + private: class CoincidencePointPos { diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h index cef45cac34..e28abfb2f8 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h @@ -234,7 +234,7 @@ void DSHPointController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::End); // handler->finish(); // Called by the change of mode } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h index c20651acd6..b58a1fb7b6 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h @@ -455,7 +455,7 @@ void DSHPolygonController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -463,7 +463,7 @@ void DSHPolygonController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h index df16b3d043..a9002a30f0 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h @@ -2346,14 +2346,14 @@ void DSHRectangleController::doChangeDrawSketchHandlerMode() switch (handler->state()) { case SelectMode::SeekFirst: { if (onViewParameters[OnViewParameter::First]->hasFinishedEditing - || onViewParameters[OnViewParameter::Second]->hasFinishedEditing) { + && onViewParameters[OnViewParameter::Second]->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; case SelectMode::SeekSecond: { if (onViewParameters[OnViewParameter::Third]->hasFinishedEditing - || onViewParameters[OnViewParameter::Fourth]->hasFinishedEditing) { + && onViewParameters[OnViewParameter::Fourth]->hasFinishedEditing) { if (handler->roundCorners || handler->makeFrame || handler->constructionMethod() == ConstructionMethod::ThreePoints @@ -2387,7 +2387,7 @@ void DSHRectangleController::doChangeDrawSketchHandlerMode() } else { if (onViewParameters[OnViewParameter::Fifth]->hasFinishedEditing - || onViewParameters[OnViewParameter::Sixth]->hasFinishedEditing) { + && onViewParameters[OnViewParameter::Sixth]->hasFinishedEditing) { if (handler->roundCorners || handler->makeFrame) { handler->setState(SelectMode::SeekFourth); } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h index 4baf4773b1..e369e22c63 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h @@ -455,8 +455,49 @@ private: return pointToRotate; } + + struct HintEntry + { + SelectMode state; + std::list hints; + }; + + using HintTable = std::vector; + + static HintTable getRotateHintTable(); + static std::list lookupRotateHints(SelectMode state); + +public: + std::list getToolHints() const override + { + return lookupRotateHints(state()); + } }; +DrawSketchHandlerRotate::HintTable DrawSketchHandlerRotate::getRotateHintTable() +{ + using enum Gui::InputHint::UserInput; + + return { + {.state = SelectMode::SeekFirst, + .hints = {{QObject::tr("%1 pick center point", "Sketcher Rotate: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekSecond, + .hints = {{QObject::tr("%1 set start angle", "Sketcher Rotate: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekThird, + .hints = {{QObject::tr("%1 set rotation angle", "Sketcher Rotate: hint"), {MouseLeft}}}}}; +} + +std::list DrawSketchHandlerRotate::lookupRotateHints(SelectMode state) +{ + const auto rotateHintTable = getRotateHintTable(); + + auto it = std::ranges::find_if(rotateHintTable, [state](const HintEntry& entry) { + return entry.state == state; + }); + + return (it != rotateHintTable.end()) ? it->hints : std::list {}; +} + template<> auto DSHRotateControllerBase::getState(int labelindex) const { @@ -661,7 +702,7 @@ void DSHRotateController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h index 30292f6e78..28e155ea88 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h @@ -132,6 +132,11 @@ public: } + std::list getToolHints() const override + { + return lookupScaleHints(state()); + } + private: void updateDataAndDrawToPosition(Base::Vector2d onSketchPos) override { @@ -228,6 +233,17 @@ private: bool allowOriginConstraint; // Conserve constraints with origin double refLength, length, scaleFactor; + struct HintEntry + { + SelectMode state; + std::list hints; + }; + + using HintTable = std::vector; + + static HintTable getScaleHintTable(); + static std::list lookupScaleHints(SelectMode state); + void deleteOriginalGeos() { @@ -447,6 +463,30 @@ private: } }; +DrawSketchHandlerScale::HintTable DrawSketchHandlerScale::getScaleHintTable() +{ + using enum Gui::InputHint::UserInput; + + return { + {.state = SelectMode::SeekFirst, + .hints = {{QObject::tr("%1 pick reference point", "Sketcher Scale: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekSecond, + .hints = {{QObject::tr("%1 set reference length", "Sketcher Scale: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekThird, + .hints = {{QObject::tr("%1 set scale factor", "Sketcher Scale: hint"), {MouseLeft}}}}}; +} + +std::list DrawSketchHandlerScale::lookupScaleHints(SelectMode state) +{ + const auto scaleHintTable = getScaleHintTable(); + + auto it = std::ranges::find_if(scaleHintTable, [state](const HintEntry& entry) { + return entry.state == state; + }); + + return (it != scaleHintTable.end()) ? it->hints : std::list {}; +} + template<> auto DSHScaleControllerBase::getState(int labelindex) const { diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h index 67c1b7381f..9ffe972cba 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h @@ -542,7 +542,7 @@ void DSHSlotController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -550,7 +550,7 @@ void DSHSlotController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h index bda7afdfbf..bb6988c17e 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h @@ -215,6 +215,15 @@ private: Sketcher::PointPos refPosId; bool deleteOriginal, createSymConstraints; +public: + std::list getToolHints() const override + { + using enum Gui::InputHint::UserInput; + + return { + {QObject::tr("%1 pick axis, edge, or point", "Sketcher Symmetry: hint"), {MouseLeft}}}; + } + void deleteOriginalGeos() { std::stringstream stream; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h index 9e19d89b57..9f2d5fa8fd 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h @@ -426,8 +426,51 @@ private: } } } + + struct HintEntry + { + SelectMode state; + std::list hints; + }; + + using HintTable = std::vector; + + static HintTable getTranslateHintTable(); + static std::list lookupTranslateHints(SelectMode state); + +public: + std::list getToolHints() const override + { + return lookupTranslateHints(state()); + } }; +DrawSketchHandlerTranslate::HintTable DrawSketchHandlerTranslate::getTranslateHintTable() +{ + using enum Gui::InputHint::UserInput; + + return {{.state = SelectMode::SeekFirst, + .hints = {{QObject::tr("%1 pick reference point", "Sketcher Translate: hint"), + {MouseLeft}}}}, + {.state = SelectMode::SeekSecond, + .hints = {{QObject::tr("%1 set translation vector", "Sketcher Translate: hint"), + {MouseLeft}}}}, + {.state = SelectMode::SeekThird, + .hints = {{QObject::tr("%1 set second translation vector", "Sketcher Translate: hint"), + {MouseLeft}}}}}; +} + +std::list DrawSketchHandlerTranslate::lookupTranslateHints(SelectMode state) +{ + const auto translateHintTable = getTranslateHintTable(); + + auto it = std::ranges::find_if(translateHintTable, [state](const HintEntry& entry) { + return entry.state == state; + }); + + return (it != translateHintTable.end()) ? it->hints : std::list {}; +} + template<> auto DSHTranslateControllerBase::getState(int labelindex) const { @@ -713,7 +756,7 @@ void DSHTranslateController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; @@ -721,7 +764,7 @@ void DSHTranslateController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { if (handler->secondNumberOfCopies == 1) { handler->setState(SelectMode::End); } @@ -734,7 +777,7 @@ void DSHTranslateController::doChangeDrawSketchHandlerMode() auto& fifthParam = onViewParameters[OnViewParameter::Fifth]; auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; diff --git a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h index c3549846e2..1b87ca8d4e 100644 --- a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h +++ b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h @@ -75,7 +75,7 @@ struct DrawingParameters const float zMidLines = 0.006f; // Height used for in-the-middle rendered lines const float zHighLines = 0.007f; // Height used for on top rendered lines const float zHighLine = 0.008f; // Height for highlighted lines (selected/preselected) - const float zConstr = 0.009f; // Height for rendering constraints + const float zConstr = 0.004f; // Height for rendering constraints const float zRootPoint = 0.010f; // Height used for rendering the root point const float zLowPoints = 0.011f; // Height used for bottom rendered points const float zMidPoints = 0.012f; // Height used for mid rendered points diff --git a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp index 92d71ab502..8e25ea1bc8 100644 --- a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp @@ -121,7 +121,6 @@ void EditModeConstraintCoinManager::processConstraints(const GeoListFacade& geol auto zConstrH = ViewProviderSketchCoinAttorney::getViewOrientationFactor(viewProvider) * drawingParameters.zConstr; - // After an undo/redo it can happen that we have an empty geometry list but a non-empty // constraint list In this case just ignore the constraints. (See bug #0000421) if (geolistfacade.geomlist.size() <= 2 && !constrlist.empty()) { @@ -1976,12 +1975,8 @@ void EditModeConstraintCoinManager::rebuildConstraintNodes( text->size.setValue(drawingParameters.labelFontSize); text->lineWidth = 2 * drawingParameters.pixelScalingFactor; text->useAntialiasing = false; - SoAnnotation* anno = new SoAnnotation(); - anno->renderCaching = SoSeparator::OFF; - anno->addChild(text); - // #define CONSTRAINT_SEPARATOR_INDEX_MATERIAL_OR_DATUMLABEL 0 sep->addChild(text); - editModeScenegraphNodes.constrGroup->addChild(anno); + editModeScenegraphNodes.constrGroup->addChild(sep); vConstrType.push_back((*it)->Type); // nodes not needed sep->unref(); diff --git a/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py b/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py index 00c9676ac0..e88c6dc6e7 100644 --- a/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py +++ b/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py @@ -464,8 +464,7 @@ class TestSketcherSolver(unittest.TestCase): sketch.addConstraint( [Sketcher.Constraint("Block", c_idx), Sketcher.Constraint("Block", l_idx)] ) - # use a negative distance to tell "line is within the circle" - expected_distance = -radius / 2 # note that we don't set this in the constraint below! + expected_distance = radius / 2 # note that we don't set this in the constraint below! # TODO: addConstraint(constraint) triggers a solve (for godd reasons) however, this way # one cannot add non-driving constraints. In contrast, addConstraint(list(constraint)) # does not solve automatically, thus we use this "overload". diff --git a/src/Mod/Spreadsheet/Gui/SheetTableView.cpp b/src/Mod/Spreadsheet/Gui/SheetTableView.cpp index 3ab62076a1..7d71dd7bd3 100644 --- a/src/Mod/Spreadsheet/Gui/SheetTableView.cpp +++ b/src/Mod/Spreadsheet/Gui/SheetTableView.cpp @@ -60,19 +60,18 @@ using namespace App; void SheetViewHeader::mouseMoveEvent(QMouseEvent* e) { // for some reason QWidget::setCursor() has no effect in QGraphicsView - // therefore we resort to override cursor + // therefore we resort to QGraphicsItem::setCursor const QCursor currentCursor = this->cursor(); QHeaderView::mouseMoveEvent(e); const QCursor newerCursor = this->cursor(); if (newerCursor != currentCursor) { - qApp->setOverrideCursor(newerCursor); + Q_EMIT cursorChanged(newerCursor); } } void SheetViewHeader::mouseReleaseEvent(QMouseEvent* event) { QHeaderView::mouseReleaseEvent(event); - qApp->setOverrideCursor(Qt::ArrowCursor); Q_EMIT resizeFinished(); } diff --git a/src/Mod/Spreadsheet/Gui/SheetTableView.h b/src/Mod/Spreadsheet/Gui/SheetTableView.h index 078b0aa61f..2c5bf81307 100644 --- a/src/Mod/Spreadsheet/Gui/SheetTableView.h +++ b/src/Mod/Spreadsheet/Gui/SheetTableView.h @@ -45,6 +45,7 @@ public: } Q_SIGNALS: void resizeFinished(); + void cursorChanged(QCursor); protected: void mouseMoveEvent(QMouseEvent* e) override; diff --git a/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp b/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp index bfea61b06f..725c893eba 100644 --- a/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp +++ b/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp @@ -148,6 +148,8 @@ SheetView::SheetView(Gui::Document* pcDocument, App::DocumentObject* docObj, QWi // Set document object to create auto completer ui->cellContent->setDocumentObject(sheet); ui->cellAlias->setDocumentObject(sheet); + + ui->cellContent->setPrefix('='); } SheetView::~SheetView() diff --git a/src/Mod/Spreadsheet/Gui/ZoomableView.cpp b/src/Mod/Spreadsheet/Gui/ZoomableView.cpp index 7c6a3b0693..52128dbf0c 100644 --- a/src/Mod/Spreadsheet/Gui/ZoomableView.cpp +++ b/src/Mod/Spreadsheet/Gui/ZoomableView.cpp @@ -122,6 +122,18 @@ ZoomableView::ZoomableView(Ui::Sheet* ui) }); resetZoom(); + + auto connectCursorChangedSignal = [this](QHeaderView* hv) { + auto header = qobject_cast(hv); + connect(header, + &SpreadsheetGui::SheetViewHeader::cursorChanged, + this, + [this](const QCursor& newerCursor) { + qpw->setCursor(newerCursor); + }); + }; + connectCursorChangedSignal(stv->horizontalHeader()); + connectCursorChangedSignal(stv->verticalHeader()); } int ZoomableView::zoomLevel() const diff --git a/src/Mod/TechDraw/App/DrawViewDetail.cpp b/src/Mod/TechDraw/App/DrawViewDetail.cpp index 4e7f322d0e..3a9d0a67e3 100644 --- a/src/Mod/TechDraw/App/DrawViewDetail.cpp +++ b/src/Mod/TechDraw/App/DrawViewDetail.cpp @@ -400,6 +400,13 @@ void DrawViewDetail::postHlrTasks(void) Scale.purgeTouched(); detailExec(m_saveShape, m_saveDvp, m_saveDvs); } + + auto* baseView = freecad_cast(BaseView.getValue()); + if (!baseView) { + throw Base::RuntimeError("Detail has no base view!"); + } + baseView->requestPaint(); // repaint the highlight on the base view. + overrideKeepUpdated(false); } diff --git a/src/Mod/TechDraw/App/DrawViewPart.cpp b/src/Mod/TechDraw/App/DrawViewPart.cpp index abc3157730..1b866a095b 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.cpp +++ b/src/Mod/TechDraw/App/DrawViewPart.cpp @@ -718,6 +718,38 @@ void DrawViewPart::onFacesFinished() requestPaint(); } + +//! returns the position of the first visible vertex within snap radius of newAnchorPoint. newAnchorPoint +//! should be unscaled in conventional coordinates. if no suitable vertex is found, newAnchorPoint +//! is returned. the result is unscaled and inverted? +Base::Vector3d DrawViewPart::snapHighlightToVertex(Base::Vector3d newAnchorPoint, + double radius) const +{ + if (!Preferences::snapDetailHighlights()) { + return newAnchorPoint; + } + + double snapRadius = radius * Preferences::detailSnapRadius(); + double dvpScale = getScale(); + std::vector vertexPoints; + auto vertsAll = getVertexGeometry(); + double nearDistance{std::numeric_limits::max()}; + Base::Vector3d nearPoint{newAnchorPoint}; + for (auto& vert: vertsAll) { + if (vert->getHlrVisible()) { + Base::Vector3d vertPointUnscaled = DU::invertY(vert->point()) / dvpScale; + double distanceToVertex = (vertPointUnscaled - newAnchorPoint).Length(); + if (distanceToVertex < snapRadius && + distanceToVertex < nearDistance) { + nearDistance = distanceToVertex; + nearPoint = vertPointUnscaled; + } + } + } + return nearPoint; +} + + //retrieve all the face hatches associated with this dvp std::vector DrawViewPart::getHatches() const { diff --git a/src/Mod/TechDraw/App/DrawViewPart.h b/src/Mod/TechDraw/App/DrawViewPart.h index 4b29b9db30..c4a971781d 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.h +++ b/src/Mod/TechDraw/App/DrawViewPart.h @@ -243,6 +243,9 @@ public: bool isCosmeticEdge(const std::string& element); bool isCenterLine(const std::string& element); + Base::Vector3d snapHighlightToVertex(Base::Vector3d newAnchorPoint, double radius) const; + + public Q_SLOTS: void onHlrFinished(void); void onFacesFinished(void); diff --git a/src/Mod/TechDraw/App/Preferences.cpp b/src/Mod/TechDraw/App/Preferences.cpp index f65a583042..c8dcee130b 100644 --- a/src/Mod/TechDraw/App/Preferences.cpp +++ b/src/Mod/TechDraw/App/Preferences.cpp @@ -693,3 +693,15 @@ bool Preferences::showUnits() } +bool Preferences::snapDetailHighlights() +{ + return Preferences::getPreferenceGroup("General")->GetBool("SnapHighlights", true); +} + + +//! distance within which we should snap a highlight to a vertex +double Preferences::detailSnapRadius() +{ + return getPreferenceGroup("General")->GetFloat("DetailSnapRadius", 0.6); +} + diff --git a/src/Mod/TechDraw/App/Preferences.h b/src/Mod/TechDraw/App/Preferences.h index 09689d6994..ea823bbb83 100644 --- a/src/Mod/TechDraw/App/Preferences.h +++ b/src/Mod/TechDraw/App/Preferences.h @@ -163,6 +163,9 @@ public: static bool showUnits(); + static bool snapDetailHighlights(); + static double detailSnapRadius(); + }; diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui index 8df8ba9242..fb821d651e 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui @@ -6,8 +6,8 @@ 0 0 - 578 - 1073 + 676 + 1200 @@ -254,7 +254,7 @@ for ProjectionGroups - + 0 @@ -264,10 +264,7 @@ for ProjectionGroups Label size - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + 8.000000000000000 @@ -449,7 +446,7 @@ for ProjectionGroups - + 1 @@ -468,7 +465,7 @@ for ProjectionGroups - + 1 @@ -487,7 +484,7 @@ for ProjectionGroups - + 1 @@ -518,7 +515,7 @@ for ProjectionGroups - + 1 @@ -537,7 +534,7 @@ for ProjectionGroups - + 1 @@ -547,9 +544,6 @@ for ProjectionGroups Default directory for welding symbols - - Gui::FileChooser::Directory - WeldingDir @@ -559,7 +553,7 @@ for ProjectionGroups - + 1 @@ -569,9 +563,6 @@ for ProjectionGroups Starting directory for menu 'Insert Page using Template' - - Gui::FileChooser::Directory - TemplateDir @@ -588,7 +579,7 @@ for ProjectionGroups - + 1 @@ -598,9 +589,6 @@ for ProjectionGroups Alternate directory to search for SVG symbol files. - - Gui::FileChooser::Directory - DirSymbol @@ -752,14 +740,11 @@ for ProjectionGroups - + Distance between Page grid lines. - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + 10.000000000000000 @@ -899,7 +884,7 @@ for ProjectionGroups - + @@ -919,6 +904,44 @@ for ProjectionGroups + + + + Check this box if you want detail view highlights to snap to the nearest vertex when dragging in TaskDetail. + + + Snap Detail Highlights + + + true + + + SnapHighlights + + + /Mod/TechDraw/General + + + + + + + When dragging a view, if it is within this fraction of view size of the correct alignment, it will snap into alignment. + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0.050000000000000 + + + SnapLimitFactor + + + /Mod/TechDraw/General + + + @@ -939,23 +962,31 @@ for ProjectionGroups - - + + + + Highlight SnappingFactor + + + + + - When dragging a view, if it is within this fraction of view size of the correct alignment, it will snap into alignment. + Controls the snap radius for highlights. Vertex must be within this factor times the highlight size to be a snap target. Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 0.050000000000000 + 0.600000000000000 - SnapLimitFactor + DetailSnapRadius /Mod/TechDraw/General + @@ -1046,8 +1077,6 @@ for ProjectionGroups

Gui/PrefWidgets.h
- - - + diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp index 30f04ee679..6cc3c89269 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp @@ -81,6 +81,8 @@ void DlgPrefsTechDrawGeneralImp::saveSettings() ui->cb_alwaysShowLabel->onSave(); ui->cb_SnapViews->onSave(); ui->psb_SnapFactor->onSave(); + ui->cb_SnapHighlights->onSave(); + ui->psb_HighlightSnapFactor->onSave(); } void DlgPrefsTechDrawGeneralImp::loadSettings() @@ -129,6 +131,9 @@ void DlgPrefsTechDrawGeneralImp::loadSettings() ui->cb_SnapViews->onRestore(); ui->psb_SnapFactor->onRestore(); + + ui->cb_SnapHighlights->onRestore(); + ui->psb_HighlightSnapFactor->onRestore(); } /** diff --git a/src/Mod/TechDraw/Gui/QGIHighlight.cpp b/src/Mod/TechDraw/Gui/QGIHighlight.cpp index a2886b657c..9fb0bc2e32 100644 --- a/src/Mod/TechDraw/Gui/QGIHighlight.cpp +++ b/src/Mod/TechDraw/Gui/QGIHighlight.cpp @@ -64,6 +64,8 @@ QGIHighlight::~QGIHighlight() } + +// QGIHighlight is no longer dragged except through TaskDetail. void QGIHighlight::onDragFinished() { // Base::Console().message("QGIH::onDragFinished - pos: %s\n", diff --git a/src/Mod/TechDraw/Gui/QGIViewPart.cpp b/src/Mod/TechDraw/Gui/QGIViewPart.cpp index 095fa93f14..8f2a7cafc3 100644 --- a/src/Mod/TechDraw/Gui/QGIViewPart.cpp +++ b/src/Mod/TechDraw/Gui/QGIViewPart.cpp @@ -930,7 +930,7 @@ void QGIViewPart::drawAllHighlights() void QGIViewPart::drawHighlight(TechDraw::DrawViewDetail* viewDetail, bool b) { - TechDraw::DrawViewPart* viewPart = static_cast(getViewObject()); + auto* viewPart = static_cast(getViewObject()); if (!viewPart || !viewDetail) { return; } @@ -950,14 +950,16 @@ void QGIViewPart::drawHighlight(TechDraw::DrawViewDetail* viewDetail, bool b) if (b) { double fontSize = Preferences::labelFontSizeMM(); - QGIHighlight* highlight = new QGIHighlight(); + auto* highlight = new QGIHighlight(); + scene()->addItem(highlight); highlight->setReference(viewDetail->Reference.getValue()); Base::Color color = Preferences::getAccessibleColor(vp->HighlightLineColor.getValue()); highlight->setColor(color.asValue()); highlight->setFeatureName(viewDetail->getNameInDocument()); - highlight->setInteractive(true); + + highlight->setInteractive(false); addToGroup(highlight); highlight->setPos(0.0, 0.0);//sb setPos(center.x, center.y)? @@ -986,20 +988,26 @@ void QGIViewPart::drawHighlight(TechDraw::DrawViewDetail* viewDetail, bool b) } } +//! this method is no longer used due to conflicts with TaskDetail dialog highlight drag void QGIViewPart::highlightMoved(QGIHighlight* highlight, QPointF newPos) { std::string highlightName = highlight->getFeatureName(); App::Document* doc = getViewObject()->getDocument(); App::DocumentObject* docObj = doc->getObject(highlightName.c_str()); auto detail = freecad_cast(docObj); - if (detail) { + auto baseView = freecad_cast(getViewObject()); + if (detail && baseView) { auto oldAnchor = detail->AnchorPoint.getValue(); Base::Vector3d delta = Rez::appX(DrawUtil::toVector3d(newPos)) / getViewObject()->getScale(); delta = DrawUtil::invertY(delta); - detail->AnchorPoint.setValue(oldAnchor + delta); + Base::Vector3d newAnchorPoint = oldAnchor + delta; + newAnchorPoint = baseView->snapHighlightToVertex(newAnchorPoint, + detail->Radius.getValue()); + detail->AnchorPoint.setValue(newAnchorPoint); } } + void QGIViewPart::drawMatting() { auto viewPart(dynamic_cast(getViewObject())); diff --git a/src/Mod/TechDraw/Gui/TaskDetail.cpp b/src/Mod/TechDraw/Gui/TaskDetail.cpp index 473674a785..f4a3776a16 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.cpp +++ b/src/Mod/TechDraw/Gui/TaskDetail.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include "ui_TaskDetail.h" #include "TaskDetail.h" @@ -59,7 +60,7 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): m_ghost(nullptr), m_detailFeat(nullptr), m_baseFeat(baseFeat), - m_basePage(nullptr), + m_basePage(m_baseFeat->findParentPage()), m_qgParent(nullptr), m_inProgressLock(false), m_btnOK(nullptr), @@ -67,9 +68,6 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): m_saveAnchor(Base::Vector3d(0.0, 0.0, 0.0)), m_saveRadius(0.0), m_saved(false), - m_baseName(std::string()), - m_pageName(std::string()), - m_detailName(std::string()), m_doc(nullptr), m_mode(CREATEMODE), m_created(false) @@ -137,9 +135,6 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): m_saveAnchor(Base::Vector3d(0.0, 0.0, 0.0)), m_saveRadius(0.0), m_saved(false), - m_baseName(std::string()), - m_pageName(std::string()), - m_detailName(std::string()), m_doc(nullptr), m_mode(EDITMODE), m_created(false) @@ -160,12 +155,13 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): App::DocumentObject* baseObj = m_detailFeat->BaseView.getValue(); m_baseFeat = dynamic_cast(baseObj); - if (m_baseFeat) { - m_baseName = m_baseFeat->getNameInDocument(); - } else { + if (!m_baseFeat) { Base::Console().error("TaskDetail - no BaseView. Can not proceed.\n"); return; } + m_baseName = m_baseFeat->getNameInDocument(); + // repaint baseObj here to make highlight inactive. + m_baseFeat->requestPaint(); ui->setupUi(this); @@ -219,7 +215,6 @@ void TaskDetail::changeEvent(QEvent *e) //save the start conditions void TaskDetail::saveDetailState() { -// Base::Console().message("TD::saveDetailState()\n"); TechDraw::DrawViewDetail* dvd = getDetailFeat(); m_saveAnchor = dvd->AnchorPoint.getValue(); m_saveRadius = dvd->Radius.getValue(); @@ -228,7 +223,6 @@ void TaskDetail::saveDetailState() void TaskDetail::restoreDetailState() { -// Base::Console().message("TD::restoreDetailState()\n"); TechDraw::DrawViewDetail* dvd = getDetailFeat(); dvd->AnchorPoint.setValue(m_saveAnchor); dvd->Radius.setValue(m_saveRadius); @@ -238,7 +232,6 @@ void TaskDetail::restoreDetailState() void TaskDetail::setUiFromFeat() { -// Base::Console().message("TD::setUIFromFeat()\n"); if (m_baseFeat) { std::string baseName = getBaseFeat()->getNameInDocument(); ui->leBaseView->setText(QString::fromStdString(baseName)); @@ -271,10 +264,12 @@ void TaskDetail::setUiFromFeat() ui->qsbRadius->setValue(radius); ui->qsbScale->setDecimals(decimals); ui->cbScaleType->setCurrentIndex(ScaleType); - if (ui->cbScaleType->currentIndex() == 2) // only if custom scale + if (ui->cbScaleType->currentIndex() == 2) { // only if custom scale ui->qsbScale->setEnabled(true); - else + } + else { ui->qsbScale->setEnabled(false); + } ui->qsbScale->setValue(scale); ui->leReference->setText(ref); } @@ -282,16 +277,23 @@ void TaskDetail::setUiFromFeat() //update ui point fields after tracker finishes void TaskDetail::updateUi(QPointF pos) { + ui->qsbX->blockSignals(true); + ui->qsbY->blockSignals(true); + ui->qsbX->setValue(pos.x()); - ui->qsbY->setValue(- pos.y()); + ui->qsbY->setValue(pos.y()); + + ui->qsbX->blockSignals(false); + ui->qsbY->blockSignals(false); } void TaskDetail::enableInputFields(bool isEnabled) { ui->qsbX->setEnabled(isEnabled); ui->qsbY->setEnabled(isEnabled); - if (ui->cbScaleType->currentIndex() == 2) // only if custom scale + if (ui->cbScaleType->currentIndex() == 2) { // only if custom scale ui->qsbScale->setEnabled(isEnabled); + } ui->qsbRadius->setEnabled(isEnabled); ui->leReference->setEnabled(isEnabled); } @@ -315,10 +317,10 @@ void TaskDetail::onScaleTypeEdit() { TechDraw::DrawViewDetail* detailFeat = getDetailFeat(); - if (ui->cbScaleType->currentIndex() == 0) { + detailFeat->ScaleType.setValue(ui->cbScaleType->currentIndex()); + if (ui->cbScaleType->currentIndex() == 0) { // page scale ui->qsbScale->setEnabled(false); - detailFeat->ScaleType.setValue(0.0); // set the page scale if there is a valid page if (m_basePage) { // set the page scale @@ -331,15 +333,12 @@ void TaskDetail::onScaleTypeEdit() else if (ui->cbScaleType->currentIndex() == 1) { // automatic scale (if view is too large to fit into page, it will be scaled down) ui->qsbScale->setEnabled(false); - detailFeat->ScaleType.setValue(1.0); // updating the feature will trigger the rescaling updateDetail(); } else if (ui->cbScaleType->currentIndex() == 2) { // custom scale ui->qsbScale->setEnabled(true); - detailFeat->ScaleType.setValue(2.0); - // no updateDetail() necessary since nothing visibly was changed } } @@ -359,12 +358,10 @@ void TaskDetail::onDraggerClicked(bool clicked) ui->pbDragger->setEnabled(false); enableInputFields(false); editByHighlight(); - return; } void TaskDetail::editByHighlight() { -// Base::Console().message("TD::editByHighlight()\n"); if (!m_ghost) { Base::Console().error("TaskDetail::editByHighlight - no ghost object\n"); return; @@ -382,34 +379,38 @@ void TaskDetail::editByHighlight() //dragEnd is in scene coords. void TaskDetail::onHighlightMoved(QPointF dragEnd) { -// Base::Console().message("TD::onHighlightMoved(%s) - highlight: %X\n", -// DrawUtil::formatVector(dragEnd).c_str(), m_ghost); ui->pbDragger->setEnabled(true); + double radius = m_detailFeat->Radius.getValue(); double scale = getBaseFeat()->getScale(); double x = Rez::guiX(getBaseFeat()->X.getValue()); double y = Rez::guiX(getBaseFeat()->Y.getValue()); DrawViewPart* dvp = getBaseFeat(); - DrawProjGroupItem* dpgi = freecad_cast(dvp); - if (dpgi) { - DrawProjGroup* dpg = dpgi->getPGroup(); - if (!dpg) { - Base::Console().message("TD::getAnchorScene - projection group is confused\n"); - //TODO::throw something. - return; - } + auto* dpgi = freecad_cast(dvp); + DrawProjGroup* dpg{nullptr}; + if (dpgi && DrawView::isProjGroupItem(dpgi)) { + dpg = dpgi->getPGroup(); + } + + if (dpg) { x += Rez::guiX(dpg->X.getValue()); y += Rez::guiX(dpg->Y.getValue()); } QPointF basePosScene(x, -y); //base position in scene coords QPointF anchorDisplace = dragEnd - basePosScene; - QPointF newAnchorPos = Rez::appX(anchorDisplace / scale); + QPointF newAnchorPosScene = Rez::appX(anchorDisplace / scale); - updateUi(newAnchorPos); + + Base::Vector3d newAnchorPosPage = DrawUtil::toVector3d(newAnchorPosScene); + newAnchorPosPage = DrawUtil::invertY(newAnchorPosPage); + Base::Vector3d snappedPos = dvp->snapHighlightToVertex(newAnchorPosPage, radius); + + updateUi(DrawUtil::toQPointF(snappedPos)); updateDetail(); enableInputFields(true); + m_ghost->setSelected(false); m_ghost->hide(); } @@ -430,7 +431,6 @@ void TaskDetail::enableTaskButtons(bool button) //***** Feature create & edit stuff ******************************************* void TaskDetail::createDetail() { -// Base::Console().message("TD::createDetail()\n"); Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Create Detail View")); const std::string objectName{"Detail"}; @@ -471,14 +471,15 @@ void TaskDetail::createDetail() void TaskDetail::updateDetail() { -// Base::Console().message("TD::updateDetail()\n"); + TechDraw::DrawViewDetail* detailFeat = getDetailFeat(); try { Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Update Detail")); double x = ui->qsbX->rawValue(); double y = ui->qsbY->rawValue(); Base::Vector3d temp(x, y, 0.0); - TechDraw::DrawViewDetail* detailFeat = getDetailFeat(); - detailFeat->AnchorPoint.setValue(temp); + + detailFeat->AnchorPoint.setValue(temp); // point2d + double scale = ui->qsbScale->rawValue(); detailFeat->Scale.setValue(scale); double radius = ui->qsbRadius->rawValue(); @@ -487,8 +488,6 @@ void TaskDetail::updateDetail() std::string ref = qRef.toStdString(); detailFeat->Reference.setValue(ref); - detailFeat->recomputeFeature(); - getBaseFeat()->requestPaint(); Gui::Command::updateActive(); Gui::Command::commitCommand(); } @@ -496,6 +495,8 @@ void TaskDetail::updateDetail() //this is probably due to appl closing while dialog is still open Base::Console().error("Task Detail - detail feature update failed.\n"); } + + detailFeat->recomputeFeature(); } //***** Getters **************************************************************** @@ -504,45 +505,42 @@ void TaskDetail::updateDetail() QPointF TaskDetail::getAnchorScene() { DrawViewPart* dvp = getBaseFeat(); - DrawProjGroupItem* dpgi = freecad_cast(dvp); + auto* dpgi = freecad_cast(dvp); DrawViewDetail* dvd = getDetailFeat(); Base::Vector3d anchorPos = dvd->AnchorPoint.getValue(); anchorPos.y = -anchorPos.y; Base::Vector3d basePos; double scale = 1; - if (!dpgi) { //base is normal view - double x = dvp->X.getValue(); - double y = dvp->Y.getValue(); - basePos = Base::Vector3d (x, -y, 0.0); - scale = dvp->getScale(); - } else { //part of projection group + double x = dvp->X.getValue(); + double y = dvp->Y.getValue(); + scale = dvp->getScale(); - DrawProjGroup* dpg = dpgi->getPGroup(); - if (!dpg) { - Base::Console().message("TD::getAnchorScene - projection group is confused\n"); - //TODO::throw something. - return QPointF(0.0, 0.0); - } - double x = dpg->X.getValue(); + DrawProjGroup* dpg{nullptr}; + if (dpgi && DrawProjGroup::isProjGroupItem(dpgi)) { + dpg = dpgi->getPGroup(); + } + + if (dpg) { + // part of a projection group + x = dpg->X.getValue(); x += dpgi->X.getValue(); - double y = dpg->Y.getValue(); + y = dpg->Y.getValue(); y += dpgi->Y.getValue(); - basePos = Base::Vector3d(x, -y, 0.0); scale = dpgi->getScale(); } + basePos = Base::Vector3d (x, -y, 0.0); + Base::Vector3d xyScene = Rez::guiX(basePos); Base::Vector3d anchorOffsetScene = Rez::guiX(anchorPos) * scale; Base::Vector3d netPos = xyScene + anchorOffsetScene; - return QPointF(netPos.x, netPos.y); + return {netPos.x, netPos.y}; } // protects against stale pointers DrawViewPart* TaskDetail::getBaseFeat() { -// Base::Console().message("TD::getBaseFeat()\n"); - if (m_doc) { App::DocumentObject* baseObj = m_doc->getObject(m_baseName.c_str()); if (baseObj) { @@ -560,8 +558,6 @@ DrawViewPart* TaskDetail::getBaseFeat() // protects against stale pointers DrawViewDetail* TaskDetail::getDetailFeat() { -// Base::Console().message("TD::getDetailFeat()\n"); - if (m_baseFeat) { App::DocumentObject* detailObj = m_baseFeat->getDocument()->getObject(m_detailName.c_str()); if (detailObj) { @@ -572,7 +568,6 @@ DrawViewDetail* TaskDetail::getDetailFeat() std::string msg = "TaskDetail - detail feature " + m_detailName + " not found \n"; -// throw Base::TypeError("TaskDetail - detail feature not found\n"); throw Base::TypeError(msg); return nullptr; } @@ -581,15 +576,14 @@ DrawViewDetail* TaskDetail::getDetailFeat() bool TaskDetail::accept() { -// Base::Console().message("TD::accept()\n"); - Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); - if (!doc) + if (!doc) { return false; + } m_ghost->hide(); - getDetailFeat()->requestPaint(); - getBaseFeat()->requestPaint(); + getDetailFeat()->recomputeFeature(); + Gui::Command::doCommand(Gui::Command::Gui, "Gui.ActiveDocument.resetEdit()"); return true; @@ -597,10 +591,10 @@ bool TaskDetail::accept() bool TaskDetail::reject() { -// Base::Console().message("TD::reject()\n"); Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); - if (!doc) + if (!doc) { return false; + } m_ghost->hide(); if (m_mode == CREATEMODE) { diff --git a/src/Mod/TechDraw/Gui/TaskDetail.h b/src/Mod/TechDraw/Gui/TaskDetail.h index 8a9cbd71b5..5c1ae309fc 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.h +++ b/src/Mod/TechDraw/Gui/TaskDetail.h @@ -28,6 +28,7 @@ #include #include +#include "QGIGhostHighlight.h" namespace TechDraw { diff --git a/src/Mod/TechDraw/Gui/TaskProjGroup.cpp b/src/Mod/TechDraw/Gui/TaskProjGroup.cpp index d19a178abf..b7b8543105 100644 --- a/src/Mod/TechDraw/Gui/TaskProjGroup.cpp +++ b/src/Mod/TechDraw/Gui/TaskProjGroup.cpp @@ -777,12 +777,10 @@ QString TaskProjGroup::formatVector(Base::Vector3d vec) } void TaskProjGroup::saveButtons(QPushButton* btnOK, - QPushButton* btnCancel, - QPushButton* btnApply) + QPushButton* btnCancel) { m_btnOK = btnOK; m_btnCancel = btnCancel; - m_btnApply = btnApply; } @@ -887,8 +885,7 @@ void TaskDlgProjGroup::modifyStandardButtons(QDialogButtonBox* box) { QPushButton* btnOK = box->button(QDialogButtonBox::Ok); QPushButton* btnCancel = box->button(QDialogButtonBox::Cancel); - QPushButton* btnApply = box->button(QDialogButtonBox::Apply); - widget->saveButtons(btnOK, btnCancel, btnApply); + widget->saveButtons(btnOK, btnCancel); } //==== calls from the TaskView =============================================================== diff --git a/src/Mod/TechDraw/Gui/TaskProjGroup.h b/src/Mod/TechDraw/Gui/TaskProjGroup.h index 501123e761..2c40f4e952 100644 --- a/src/Mod/TechDraw/Gui/TaskProjGroup.h +++ b/src/Mod/TechDraw/Gui/TaskProjGroup.h @@ -61,8 +61,7 @@ public: virtual bool apply(); void modifyStandardButtons(QDialogButtonBox* box); void saveButtons(QPushButton* btnOK, - QPushButton* btnCancel, - QPushButton* btnApply); + QPushButton* btnCancel); void updateTask(); // Sets the numerator and denominator widgets to match newScale @@ -126,7 +125,6 @@ private: QPushButton* m_btnOK{nullptr}; QPushButton* m_btnCancel{nullptr}; - QPushButton* m_btnApply{nullptr}; std::vector m_saveSource; std::string m_saveProjType; @@ -151,7 +149,7 @@ public: TechDraw::DrawView* getView() const { return view; } QDialogButtonBox::StandardButtons getStandardButtons() const override - { return QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel; } + { return QDialogButtonBox::Ok | QDialogButtonBox::Cancel; } void modifyStandardButtons(QDialogButtonBox* box) override; /// is called the TaskView when the dialog is opened diff --git a/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp b/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp index 10e6d080c2..a3696d81ff 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp +++ b/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp @@ -60,6 +60,7 @@ #include "TaskProjGroup.h" #include "ViewProviderViewPart.h" #include "ViewProviderPage.h" +#include "QGIViewPart.h" #include "QGIViewDimension.h" #include "QGIViewBalloon.h" #include "QGSPage.h" @@ -213,8 +214,8 @@ void ViewProviderViewPart::onChanged(const App::Property* prop) void ViewProviderViewPart::attach(App::DocumentObject *pcFeat) { // Base::Console().message("VPVP::attach(%s)\n", pcFeat->getNameInDocument()); - TechDraw::DrawViewMulti* dvm = dynamic_cast(pcFeat); - TechDraw::DrawViewDetail* dvd = dynamic_cast(pcFeat); + auto* dvm = dynamic_cast(pcFeat); + auto* dvd = dynamic_cast(pcFeat); if (dvm) { sPixmap = "TechDraw_TreeMulti"; } else if (dvd) { @@ -269,7 +270,7 @@ std::vector ViewProviderViewPart::claimChildren() const } return temp; } catch (...) { - return std::vector(); + return {}; } } @@ -287,25 +288,32 @@ bool ViewProviderViewPart::setEdit(int ModNum) Gui::Selection().clearSelection(); TechDraw::DrawViewPart* dvp = getViewObject(); - TechDraw::DrawViewDetail* dvd = dynamic_cast(dvp); + auto* dvd = dynamic_cast(dvp); if (dvd) { if (!dvd->BaseView.getValue()) { Base::Console().error("DrawViewDetail - %s - has no BaseView!\n", dvd->getNameInDocument()); return false; } - Gui::Control().showDialog(new TaskDlgDetail(dvd)); - Gui::Selection().clearSelection(); - Gui::Selection().addSelection(dvd->getDocument()->getName(), - dvd->getNameInDocument()); - } - else { - auto* view = getObject(); - Gui::Control().showDialog(new TaskDlgProjGroup(view, false)); + return setDetailEdit(ModNum, dvd); } + auto* view = getObject(); + Gui::Control().showDialog(new TaskDlgProjGroup(view, false)); return true; } +bool ViewProviderViewPart::setDetailEdit(int ModNum, DrawViewDetail* dvd) +{ + Q_UNUSED(ModNum); + + Gui::Control().showDialog(new TaskDlgDetail(dvd)); + Gui::Selection().clearSelection(); + Gui::Selection().addSelection(dvd->getDocument()->getName(), + dvd->getNameInDocument()); + return true; +} + + bool ViewProviderViewPart::doubleClicked() { setEdit(ViewProvider::Default); diff --git a/src/Mod/TechDraw/Gui/ViewProviderViewPart.h b/src/Mod/TechDraw/Gui/ViewProviderViewPart.h index d19137e349..2d9b89ab0c 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderViewPart.h +++ b/src/Mod/TechDraw/Gui/ViewProviderViewPart.h @@ -71,6 +71,7 @@ public: bool onDelete(const std::vector &) override; bool canDelete(App::DocumentObject* obj) const override; bool setEdit(int ModNum) override; + bool setDetailEdit(int ModNum, TechDraw::DrawViewDetail* dvd); bool doubleClicked(void) override; void onChanged(const App::Property *prop) override; void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property * prop) override; diff --git a/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py b/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py index b6f113e922..b66aa1fecf 100644 --- a/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py +++ b/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py @@ -54,7 +54,13 @@ class CommandVertexCreationGroup: return 0 def GetResources(self): - return {'Pixmap':'TechDraw_ExtensionVertexAtIntersection'} + """Return a dictionary with data that will be used by the button or menu item.""" + return {'Pixmap': 'TechDraw_ExtensionVertexAtIntersection.svg', + 'Accel': "", + 'MenuText': QT_TRANSLATE_NOOP("TechDraw_ExtensionVertexAtIntersection","Add Cosmetic Intersection Vertex(es)"), + 'ToolTip': QT_TRANSLATE_NOOP("TechDraw_ExtensionVertexAtIntersection", "Add cosmetic vertex(es) at the intersection(s) of selected edges:
\ + - Select two edges
\ + - Click this tool")} def IsActive(self): """Return True when the command should be active or False when it should be disabled (greyed).""" diff --git a/src/Tools/SubWCRev.py b/src/Tools/SubWCRev.py index 22e0cc9a6a..7c73a2d321 100644 --- a/src/Tools/SubWCRev.py +++ b/src/Tools/SubWCRev.py @@ -10,6 +10,7 @@ # 2011/02/05: The script was extended to support also Bazaar import os, sys, re, time, getopt +from urllib.parse import urlparse import xml.sax import xml.sax.handler import xml.sax.xmlreader @@ -275,9 +276,10 @@ class GitControl(VersionControl): match = re.match(r"ssh://\S+?@(\S+)", url) if match is not None: url = "git://%s" % match.group(1) + parsed_url = urlparse(url) entryscore = ( url == "git://github.com/FreeCAD/FreeCAD.git", - "github.com" in url, + parsed_url.netloc == "github.com", branch == self.branch, branch == "main", "@" not in url, diff --git a/src/Tools/plugins/widget/customwidgets.cpp b/src/Tools/plugins/widget/customwidgets.cpp index 79836644b2..2514dbc18f 100644 --- a/src/Tools/plugins/widget/customwidgets.cpp +++ b/src/Tools/plugins/widget/customwidgets.cpp @@ -22,11 +22,13 @@ #include +#include #include #include #include #include #include +#include #include #include #include @@ -544,6 +546,66 @@ void InputField::setHistorySize(int i) // -------------------------------------------------------------------- +ExpressionLineEdit::ExpressionLineEdit(QWidget* parent) + : QLineEdit(parent) + , exactMatch {false} +{ + completer = new QCompleter(this); + connect(this, &QLineEdit::textEdited, this, &ExpressionLineEdit::slotTextChanged); +} + +void ExpressionLineEdit::setExactMatch(bool enabled) +{ + exactMatch = enabled; + if (completer) { + completer->setFilterMode(exactMatch ? Qt::MatchStartsWith : Qt::MatchContains); + } +} + +void ExpressionLineEdit::slotTextChanged(const QString& text) +{ + Q_EMIT textChanged2(text, cursorPosition()); +} + +void ExpressionLineEdit::slotCompleteText(const QString& completionPrefix, bool isActivated) +{ + Q_UNUSED(completionPrefix) + Q_UNUSED(isActivated) +} + +void ExpressionLineEdit::slotCompleteTextHighlighted(const QString& completionPrefix) +{ + slotCompleteText(completionPrefix, false); +} + +void ExpressionLineEdit::slotCompleteTextSelected(const QString& completionPrefix) +{ + slotCompleteText(completionPrefix, true); +} + +void ExpressionLineEdit::keyPressEvent(QKeyEvent* e) +{ + QLineEdit::keyPressEvent(e); +} + +void ExpressionLineEdit::contextMenuEvent(QContextMenuEvent* event) +{ + QMenu* menu = createStandardContextMenu(); + + if (completer) { + menu->addSeparator(); + QAction* match = menu->addAction(tr("Exact match")); + match->setCheckable(true); + match->setChecked(completer->filterMode() == Qt::MatchStartsWith); + QObject::connect(match, &QAction::toggled, this, &Gui::ExpressionLineEdit::setExactMatch); + } + menu->setAttribute(Qt::WA_DeleteOnClose); + + menu->popup(event->globalPos()); +} + +// -------------------------------------------------------------------- + namespace Base { diff --git a/src/Tools/plugins/widget/customwidgets.h b/src/Tools/plugins/widget/customwidgets.h index c80966b493..ed91cf2c24 100644 --- a/src/Tools/plugins/widget/customwidgets.h +++ b/src/Tools/plugins/widget/customwidgets.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -386,6 +387,33 @@ private: // ------------------------------------------------------------------------------ +class ExpressionLineEdit: public QLineEdit +{ + Q_OBJECT +public: + ExpressionLineEdit(QWidget* parent = nullptr); + +public Q_SLOTS: + void slotTextChanged(const QString& text); + void slotCompleteText(const QString& completionPrefix, bool isActivated); + void slotCompleteTextHighlighted(const QString& completionPrefix); + void slotCompleteTextSelected(const QString& completionPrefix); + void setExactMatch(bool enabled = true); + +protected: + void keyPressEvent(QKeyEvent* event) override; + void contextMenuEvent(QContextMenuEvent* event) override; + +Q_SIGNALS: + void textChanged2(QString text, int pos); + +private: + QCompleter* completer; + bool exactMatch; +}; + +// ------------------------------------------------------------------------------ + class QuantitySpinBoxPrivate; class QuantitySpinBox: public QAbstractSpinBox { diff --git a/src/Tools/plugins/widget/plugin.cpp b/src/Tools/plugins/widget/plugin.cpp index 62b9672fff..dee5844e13 100644 --- a/src/Tools/plugins/widget/plugin.cpp +++ b/src/Tools/plugins/widget/plugin.cpp @@ -523,6 +523,56 @@ public: } }; +class ExpressionLineEditPlugin: public QDesignerCustomWidgetInterface +{ + Q_INTERFACES(QDesignerCustomWidgetInterface) +public: + ExpressionLineEditPlugin() + {} + QWidget* createWidget(QWidget* parent) + { + return new Gui::ExpressionLineEdit(parent); + } + QString group() const + { + return QLatin1String("Input Widgets"); + } + QIcon icon() const + { + return QIcon(QPixmap(inputfield_pixmap)); + } + QString includeFile() const + { + return QLatin1String("Gui/InputField.h"); + } + QString toolTip() const + { + return QLatin1String("Expression line edit"); + } + QString whatsThis() const + { + return QLatin1String("A widget to work with expressions."); + } + bool isContainer() const + { + return false; + } + QString domXml() const + { + return "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + ""; + } + QString name() const + { + return QLatin1String("Gui::ExpressionLineEdit"); + } +}; + /* XPM */ static const char* quantityspinbox_pixmap[] = {"22 22 6 1", "a c #000000", @@ -1653,6 +1703,7 @@ QList CustomWidgetPlugin::customWidgets() const cw.append(new FileChooserPlugin); cw.append(new AccelLineEditPlugin); cw.append(new ActionSelectorPlugin); + cw.append(new ExpressionLineEditPlugin); cw.append(new InputFieldPlugin); cw.append(new QuantitySpinBoxPlugin); cw.append(new CommandIconViewPlugin); diff --git a/tests/src/Base/Matrix.cpp b/tests/src/Base/Matrix.cpp index 63b3bb0a01..6d1e280627 100644 --- a/tests/src/Base/Matrix.cpp +++ b/tests/src/Base/Matrix.cpp @@ -1,6 +1,7 @@ #include #include #include +#include // NOLINTBEGIN(cppcoreguidelines-*,readability-magic-numbers) // clang-format off @@ -155,6 +156,18 @@ TEST(Matrix, TestMultVec) Base::Vector3d vec2 {1, 1, 1}; mat.multVec(vec2, vec2); EXPECT_EQ(vec2, Base::Vector3d(6.0, 7.0, 8.0)); + + Base::Vector3f vec3{1.0F,1.0F,3.0F}; + vec3 = mat * vec3; + EXPECT_EQ(vec3, Base::Vector3f(12.0F, 9.0F, 12.0F)); + + Base::Vector3f vec4 {1.0F, 1.0F, 1.0F}; + mat.multVec(vec4, vec4); + EXPECT_EQ(vec4, Base::Vector3f(6.0F, 7.0F, 8.0F)); + + Base::Vector3f vec5 {1.0F, 1.0F, 1.0F}; + vec5 *= mat; + EXPECT_EQ(vec5, Base::Vector3f(6.0F, 7.0F, 8.0F)); } TEST(Matrix, TestMult) @@ -173,6 +186,7 @@ TEST(Matrix, TestMult) 10.0, 13.0, 13.0, 7.0, 0.0, 0.0, 0.0, 1.0}; EXPECT_EQ(mat3, mat4); + EXPECT_NE(mat3, Base::Matrix4D()); } TEST(Matrix, TestMultAssign) @@ -280,6 +294,21 @@ TEST(Matrix, TestHatOperator) EXPECT_EQ(mat1, mat2); } +TEST(Matrix, TestHatOperatorFloat) +{ + Base::Vector3f vec{1.0, 2.0, 3.0}; + + Base::Matrix4D mat1; + mat1.Hat(vec); + + Base::Matrix4D mat2{0.0F, -vec.z, vec.y, 0.0F, + vec.z, 0.0F, -vec.x, 0.0F, + -vec.y, vec.x, 0.0F, 0.0F, + 0.0F, 0.0F, 0.0F, 1.0F}; + + EXPECT_EQ(mat1, mat2); +} + TEST(Matrix, TestDyadic) { Base::Vector3d vec{1.0, 2.0, 3.0}; @@ -295,6 +324,21 @@ TEST(Matrix, TestDyadic) EXPECT_EQ(mat1, mat2); } +TEST(Matrix, TestDyadicFloat) +{ + Base::Vector3f vec{1.0F, 2.0F, 3.0F}; + + Base::Matrix4D mat1; + mat1.Outer(vec, vec); + + Base::Matrix4D mat2{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + EXPECT_EQ(mat1, mat2); +} + TEST(Matrix, TestDecomposeScale) { Base::Matrix4D mat; @@ -333,6 +377,18 @@ TEST(Matrix, TestDecomposeMove) EXPECT_EQ(res[3], mat); } +TEST(Matrix, TestDecomposeMoveFloat) +{ + Base::Matrix4D mat; + mat.move(Base::Vector3f(1.0F, 2.0F, 3.0F)); + auto res = mat.decompose(); + + EXPECT_TRUE(res[0].isUnity()); + EXPECT_TRUE(res[1].isUnity()); + EXPECT_TRUE(res[2].isUnity()); + EXPECT_EQ(res[3], mat); +} + TEST(Matrix, TestDecompose) { Base::Matrix4D mat; @@ -401,5 +457,121 @@ TEST(Matrix, TestRotAxisFormula) //NOLINT EXPECT_DOUBLE_EQ(mat1[2][1], mat2[2][1]); EXPECT_DOUBLE_EQ(mat1[2][2], mat2[2][2]); } + +TEST(Matrix, TestTransform) +{ + Base::Matrix4D mat; + mat.rotZ(Base::toRadians(90.0)); + + Base::Matrix4D unity; + unity.transform(Base::Vector3d(10.0, 0.0, 0.0), mat); + + Base::Matrix4D mov{0.0, -1.0, 0.0, 10.0, + 1.0, 0.0, 0.0, -10.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + EXPECT_EQ(unity, mov); +} + +TEST(Matrix, TestTransformFloat) +{ + Base::Matrix4D mat; + mat.rotZ(Base::toRadians(90.0)); + + Base::Matrix4D mat2; + mat2.transform(Base::Vector3f(10.0F, 0.0F, 0.0F), mat); + + Base::Matrix4D mov{0.0, -1.0, 0.0, 10.0, + 1.0, 0.0, 0.0, -10.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + EXPECT_EQ(mat2, mov); +} + +TEST(Matrix, TestInverseOrthogonal) +{ + Base::Matrix4D mat; + mat.rotZ(Base::toRadians(90.0)); + + Base::Matrix4D mat2; + mat2.transform(Base::Vector3d(10.0, 0.0, 0.0), mat); + mat2.inverseOrthogonal(); + + Base::Matrix4D mov{0.0, 1.0, 0.0, 10.0, + -1.0, 0.0, 0.0, 10.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + EXPECT_EQ(mat2, mov); +} + +TEST(Matrix, TestTranspose) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, 7.0}; + + Base::Matrix4D trp{1.0, 5.0, 9.0, 4.0, + 2.0, 6.0, 1.0, 5.0, + 3.0, 7.0, 2.0, 6.0, + 4.0, 8.0, 3.0, 7.0}; + + mat.transpose(); + EXPECT_EQ(mat, trp); +} + +TEST(Matrix, TestTrace) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, 7.0}; + EXPECT_DOUBLE_EQ(mat.trace(), 16.0); + EXPECT_DOUBLE_EQ(mat.trace3(), 9.0); +} + +TEST(Matrix, TestSetAndGetMatrix) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + std::array values; + mat.getMatrix(values.data()); + Base::Matrix4D inp; + inp.setMatrix(values.data()); + + EXPECT_EQ(mat, inp); +} + +TEST(Matrix, TestSetAndGetGLMatrix) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + std::array values; + mat.getGLMatrix(values.data()); + Base::Matrix4D inp; + inp.setGLMatrix(values.data()); + + EXPECT_EQ(mat, inp); +} + +TEST(Matrix, TestToAndFromString) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + std::string str = mat.toString(); + Base::Matrix4D inp; + inp.fromString(str); + + EXPECT_EQ(mat, inp); +} // clang-format on // NOLINTEND(cppcoreguidelines-*,readability-magic-numbers) diff --git a/tools/lint/changed_lines.py b/tools/lint/changed_lines.py new file mode 100644 index 0000000000..f292e0e46b --- /dev/null +++ b/tools/lint/changed_lines.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 + +# Modified to generate output compatible with `clang-tidy`'s `--line-filter` option +# +# Based on https://github.com/hestonhoffman/changed-lines/blob/main/main.py +# +# Original License +# +# The MIT License (MIT) +# +# Copyright (c) 2023 Heston Hoffman +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +Uses GitHub API to grab patch data for a PR and calculate changed lines +""" + +import argparse +import json +import os +import re +import requests + + +class MissingPatchData(Exception): + """Raised when the patch data is missing""" + + +def fetch_patch(args): + """Grabs the patch data from the GitHub API.""" + git_session = requests.Session() + headers = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + if args.token: + headers["Authorization"] = f"Bearer {args.token}" + + git_request = git_session.get( + f"{args.api_url}/repos/{args.repo}/pulls/{args.pr}/files", headers=headers + ) + return git_request.json() + + +def parse_patch_file(entry): + """Parses the individual file changes within a patch""" + line_array = [] + sublist = [] + + patch_array = re.split("\n", entry["patch"]) + # clean patch array + patch_array = [i for i in patch_array if i] + + for item in patch_array: + # Grabs hunk annotation and strips out added lines + if item.startswith("@@ -"): + if sublist: + line_array.append(sublist) + sublist = [re.sub(r"\s@@(.*)", "", item.split("+")[1])] + # We don't need removed lines ('-') + elif not item.startswith("-") and not item == "\\ No newline at end of file": + sublist.append(item) + if sublist: + line_array.append(sublist) + return line_array + + +def parse_patch_data(patch_data): + """Takes the patch data and returns a dictionary of files and the lines""" + + final_dict = {} + for entry in patch_data: + # We don't need removed files + if entry["status"] == "removed": + continue + + # We can only operate on files with additions and a patch key + # Some really big files don't have a patch key because GitHub + # returns a message in the PR that the file is too large to display + if entry["additions"] != 0 and "patch" in entry: + line_array = parse_patch_file(entry) + final_dict[entry["filename"]] = line_array + return final_dict + + +def get_lines(line_dict): + """Takes the dictionary of files and lines and returns a dictionary of files and line numbers""" + final_dict = {} + for file_name, sublist in line_dict.items(): + line_array = [] + for array in sublist: + line_number = 0 + if "," not in array[0]: + line_number = int(array[0]) - 1 + else: + line_number = int(array[0].split(",")[0]) - 1 + + start = -1 + end = -1 + for line in array: + if line.startswith("+"): + if start < 0: + start = line_number + end = line_number + # line_array.append(line_number) + line_number += 1 + line_array.append([start, end]) + + # Remove deleted/renamed files (which appear as empty arrays) + if line_array: + final_dict[file_name] = line_array + return final_dict + + +def main(): + """main()""" + parser = argparse.ArgumentParser( + prog="changed_lines.py", + description="Identifies the changed files and lines in a GitHub PR.", + ) + parser.add_argument("--token") + parser.add_argument("--api-url", default="https://api.github.com") + parser.add_argument("--repo", default="FreeCAD/FreeCAD") + parser.add_argument("--ref", required=True) + parser.add_argument("--pr", required=True) + parser.add_argument("--file-filter", default="") + args = parser.parse_args() + + data = fetch_patch(args) + added_line_data = parse_patch_data(data) + added_lines = get_lines(added_line_data) + + if args.file_filter: + args.file_filter = set(args.file_filter.replace(" ", "").split(",")) + + filename_list = [] + line_filter = [] + for filename, _ in added_lines.items(): + if (not args.file_filter) or ( + os.path.splitext(filename)[1] in args.file_filter + ): + filename_list.append(filename) + lines_modified = {} + lines_modified["name"] = filename + lines_modified["lines"] = added_lines[filename] + line_filter.append(lines_modified) + + print(f"{json.dumps(line_filter)}") + + +if __name__ == "__main__": + main() diff --git a/tools/lint/clang_tidy.py b/tools/lint/clang_tidy.py index ea48be086c..8cb86c4f25 100644 --- a/tools/lint/clang_tidy.py +++ b/tools/lint/clang_tidy.py @@ -76,6 +76,11 @@ def main(): required=True, help="Clang-format style (e.g., 'file' to use .clang-format or a specific style).", ) + parser.add_argument( + "--line-filter", + required=False, + help='Line-filter for clang-tidy (i.e. [{"name":"file1.cpp","lines":[[1,3],[5,7]]},...])', + ) args = parser.parse_args() init_environment(args) @@ -96,7 +101,11 @@ def main(): enabled_checks_log = os.path.join(args.log_dir, "clang-tidy-enabled-checks.log") write_file(enabled_checks_log, enabled_output) - clang_cmd = clang_tidy_base_cmd + args.files.split() + clang_cmd = clang_tidy_base_cmd + if args.line_filter: + clang_cmd = clang_cmd + [f"--line-filter={args.line_filter}"] + clang_cmd = clang_cmd + args.files.split() + print("clang_cmd = ", clang_cmd) clang_stdout, clang_stderr, _ = run_command(clang_cmd) clang_tidy_output = clang_stdout + clang_stderr