Merge pull request #20891 from ickby/FEM_python_filter

FEM: Enabling postprocessing filters written in python, and adding a glyph example
This commit is contained in:
Chris Hennes
2025-05-05 21:19:49 -05:00
committed by GitHub
37 changed files with 2201 additions and 195 deletions

View File

@@ -42,7 +42,7 @@ macro(SetupSalomeSMESH)
endif()
endforeach()
else()
set(VTK_COMPONENTS "CommonCore;CommonDataModel;FiltersVerdict;IOXML;FiltersCore;FiltersGeneral;IOLegacy;FiltersExtraction;FiltersSources;FiltersGeometry")
set(VTK_COMPONENTS "CommonCore;CommonDataModel;FiltersVerdict;IOXML;FiltersCore;FiltersGeneral;IOLegacy;FiltersExtraction;FiltersSources;FiltersGeometry;WrappingPythonCore")
list(APPEND VTK_COMPONENTS "IOMPIParallel;ParallelMPI;hdf5;FiltersParallelDIY2;RenderingCore;InteractionStyle;RenderingFreeType;RenderingOpenGL2")
foreach(_module ${VTK_COMPONENTS})
list (FIND VTK_AVAILABLE_COMPONENTS ${_module} _index)
@@ -63,6 +63,17 @@ macro(SetupSalomeSMESH)
endif()
set(BUILD_FEM_VTK ON)
# Check if PythonWrapperCore was found
# Note: VTK 9 only, as the implementations use the VTK modules introduced in 8.1
# VTK_WrappingPythonCore_FOUND is named differently for versions <9.0
if (${VTK_WrappingPythonCore_FOUND})
set(BUILD_FEM_VTK_PYTHON 1)
message(STATUS "VTK python wrapper: available")
else()
message(STATUS "VTK python wrapper: NOT available")
endif()
if(${VTK_MAJOR_VERSION} LESS 6)
message( FATAL_ERROR "Found VTK version is <6, this is not compatible" )
endif()

View File

@@ -206,6 +206,8 @@ PyMOD_INIT_FUNC(Fem)
Fem::FemPostSphereFunction ::init();
Fem::PropertyPostDataObject ::init();
Fem::PostFilterPython ::init();
#endif
// clang-format on

View File

@@ -40,6 +40,11 @@
#ifdef FC_USE_VTK
#include "FemPostPipeline.h"
#include "FemVTKTools.h"
#include <LibraryVersions.h>
#endif
#ifdef FC_USE_VTK_PYTHON
#include <vtkPythonUtil.h>
#endif
@@ -75,6 +80,15 @@ public:
&Module::writeResult,
"write a CFD or FEM result (auto detect) to a file (file format "
"detected from file suffix)");
add_varargs_method("getVtkVersion",
&Module::getVtkVersion,
"Returns the VTK version FreeCAD is linked against");
#ifdef FC_USE_VTK_PYTHON
add_varargs_method(
"isVtkCompatible",
&Module::isVtkCompatible,
"Checks if the passed vtkObject is compatible with the c++ VTK version FreeCAD uses");
#endif
#endif
add_varargs_method("show",
&Module::show,
@@ -318,6 +332,34 @@ private:
return Py::None();
}
Py::Object getVtkVersion(const Py::Tuple& args)
{
if (!PyArg_ParseTuple(args.ptr(), "")) {
throw Py::Exception();
}
return Py::String(fcVtkVersion);
}
#ifdef FC_USE_VTK_PYTHON
Py::Object isVtkCompatible(const Py::Tuple& args)
{
PyObject* pcObj = nullptr;
if (!PyArg_ParseTuple(args.ptr(), "O", &pcObj)) {
throw Py::Exception();
}
// if non is returned the VTK object was created by annother VTK library, and the
// python api used to create it cannot be used with FreeCAD
vtkObjectBase* obj = vtkPythonUtil::GetPointerFromObject(pcObj, "vtkObject");
if (!obj) {
PyErr_Clear();
return Py::False();
}
return Py::True();
}
#endif
#endif
Py::Object show(const Py::Tuple& args)

View File

@@ -67,11 +67,14 @@ if(BUILD_FEM_VTK)
FemPostObjectPyImp.cpp
FemPostPipelinePy.xml
FemPostPipelinePyImp.cpp
FemPostFilterPy.xml
FemPostFilterPyImp.cpp
FemPostBranchFilterPy.xml
FemPostBranchFilterPyImp.cpp
)
generate_from_xml(FemPostObjectPy)
generate_from_xml(FemPostPipelinePy)
generate_from_xml(FemPostFilterPy)
generate_from_xml(FemPostBranchFilterPy)
endif(BUILD_FEM_VTK)
SOURCE_GROUP("Python" FILES ${Python_SRCS})

View File

@@ -31,10 +31,13 @@
#include <vtkUnstructuredGrid.h>
#endif
#include <App/FeaturePythonPyImp.h>
#include <App/Document.h>
#include <Base/Console.h>
#include "FemPostFilter.h"
#include "FemPostFilterPy.h"
#include "FemPostPipeline.h"
#include "FemPostBranchFilter.h"
@@ -52,22 +55,42 @@ FemPostFilter::FemPostFilter()
"Data",
App::Prop_ReadOnly,
"The step used to calculate the data");
// the default pipeline: just a passthrough
// this is used to simplify the python filter handling,
// as those do not have filter pipelines setup till later
// in the document loading process.
auto filter = vtkPassThrough::New();
auto pipeline = FemPostFilter::FilterPipeline();
pipeline.algorithmStorage.push_back(filter);
pipeline.source = filter;
pipeline.target = filter;
addFilterPipeline(pipeline, "__passthrough__");
}
FemPostFilter::~FemPostFilter() = default;
void FemPostFilter::addFilterPipeline(const FemPostFilter::FilterPipeline& p, std::string name)
{
m_pipelines[name] = p;
if (m_activePipeline.empty()) {
m_activePipeline = name;
}
}
FemPostFilter::FilterPipeline& FemPostFilter::getFilterPipeline(std::string name)
{
return m_pipelines[name];
return m_pipelines.at(name);
}
void FemPostFilter::setActiveFilterPipeline(std::string name)
{
if (m_pipelines.count(name) == 0) {
throw Base::ValueError("Not a filter pipline name");
}
if (m_activePipeline != name && isValid()) {
// disable all inputs of current pipeline
@@ -129,6 +152,7 @@ void FemPostFilter::onChanged(const App::Property* prop)
{
if (prop == &Placement) {
if (Placement.getValue().isIdentity() && m_use_transform) {
// remove transform from pipeline
if (m_transform_location == TransformLocation::output) {
@@ -191,7 +215,6 @@ DocumentObjectExecReturn* FemPostFilter::execute()
Data.setValue(output->GetOutputDataObject(0));
}
return StdReturn;
}
@@ -203,8 +226,19 @@ vtkSmartPointer<vtkDataSet> FemPostFilter::getInputData()
}
vtkAlgorithmOutput* output = active.source->GetInputConnection(0, 0);
if (!output) {
return nullptr;
}
vtkAlgorithm* algo = output->GetProducer();
if (!algo) {
return nullptr;
}
if (Frame.getValue() > 0) {
algo->UpdateTimeStep(Frame.getValue());
}
else {
algo->Update();
}
return vtkDataSet::SafeDownCast(algo->GetOutputDataObject(0));
}
@@ -251,6 +285,45 @@ void FemPostFilter::setTransformLocation(TransformLocation loc)
m_transform_location = loc;
}
PyObject* FemPostFilter::getPyObject()
{
if (PythonObject.is(Py::_None())) {
// ref counter is set to 1
PythonObject = Py::Object(new FemPostFilterPy(this), true);
}
return Py::new_reference_to(PythonObject);
}
// Python Filter feature ---------------------------------------------------------
namespace App
{
/// @cond DOXERR
PROPERTY_SOURCE_TEMPLATE(Fem::PostFilterPython, Fem::FemPostFilter)
template<>
const char* Fem::PostFilterPython::getViewProviderName(void) const
{
return "FemGui::ViewProviderPostFilterPython";
}
template<>
PyObject* Fem::PostFilterPython::getPyObject()
{
if (PythonObject.is(Py::_None())) {
// ref counter is set to 1
PythonObject = Py::Object(new App::FeaturePythonPyT<FemPostFilterPy>(this), true);
}
return Py::new_reference_to(PythonObject);
}
/// @endcond
// explicit template instantiation
template class FemExport FeaturePythonT<Fem::FemPostFilter>;
} // namespace App
// ***************************************************************************
// in the following, the different filters sorted alphabetically
// ***************************************************************************

View File

@@ -40,6 +40,7 @@
#include <App/PropertyUnits.h>
#include <App/DocumentObjectExtension.h>
#include <App/FeaturePython.h>
#include "FemPostObject.h"
@@ -53,6 +54,8 @@ enum class TransformLocation : size_t
output
};
class FemPostFilterPy;
class FemExport FemPostFilter: public Fem::FemPostObject
{
PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostFilter);
@@ -69,12 +72,16 @@ protected:
std::vector<vtkSmartPointer<vtkAlgorithm>> algorithmStorage;
};
// pipeline handling
void addFilterPipeline(const FilterPipeline& p, std::string name);
void setActiveFilterPipeline(std::string name);
FilterPipeline& getFilterPipeline(std::string name);
void setActiveFilterPipeline(std::string name);
// Transformation handling
void setTransformLocation(TransformLocation loc);
friend class FemPostFilterPy;
public:
/// Constructor
FemPostFilter();
@@ -88,16 +95,21 @@ public:
vtkSmartPointer<vtkAlgorithm> getFilterInput();
vtkSmartPointer<vtkAlgorithm> getFilterOutput();
PyObject* getPyObject() override;
private:
// handling of multiple pipelines which can be the filter
std::map<std::string, FilterPipeline> m_pipelines;
std::string m_activePipeline;
bool m_use_transform = false;
bool m_running_setup = false;
TransformLocation m_transform_location = TransformLocation::output;
void pipelineChanged(); // inform parents that the pipeline changed
};
using PostFilterPython = App::FeaturePythonT<FemPostFilter>;
class FemExport FemPostSmoothFilterExtension: public App::DocumentObjectExtension
{
EXTENSION_PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostSmoothFilterExtension);

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<GenerateModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="generateMetaModel_Module.xsd">
<PythonExport
Father="FemPostObjectPy"
Name="FemPostFilterPy"
Twin="FemPostFilter"
TwinPointer="FemPostFilter"
Include="Mod/Fem/App/FemPostFilter.h"
Namespace="Fem"
FatherInclude="Mod/Fem/App/FemPostObjectPy.h"
FatherNamespace="Fem">
<Documentation>
<Author Licence="LGPL" Name="Stefan Tröger" EMail="stefantroeger@gmx.net" />
<UserDocu>The FemPostFilter class.</UserDocu>
</Documentation>
<Methode Name="addFilterPipeline">
<Documentation>
<UserDocu>Registers a new vtk filter pipeline for data processing. Arguments are (name, source algorithm, target algorithm).</UserDocu>
</Documentation>
</Methode>
<Methode Name="setActiveFilterPipeline">
<Documentation>
<UserDocu>Sets the filter pipeline that shall be used for data processing. Argument is the name of the filter pipeline to activate.</UserDocu>
</Documentation>
</Methode>
<Methode Name="getParentPostGroup">
<Documentation>
<UserDocu>Returns the postprocessing group the filter is in (e.g. a pipeline or branch object). None is returned if not in any.</UserDocu>
</Documentation>
</Methode>
<Methode Name="getInputData">
<Documentation>
<UserDocu>
Returns the dataset available at the 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.
</UserDocu>
</Documentation>
</Methode>
<Methode Name="getInputVectorFields">
<Documentation>
<UserDocu>
Returns the names of all vector 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.
</UserDocu>
</Documentation>
</Methode>
<Methode Name="getInputScalarFields">
<Documentation>
<UserDocu>
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.
</UserDocu>
</Documentation>
</Methode>"
</PythonExport>
</GenerateModel>

View File

@@ -0,0 +1,194 @@
/***************************************************************************
* Copyright (c) 2017 Werner Mayer <wmayer[at]users.sourceforge.net> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include "PreCompiled.h"
#ifndef _PreComp_
#include <Python.h>
#endif
#include <Base/FileInfo.h>
#include <Base/UnitPy.h>
// clang-format off
#include "FemPostGroupExtension.h"
#include "FemPostFilter.h"
#include "FemPostFilterPy.h"
#include "FemPostFilterPy.cpp"
// clang-format on
#ifdef FC_USE_VTK_PYTHON
#include <vtkUnstructuredGrid.h>
#include <vtkPythonUtil.h>
#endif // BUILD_FEM_VTK
using namespace Fem;
// returns a string which represents the object e.g. when printed in python
std::string FemPostFilterPy::representation() const
{
std::stringstream str;
str << "<FemPostFilter object at " << getFemPostFilterPtr() << ">";
return str.str();
}
PyObject* FemPostFilterPy::addFilterPipeline(PyObject* args)
{
#ifdef FC_USE_VTK_PYTHON
const char* name;
PyObject* source = nullptr;
PyObject* target = nullptr;
if (PyArg_ParseTuple(args, "sOO", &name, &source, &target)) {
// extract the algorithms
vtkObjectBase* obj = vtkPythonUtil::GetPointerFromObject(source, "vtkAlgorithm");
if (!obj) {
// error marker is set by PythonUtil
return nullptr;
}
auto source_algo = static_cast<vtkAlgorithm*>(obj);
obj = vtkPythonUtil::GetPointerFromObject(target, "vtkAlgorithm");
if (!obj) {
// error marker is set by PythonUtil
return nullptr;
}
auto target_algo = static_cast<vtkAlgorithm*>(obj);
// add the pipeline
FemPostFilter::FilterPipeline pipe;
pipe.source = source_algo;
pipe.target = target_algo;
getFemPostFilterPtr()->addFilterPipeline(pipe, name);
}
Py_Return;
#else
PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
Py_Return;
#endif
}
PyObject* FemPostFilterPy::setActiveFilterPipeline(PyObject* args)
{
const char* name;
if (PyArg_ParseTuple(args, "s", &name)) {
getFemPostFilterPtr()->setActiveFilterPipeline(std::string(name));
}
Py_Return;
}
PyObject* FemPostFilterPy::getParentPostGroup(PyObject* args)
{
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
auto group = Fem::FemPostGroupExtension::getGroupOfObject(getFemPostFilterPtr());
if (group) {
return group->getPyObject();
}
return Py_None;
}
PyObject* FemPostFilterPy::getInputData(PyObject* args)
{
#ifdef FC_USE_VTK_PYTHON
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
// make a copy of the dataset
auto dataset = getFemPostFilterPtr()->getInputData();
vtkDataSet* copy;
switch (dataset->GetDataObjectType()) {
case VTK_UNSTRUCTURED_GRID:
copy = vtkUnstructuredGrid::New();
break;
default:
PyErr_SetString(PyExc_TypeError,
"cannot return datatype object; not unstructured grid");
Py_Return;
}
// return the python wrapper
copy->DeepCopy(dataset);
PyObject* py_dataset = vtkPythonUtil::GetObjectFromPointer(copy);
return Py::new_reference_to(py_dataset);
#else
PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
Py_Return;
#endif
}
PyObject* FemPostFilterPy::getInputVectorFields(PyObject* args)
{
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
std::vector<std::string> vector_fields = getFemPostFilterPtr()->getInputVectorFields();
// convert to python list of strings
Py::List list;
for (std::string& field : vector_fields) {
list.append(Py::String(field));
}
return Py::new_reference_to(list);
}
PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args)
{
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
std::vector<std::string> scalar_fields = getFemPostFilterPtr()->getInputScalarFields();
// convert to python list of strings
Py::List list;
for (std::string& field : scalar_fields) {
list.append(Py::String(field));
}
return Py::new_reference_to(list);
}
PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
}
int FemPostFilterPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
{
return 0;
}

View File

@@ -42,6 +42,12 @@
#include <vtkXMLUnstructuredGridReader.h>
#endif
#ifdef FC_USE_VTK_PYTHON
#include <vtkPythonUtil.h>
#else
#include <Base/PyObjectBase.h>
#endif
#include <App/Application.h>
#include <App/DocumentObject.h>
#include <Base/Console.h>
@@ -162,12 +168,39 @@ int PropertyPostDataObject::getDataType()
PyObject* PropertyPostDataObject::getPyObject()
{
// TODO: fetch the vtk python object from the data set and return it
return Py::new_reference_to(Py::None());
#ifdef FC_USE_VTK_PYTHON
// create a copy first
auto copy = static_cast<PropertyPostDataObject*>(Copy());
// get the data python wrapper
PyObject* py_dataset = vtkPythonUtil::GetObjectFromPointer(copy->getValue());
auto result = Py::new_reference_to(py_dataset);
delete copy;
return result;
#else
PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
Py_Return;
#endif
}
void PropertyPostDataObject::setPyObject(PyObject* /*value*/)
{}
void PropertyPostDataObject::setPyObject(PyObject* value)
{
#ifdef FC_USE_VTK_PYTHON
vtkObjectBase* obj = vtkPythonUtil::GetPointerFromObject(value, "vtkDataObject");
if (!obj) {
throw Base::TypeError("Can only set vtkDataObject");
}
auto dobj = static_cast<vtkDataObject*>(obj);
createDataObjectByExternalType(dobj);
aboutToSetValue();
m_dataObject->DeepCopy(dobj);
hasSetValue();
#else
throw Base::NotImplementedError();
#endif
}
App::Property* PropertyPostDataObject::Copy() const
{

View File

@@ -1,6 +1,12 @@
if(BUILD_FEM_VTK)
add_definitions(-DFC_USE_VTK)
# we may use VTK but do not have the python wrappers available
if(BUILD_FEM_VTK_PYTHON)
add_definitions(-DFC_USE_VTK_PYTHON)
endif(BUILD_FEM_VTK_PYTHON)
endif(BUILD_FEM_VTK)
@@ -207,6 +213,13 @@ SET(FemObjects_SRCS
femobjects/solver_ccxtools.py
)
if(BUILD_FEM_VTK_PYTHON)
SET(FemObjects_SRCS
${FemObjects_SRCS}
femobjects/post_glyphfilter.py
)
endif(BUILD_FEM_VTK_PYTHON)
SET(FemResult_SRCS
femresult/__init__.py
femresult/resulttools.py
@@ -609,6 +622,13 @@ SET(FemGuiTaskPanels_SRCS
femtaskpanels/task_solver_ccxtools.py
)
if(BUILD_FEM_VTK_PYTHON)
SET(FemGuiTaskPanels_SRCS
${FemGuiTaskPanels_SRCS}
femtaskpanels/task_post_glyphfilter.py
)
endif(BUILD_FEM_VTK_PYTHON)
SET(FemGuiTests_SRCS
femtest/gui/__init__.py
femtest/gui/test_open.py
@@ -619,6 +639,7 @@ SET(FemGuiUtils_SRCS
femguiutils/disambiguate_solid_selection.py
femguiutils/migrate_gui.py
femguiutils/selection_widgets.py
femguiutils/vtk_module_handling.py
)
SET(FemGuiViewProvider_SRCS
@@ -659,6 +680,13 @@ SET(FemGuiViewProvider_SRCS
femviewprovider/view_solver_ccxtools.py
)
if(BUILD_FEM_VTK_PYTHON)
SET(FemGuiViewProvider_SRCS
${FemGuiViewProvider_SRCS}
femviewprovider/view_post_glyphfilter.py
)
endif(BUILD_FEM_VTK_PYTHON)
SET(FemGuiPreferencePages_SRCS
fempreferencepages/__init__.py
fempreferencepages/dlg_settings_netgen.py

View File

@@ -161,6 +161,8 @@ PyMOD_INIT_FUNC(FemGui)
#ifdef FC_USE_VTK
FemGui::ViewProviderFemPostObject ::init();
FemGui::ViewProviderFemPostPipeline ::init();
FemGui::ViewProviderFemPostFilterPythonBase ::init();
FemGui::ViewProviderPostFilterPython ::init();
FemGui::ViewProviderFemPostBranchFilter ::init();
FemGui::ViewProviderFemPostCalculator ::init();
FemGui::ViewProviderFemPostClip ::init();

View File

@@ -36,6 +36,7 @@ set(FemGui_LIBS
generate_from_xml(ViewProviderFemConstraintPy)
generate_from_xml(ViewProviderFemMeshPy)
generate_from_xml(ViewProviderFemPostPipelinePy)
generate_from_xml(ViewProviderFemPostFilterPy)
SET(Python_SRCS
ViewProviderFemConstraintPy.xml
@@ -44,6 +45,8 @@ SET(Python_SRCS
ViewProviderFemMeshPyImp.cpp
ViewProviderFemPostPipelinePy.xml
ViewProviderFemPostPipelinePyImp.cpp
ViewProviderFemPostFilterPy.xml
ViewProviderFemPostFilterPyImp.cpp
)
SOURCE_GROUP("Python" FILES ${Python_SRCS})
@@ -430,6 +433,7 @@ SET(FemGuiPythonUI_SRCS
Resources/ui/ResultShow.ui
Resources/ui/SolverCalculiX.ui
Resources/ui/SolverCcxTools.ui
Resources/ui/TaskPostGlyph.ui
)
ADD_CUSTOM_TARGET(FemPythonUi ALL

View File

@@ -82,6 +82,7 @@
<file>icons/FEM_PostFilterDataAtPoint.svg</file>
<file>icons/FEM_PostFilterLinearizedStresses.svg</file>
<file>icons/FEM_PostFilterWarp.svg</file>
<file>icons/FEM_PostFilterGlyph.svg</file>
<file>icons/FEM_PostFrames.svg</file>
<file>icons/FEM_PostBranchFilter.svg</file>
<file>icons/FEM_PostPipelineFromResult.svg</file>
@@ -150,5 +151,6 @@
<file>ui/ResultShow.ui</file>
<file>ui/SolverCalculiX.ui</file>
<file>ui/SolverCcxTools.ui</file>
<file>ui/TaskPostGlyph.ui</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
id="svg2"
version="1.1"
sodipodi:docname="FEM_PostFilterGlyph.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="8.6344367"
inkscape:cx="33.238995"
inkscape:cy="26.232169"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs4">
<linearGradient
id="linearGradient3802">
<stop
style="stop-color:#4e9a06;stop-opacity:1"
offset="0"
id="stop3804" />
<stop
style="stop-color:#73d216;stop-opacity:1"
offset="1"
id="stop3806" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient3802"
id="linearGradient3808"
x1="49"
y1="58"
x2="47"
y2="42"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-77.65959,-38.104787)" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>[Alexander Gryson]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>fem-warp</dc:title>
<dc:date>2017-03-11</dc:date>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path5"
style="fill:#5bae0c;stroke:#172a04;stroke-width:0.783709;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:9.6"
d="M 28.643627,7.4834885 14.376899,11.359125 26.588343,21.800077 Z M 23.279624,18.970903 17.46429,13.999367 0.84260476,32.570907 6.6625341,37.537309 Z"
sodipodi:nodetypes="ccccccccc" />
<path
id="path6"
style="fill:#5bae0c;stroke:#172a04;stroke-width:0.709324;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:9.6"
d="m 54.160189,35.642295 -12.759634,3.549837 10.921464,9.563249 z m -4.797366,10.521743 -5.20102,-4.553612 -14.865822,17.01035 5.20513,4.548909 z"
sodipodi:nodetypes="ccccccccc" />
<path
id="path6-6"
style="fill:#5bae0c;stroke:#172a04;stroke-width:1.43581;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:9.6"
d="M 59.446346,1.4589408 33.618389,8.6444946 55.725536,28.002353 Z M 49.735554,22.756974 39.207689,13.539587 9.1164014,47.971811 19.652586,57.179679 Z"
sodipodi:nodetypes="ccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,355 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskPostGlyph</class>
<widget class="QWidget" name="TaskPostGlyph">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>440</width>
<height>428</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Glyph settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>The form of the glyph</string>
</property>
<property name="text">
<string>Form</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="FormComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>The form of the glyph</string>
</property>
<item>
<property name="text">
<string>Arrow</string>
</property>
</item>
<item>
<property name="text">
<string>Cube</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_10">
<property name="toolTip">
<string>Which vector field is used to orient the glyphs</string>
</property>
<property name="text">
<string>Orientation</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="OrientationComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Which vector field is used to orient the glyphs</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="Scale">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Sca&amp;le</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="toolTip">
<string>If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components</string>
</property>
<property name="text">
<string>Data</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="ScaleComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
<string>A constant multiplier the glyphs are scaled with</string>
</property>
<property name="text">
<string>Factor</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QDoubleSpinBox" name="ScaleFactorBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>A constant multiplier the glyphs are scaled with</string>
</property>
<property name="maximum">
<double>99999999999.000000000000000</double>
</property>
<property name="singleStep">
<double>0.000000000000000</double>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="ScaleSlider">
<property name="toolTip">
<string>Changes the scale factor by +/- 50% of the set scale factor</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>5</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="VectorModeComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Which data field is used to scale the glyphs</string>
</property>
<item>
<property name="text">
<string>Not a vector</string>
</property>
</item>
<item>
<property name="text">
<string>By magnitude</string>
</property>
</item>
<item>
<property name="text">
<string>By components</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="Mask">
<property name="title">
<string>Vertex Mas&amp;king</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="toolTip">
<string>Which vertices are used as glyph locations</string>
</property>
<property name="text">
<string>Mode</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="MaxBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Defines the maximal number of vertices used for &quot;Uniform Sampling&quot; masking mode</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>999999999</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="StrideLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Define the stride for &quot;Every Nth&quot; masking mode</string>
</property>
<property name="text">
<string>Stride</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="MaxLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Defines the maximal number of vertices used for &quot;Uniform Sampling&quot; masking mode</string>
</property>
<property name="text">
<string>Max </string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="StrideBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Define the stride for &quot;Every Nth&quot; masking mode</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>999999999</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="MaskModeComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Which vertices are used as glyph locations</string>
</property>
<item>
<property name="text">
<string>All</string>
</property>
</item>
<item>
<property name="text">
<string>Every Nth</string>
</property>
</item>
<item>
<property name="text">
<string>Uniform Sampling</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -203,29 +203,33 @@ void DataAlongLineMarker::customEvent(QEvent*)
// ***************************************************************************
// main task dialog
TaskPostBox::TaskPostBox(Gui::ViewProviderDocumentObject* view,
TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view,
const QPixmap& icon,
const QString& title,
QWidget* parent)
: TaskBox(icon, title, true, parent)
: QWidget(parent)
, m_object(view->getObject())
, m_view(view)
{}
{
setWindowTitle(title);
setWindowIcon(icon);
m_icon = icon;
}
TaskPostBox::~TaskPostBox() = default;
TaskPostWidget::~TaskPostWidget() = default;
bool TaskPostBox::autoApply()
bool TaskPostWidget::autoApply()
{
return FemSettings().getPostAutoRecompute();
}
App::Document* TaskPostBox::getDocument() const
App::Document* TaskPostWidget::getDocument() const
{
App::DocumentObject* obj = getObject();
return (obj ? obj->getDocument() : nullptr);
}
void TaskPostBox::recompute()
void TaskPostWidget::recompute()
{
if (autoApply()) {
App::Document* doc = getDocument();
@@ -235,7 +239,7 @@ void TaskPostBox::recompute()
}
}
void TaskPostBox::updateEnumerationList(App::PropertyEnumeration& prop, QComboBox* box)
void TaskPostWidget::updateEnumerationList(App::PropertyEnumeration& prop, QComboBox* box)
{
QStringList list;
std::vector<std::string> vec = prop.getEnumVector();
@@ -266,10 +270,20 @@ TaskDlgPost::~TaskDlgPost() = default;
QDialogButtonBox::StandardButtons TaskDlgPost::getStandardButtons() const
{
// check if we only have gui task boxes
bool guionly = true;
for (auto it : m_boxes) {
guionly = guionly && it->isGuiTaskOnly();
for (auto& widget : Content) {
if (auto task_box = dynamic_cast<Gui::TaskView::TaskBox*>(widget)) {
// get the task widget and check if it is a post widget
auto widget = task_box->groupLayout()->itemAt(0)->widget();
if (auto post_widget = dynamic_cast<TaskPostWidget*>(widget)) {
guionly = guionly && post_widget->isGuiTaskOnly();
}
else {
// unknown panel, we can only assume
guionly = false;
}
}
}
if (!guionly) {
@@ -285,7 +299,7 @@ void TaskDlgPost::connectSlots()
// Connect emitAddedFunction() with slotAddedFunction()
QObject* sender = nullptr;
int indexSignal = 0;
for (const auto dlg : m_boxes) {
for (const auto dlg : Content) {
indexSignal = dlg->metaObject()->indexOfSignal(
QMetaObject::normalizedSignature("emitAddedFunction()"));
if (indexSignal >= 0) {
@@ -295,7 +309,7 @@ void TaskDlgPost::connectSlots()
}
if (sender) {
for (const auto dlg : m_boxes) {
for (const auto dlg : Content) {
int indexSlot = dlg->metaObject()->indexOfSlot(
QMetaObject::normalizedSignature("slotAddedFunction()"));
if (indexSlot >= 0) {
@@ -308,12 +322,6 @@ void TaskDlgPost::connectSlots()
}
}
void TaskDlgPost::appendBox(TaskPostBox* box)
{
m_boxes.push_back(box);
Content.push_back(box);
}
void TaskDlgPost::open()
{
// only open a new command if none is pending (e.g. if the object was newly created)
@@ -326,8 +334,14 @@ void TaskDlgPost::open()
void TaskDlgPost::clicked(int button)
{
if (button == QDialogButtonBox::Apply) {
for (auto box : m_boxes) {
box->apply();
for (auto& widget : Content) {
if (auto task_box = dynamic_cast<Gui::TaskView::TaskBox*>(widget)) {
// get the task widget and check if it is a post widget
auto widget = task_box->groupLayout()->itemAt(0)->widget();
if (auto post_widget = dynamic_cast<TaskPostWidget*>(widget)) {
post_widget->apply();
}
}
}
recompute();
}
@@ -336,8 +350,14 @@ void TaskDlgPost::clicked(int button)
bool TaskDlgPost::accept()
{
try {
for (auto& box : m_boxes) {
box->applyPythonCode();
for (auto& widget : Content) {
if (auto task_box = dynamic_cast<Gui::TaskView::TaskBox*>(widget)) {
// get the task widget and check if it is a post widget
auto widget = task_box->groupLayout()->itemAt(0)->widget();
if (auto post_widget = dynamic_cast<TaskPostWidget*>(widget)) {
post_widget->applyPythonCode();
}
}
}
}
catch (const Base::Exception& e) {
@@ -377,19 +397,15 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box)
// ***************************************************************************
// box to set the coloring
TaskPostDisplay::TaskPostDisplay(ViewProviderFemPostObject* view, QWidget* parent)
: TaskPostBox(view,
Gui::BitmapFactory().pixmap("FEM_ResultShow"),
tr("Result display options"),
parent)
: TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_ResultShow"), QString(), parent)
, ui(new Ui_TaskPostDisplay)
{
// we need a separate container widget to add all controls to
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(
tr("Result display options")); // set title here as setupUi overrides the constructor title
setupConnections();
this->groupLayout()->addWidget(proxy);
// update all fields
updateEnumerationList(getTypedView<ViewProviderFemPostObject>()->DisplayMode,
ui->Representation);
@@ -463,7 +479,7 @@ void TaskPostDisplay::applyPythonCode()
// ***************************************************************************
// functions
TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("fem-post-geo-plane"),
tr("Implicit function"),
parent)
@@ -472,7 +488,10 @@ TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* p
FunctionWidget* w = getTypedView<ViewProviderFemPostFunction>()->createControlWidget();
w->setParent(this);
w->setViewProvider(getTypedView<ViewProviderFemPostFunction>());
this->groupLayout()->addWidget(w);
QVBoxLayout* layout = new QVBoxLayout;
layout->addWidget(w);
setLayout(layout);
}
TaskPostFunction::~TaskPostFunction() = default;
@@ -486,13 +505,12 @@ void TaskPostFunction::applyPythonCode()
// ***************************************************************************
// Frames
TaskPostFrames::TaskPostFrames(ViewProviderFemPostObject* view, QWidget* parent)
: TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFrames"), tr("Result Frames"), parent)
: TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFrames"), QString(), parent)
, ui(new Ui_TaskPostFrames)
{
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
this->groupLayout()->addWidget(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Result Frames"));
setupConnections();
// populate the data
@@ -548,16 +566,12 @@ void TaskPostFrames::applyPythonCode()
// ***************************************************************************
// Branch
TaskPostBranch::TaskPostBranch(ViewProviderFemPostBranchFilter* view, QWidget* parent)
: TaskPostBox(view,
Gui::BitmapFactory().pixmap("FEM_PostBranchFilter"),
tr("Branch behaviour"),
parent)
: TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostBranchFilter"), QString(), parent)
, ui(new Ui_TaskPostBranch)
{
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
this->groupLayout()->addWidget(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Branch behaviour"));
setupConnections();
// populate the data
@@ -603,19 +617,17 @@ void TaskPostBranch::applyPythonCode()
// data along line filter
TaskPostDataAlongLine::TaskPostDataAlongLine(ViewProviderFemPostDataAlongLine* view,
QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterDataAlongLine"),
tr("Data along a line options"),
QString(),
parent)
, ui(new Ui_TaskPostDataAlongLine)
, marker(nullptr)
{
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Data along a line options"));
setupConnectionsStep1();
this->groupLayout()->addWidget(proxy);
QSize size = ui->point1X->sizeForText(QStringLiteral("000000000000"));
ui->point1X->setMinimumWidth(size.width());
@@ -1025,21 +1037,19 @@ plt.show()\n";
// ***************************************************************************
// data at point filter
TaskPostDataAtPoint::TaskPostDataAtPoint(ViewProviderFemPostDataAtPoint* view, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterDataAtPoint"),
tr("Data at point options"),
QString(),
parent)
, viewer(nullptr)
, connSelectPoint(QMetaObject::Connection())
, ui(new Ui_TaskPostDataAtPoint)
{
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Data at point options"));
setupConnections();
this->groupLayout()->addWidget(proxy);
QSize size = ui->centerX->sizeForText(QStringLiteral("000000000000"));
ui->centerX->setMinimumWidth(size.width());
ui->centerY->setMinimumWidth(size.width());
@@ -1382,9 +1392,9 @@ std::string TaskPostDataAtPoint::toString(double val) const
TaskPostClip::TaskPostClip(ViewProviderFemPostClip* view,
App::PropertyLink* function,
QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterClipRegion"),
tr("Clip region, choose implicit function"),
QString(),
parent)
, ui(new Ui_TaskPostClip)
{
@@ -1393,11 +1403,10 @@ TaskPostClip::TaskPostClip(ViewProviderFemPostClip* view,
fwidget = nullptr;
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Clip region, choose implicit function"));
setupConnections();
this->groupLayout()->addWidget(proxy);
// the layout for the container widget
QVBoxLayout* layout = new QVBoxLayout();
@@ -1542,17 +1551,13 @@ void TaskPostClip::onInsideOutToggled(bool val)
// ***************************************************************************
// contours filter
TaskPostContours::TaskPostContours(ViewProviderFemPostContours* view, QWidget* parent)
: TaskPostBox(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterContours"),
tr("Contours filter options"),
parent)
: TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterContours"), QString(), parent)
, ui(new Ui_TaskPostContours)
{
// load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Contours filter options"));
QMetaObject::connectSlotsByName(this);
this->groupLayout()->addWidget(proxy);
auto obj = getObject<Fem::FemPostContoursFilter>();
@@ -1697,9 +1702,9 @@ void TaskPostContours::onRelaxationChanged(double value)
// ***************************************************************************
// cut filter
TaskPostCut::TaskPostCut(ViewProviderFemPostCut* view, App::PropertyLink* function, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterCutFunction"),
tr("Function cut, choose implicit function"),
QString(),
parent)
, ui(new Ui_TaskPostCut)
{
@@ -1708,11 +1713,10 @@ TaskPostCut::TaskPostCut(ViewProviderFemPostCut* view, App::PropertyLink* functi
fwidget = nullptr;
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Function cut, choose implicit function"));
setupConnections();
this->groupLayout()->addWidget(proxy);
// the layout for the container widget
QVBoxLayout* layout = new QVBoxLayout();
@@ -1836,17 +1840,16 @@ void TaskPostCut::onFunctionBoxCurrentIndexChanged(int idx)
// ***************************************************************************
// scalar clip filter
TaskPostScalarClip::TaskPostScalarClip(ViewProviderFemPostScalarClip* view, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterClipScalar"),
tr("Scalar clip options"),
QString(),
parent)
, ui(new Ui_TaskPostScalarClip)
{
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Scalar clip options"));
setupConnections();
this->groupLayout()->addWidget(proxy);
// load the default values
updateEnumerationList(getTypedObject<Fem::FemPostScalarClipFilter>()->Scalars, ui->Scalar);
@@ -1961,17 +1964,13 @@ void TaskPostScalarClip::onInsideOutToggled(bool val)
// ***************************************************************************
// warp vector filter
TaskPostWarpVector::TaskPostWarpVector(ViewProviderFemPostWarpVector* view, QWidget* parent)
: TaskPostBox(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"),
tr("Warp options"),
parent)
: TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"), QString(), parent)
, ui(new Ui_TaskPostWarpVector)
{
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
// setup the ui
ui->setupUi(this);
setWindowTitle(tr("Warp options"));
setupConnections();
this->groupLayout()->addWidget(proxy);
// load the default values for warp display
updateEnumerationList(getTypedObject<Fem::FemPostWarpVectorFilter>()->Vector, ui->Vector);
@@ -2136,17 +2135,15 @@ static const std::vector<std::string> calculatorOperators = {
"log", "pow", "sqrt", "iHat", "jHat", "kHat", "cross", "dot", "mag", "norm"};
TaskPostCalculator::TaskPostCalculator(ViewProviderFemPostCalculator* view, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterCalculator"),
tr("Calculator options"),
parent)
, ui(new Ui_TaskPostCalculator)
{
// we load the views widget
proxy = new QWidget(this);
ui->setupUi(proxy);
ui->setupUi(this);
setupConnections();
this->groupLayout()->addWidget(proxy);
// load the default values
auto obj = getObject<Fem::FemPostCalculatorFilter>();

View File

@@ -131,18 +131,22 @@ protected:
// ***************************************************************************
// main task dialog
class TaskPostBox: public Gui::TaskView::TaskBox
class TaskPostWidget: public QWidget
{
Q_OBJECT
// Q_OBJECT
public:
TaskPostBox(Gui::ViewProviderDocumentObject* view,
TaskPostWidget(Gui::ViewProviderDocumentObject* view,
const QPixmap& icon,
const QString& title,
const QString& title = QString(),
QWidget* parent = nullptr);
~TaskPostBox() override;
~TaskPostWidget() override;
virtual void applyPythonCode() {};
QPixmap getIcon()
{
return m_icon;
}
virtual bool isGuiTaskOnly()
{
return false;
@@ -184,6 +188,7 @@ protected:
static void updateEnumerationList(App::PropertyEnumeration&, QComboBox* box);
private:
QPixmap m_icon;
App::DocumentObjectWeakPtrT m_object;
Gui::ViewProviderWeakPtrT m_view;
};
@@ -200,7 +205,6 @@ public:
~TaskDlgPost() override;
void connectSlots();
void appendBox(TaskPostBox* box);
Gui::ViewProviderDocumentObject* getView() const
{
return *m_view;
@@ -230,7 +234,6 @@ protected:
protected:
Gui::ViewProviderWeakPtrT m_view;
std::vector<TaskPostBox*> m_boxes;
};
@@ -238,7 +241,7 @@ protected:
// box to set the coloring
class ViewProviderFemPostObject;
class TaskPostDisplay: public TaskPostBox
class TaskPostDisplay: public TaskPostWidget
{
Q_OBJECT
@@ -261,7 +264,6 @@ private:
void slotAddedFunction();
private:
QWidget* proxy;
std::unique_ptr<Ui_TaskPostDisplay> ui;
};
@@ -270,7 +272,7 @@ private:
// functions
class ViewProviderFemPostFunction;
class TaskPostFunction: public TaskPostBox
class TaskPostFunction: public TaskPostWidget
{
Q_OBJECT
@@ -283,7 +285,7 @@ public:
// ***************************************************************************
// frames
class TaskPostFrames: public TaskPostBox
class TaskPostFrames: public TaskPostWidget
{
Q_OBJECT
@@ -297,7 +299,6 @@ private:
void setupConnections();
void onSelectionChanged();
QWidget* proxy;
std::unique_ptr<Ui_TaskPostFrames> ui;
};
@@ -311,7 +312,7 @@ private:
// branch
class ViewProviderFemPostBranchFilter;
class TaskPostBranch: public TaskPostBox
class TaskPostBranch: public TaskPostWidget
{
Q_OBJECT
@@ -326,7 +327,6 @@ private:
void onModeIndexChanged(int);
void onOutputIndexChanged(int);
QWidget* proxy;
std::unique_ptr<Ui_TaskPostBranch> ui;
};
@@ -334,7 +334,7 @@ private:
// data along line filter
class ViewProviderFemPostDataAlongLine;
class TaskPostDataAlongLine: public TaskPostBox
class TaskPostDataAlongLine: public TaskPostWidget
{
Q_OBJECT
@@ -362,7 +362,6 @@ private:
private:
std::string Plot();
std::string ObjectVisible();
QWidget* proxy;
std::unique_ptr<Ui_TaskPostDataAlongLine> ui;
DataAlongLineMarker* marker;
};
@@ -372,7 +371,7 @@ private:
// data at point filter
class ViewProviderFemPostDataAtPoint;
class TaskPostDataAtPoint: public TaskPostBox
class TaskPostDataAtPoint: public TaskPostWidget
{
Q_OBJECT
@@ -400,7 +399,6 @@ private:
std::string toString(double val) const;
void showValue(double value, const char* unit);
std::string objectVisible(bool visible) const;
QWidget* proxy;
std::unique_ptr<Ui_TaskPostDataAtPoint> ui;
};
@@ -409,7 +407,7 @@ private:
// clip filter
class ViewProviderFemPostClip;
class TaskPostClip: public TaskPostBox
class TaskPostClip: public TaskPostWidget
{
Q_OBJECT
@@ -435,7 +433,6 @@ private:
void collectImplicitFunctions();
// App::PropertyLink* m_functionProperty;
QWidget* proxy;
std::unique_ptr<Ui_TaskPostClip> ui;
FunctionWidget* fwidget;
};
@@ -445,7 +442,7 @@ private:
// contours filter
class ViewProviderFemPostContours;
class TaskPostContours: public TaskPostBox
class TaskPostContours: public TaskPostWidget
{
Q_OBJECT
@@ -464,7 +461,6 @@ private:
void onRelaxationChanged(double v);
private:
QWidget* proxy;
std::unique_ptr<Ui_TaskPostContours> ui;
bool blockVectorUpdate = false;
void updateFields();
@@ -475,7 +471,7 @@ private:
// cut filter
class ViewProviderFemPostCut;
class TaskPostCut: public TaskPostBox
class TaskPostCut: public TaskPostWidget
{
Q_OBJECT
@@ -499,7 +495,6 @@ private:
void collectImplicitFunctions();
// App::PropertyLink* m_functionProperty;
QWidget* proxy;
std::unique_ptr<Ui_TaskPostCut> ui;
FunctionWidget* fwidget;
};
@@ -509,7 +504,7 @@ private:
// scalar clip filter
class ViewProviderFemPostScalarClip;
class TaskPostScalarClip: public TaskPostBox
class TaskPostScalarClip: public TaskPostWidget
{
Q_OBJECT
@@ -527,7 +522,6 @@ private:
void onInsideOutToggled(bool val);
private:
QWidget* proxy;
std::unique_ptr<Ui_TaskPostScalarClip> ui;
};
@@ -536,7 +530,7 @@ private:
// warp vector filter
class ViewProviderFemPostWarpVector;
class TaskPostWarpVector: public TaskPostBox
class TaskPostWarpVector: public TaskPostWidget
{
Q_OBJECT
@@ -555,7 +549,6 @@ private:
void onVectorCurrentIndexChanged(int idx);
private:
QWidget* proxy;
std::unique_ptr<Ui_TaskPostWarpVector> ui;
};
@@ -564,7 +557,7 @@ private:
// calculator filter
class ViewProviderFemPostCalculator;
class TaskPostCalculator: public TaskPostBox
class TaskPostCalculator: public TaskPostWidget
{
Q_OBJECT

View File

@@ -25,6 +25,7 @@
#include "TaskPostBoxes.h"
#include "ViewProviderFemPostBranchFilter.h"
#include <Mod/Fem/App/FemPostGroupExtension.h>
#include <Gui/BitmapFactory.h>
using namespace FemGui;
@@ -46,7 +47,8 @@ ViewProviderFemPostBranchFilter::~ViewProviderFemPostBranchFilter()
void ViewProviderFemPostBranchFilter::setupTaskDialog(TaskDlgPost* dlg)
{
// add the branch ui
dlg->appendBox(new TaskPostBranch(this));
auto panel = new TaskPostBranch(this);
dlg->addTaskBox(panel->windowIcon().pixmap(32), panel);
// add the display options
FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg);

View File

@@ -30,10 +30,43 @@
#include "TaskPostBoxes.h"
#include "ViewProviderFemPostFilter.h"
#include "ViewProviderFemPostFilterPy.h"
using namespace FemGui;
PROPERTY_SOURCE(FemGui::ViewProviderFemPostFilterPythonBase, FemGui::ViewProviderFemPostObject)
ViewProviderFemPostFilterPythonBase::ViewProviderFemPostFilterPythonBase()
{}
ViewProviderFemPostFilterPythonBase::~ViewProviderFemPostFilterPythonBase() = default;
std::vector<std::string> ViewProviderFemPostFilterPythonBase::getDisplayModes() const
{
return std::vector<std::string>();
}
namespace Gui
{
PROPERTY_SOURCE_TEMPLATE(FemGui::ViewProviderPostFilterPython,
FemGui::ViewProviderFemPostFilterPythonBase)
template<>
PyObject* FemGui::ViewProviderPostFilterPython::getPyObject()
{
if (!pyViewObject) {
pyViewObject = new ViewProviderFemPostFilterPy(this);
}
pyViewObject->IncRef();
return pyViewObject;
}
// explicit template instantiation
template class FemGuiExport ViewProviderFeaturePythonT<FemGui::ViewProviderFemPostFilterPythonBase>;
} // namespace Gui
// ***************************************************************************
// in the following, the different filters sorted alphabetically
// ***************************************************************************
@@ -54,7 +87,8 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg)
{
// add the function box
assert(dlg->getView() == this);
dlg->appendBox(new TaskPostDataAlongLine(this));
auto panel = new TaskPostDataAlongLine(this);
dlg->addTaskBox(panel->getIcon(), panel);
}
@@ -102,7 +136,8 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg)
{
// add the function box
assert(dlg->getView() == this);
dlg->appendBox(new TaskPostDataAtPoint(this));
auto panel = new TaskPostDataAtPoint(this);
dlg->addTaskBox(panel->getIcon(), panel);
}
@@ -123,8 +158,9 @@ void ViewProviderFemPostClip::setupTaskDialog(TaskDlgPost* dlg)
// add the function box
assert(dlg->getView() == this);
dlg->appendBox(
new TaskPostClip(this, &dlg->getView()->getObject<Fem::FemPostClipFilter>()->Function));
auto panel =
new TaskPostClip(this, &dlg->getView()->getObject<Fem::FemPostClipFilter>()->Function);
dlg->addTaskBox(panel->getIcon(), panel);
// add the display options
FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg);
@@ -146,7 +182,8 @@ void ViewProviderFemPostContours::setupTaskDialog(TaskDlgPost* dlg)
{
// the filter-specific task panel
assert(dlg->getView() == this);
dlg->appendBox(new TaskPostContours(this));
auto panel = new TaskPostContours(this);
dlg->addTaskBox(panel->getIcon(), panel);
}
@@ -165,8 +202,9 @@ void ViewProviderFemPostCut::setupTaskDialog(TaskDlgPost* dlg)
{
// add the function box
assert(dlg->getView() == this);
dlg->appendBox(
new TaskPostCut(this, &dlg->getView()->getObject<Fem::FemPostCutFilter>()->Function));
auto panel =
new TaskPostCut(this, &dlg->getView()->getObject<Fem::FemPostCutFilter>()->Function);
dlg->addTaskBox(panel->getIcon(), panel);
// add the display options
FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg);
@@ -188,7 +226,8 @@ void ViewProviderFemPostScalarClip::setupTaskDialog(TaskDlgPost* dlg)
{
// add the function box
assert(dlg->getView() == this);
dlg->appendBox(new TaskPostScalarClip(this));
auto panel = new TaskPostScalarClip(this);
dlg->addTaskBox(panel->getIcon(), panel);
// add the display options
FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg);
@@ -210,7 +249,8 @@ void ViewProviderFemPostWarpVector::setupTaskDialog(TaskDlgPost* dlg)
{
// add the function box
assert(dlg->getView() == this);
dlg->appendBox(new TaskPostWarpVector(this));
auto panel = new TaskPostWarpVector(this);
dlg->addTaskBox(panel->getIcon(), panel);
// add the display options
FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg);
@@ -245,7 +285,8 @@ void ViewProviderFemPostCalculator::setupTaskDialog(TaskDlgPost* dlg)
{
// add the function box
assert(dlg->getView() == this);
dlg->appendBox(new TaskPostCalculator(this));
auto panel = new TaskPostCalculator(this);
dlg->addTaskBox(panel->getIcon(), panel);
// add the display options
FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg);

View File

@@ -23,12 +23,37 @@
#ifndef FEM_VIEWPROVIDERFEMPOSTFILTER_H
#define FEM_VIEWPROVIDERFEMPOSTFILTER_H
#include <Gui/ViewProviderFeaturePython.h>
#include "ViewProviderFemPostObject.h"
namespace FemGui
{
// ***************************************************************************
// Special classes to enable python filter view providers
// ***************************************************************************
// Special class for the python view providers, which need some special behaviour
class FemGuiExport ViewProviderFemPostFilterPythonBase: public ViewProviderFemPostObject
{
PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostFilterPythonBase);
public:
/// constructor.
ViewProviderFemPostFilterPythonBase();
~ViewProviderFemPostFilterPythonBase() override;
// we do not use default display modes but let the python implementation choose
// Python view provider needs to return a sublist of PostObject supporter DisplayModes
std::vector<std::string> getDisplayModes() const override;
};
// Viewprovider for the python filters
using ViewProviderPostFilterPython =
Gui::ViewProviderFeaturePythonT<ViewProviderFemPostFilterPythonBase>;
// ***************************************************************************
// in the following, the different filters sorted alphabetically
// ***************************************************************************

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<GenerateModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="generateMetaModel_Module.xsd">
<PythonExport
Father="ViewProviderDocumentObjectPy"
Name="ViewProviderFemPostFilterPy"
Twin="ViewProviderFemPostObject"
TwinPointer="ViewProviderFemPostObject"
Include="Mod/Fem/Gui/ViewProviderFemPostObject.h"
Namespace="FemGui"
FatherInclude="Gui/ViewProviderDocumentObjectPy.h"
FatherNamespace="Gui"
Constructor="false"
Delete="false">
<Documentation>
<Author Licence="LGPL" Name="Stefan Tröger" EMail="stefantroeger@gmx.net" />
<UserDocu>ViewProviderFemPostPipeline class</UserDocu>
</Documentation>
<Methode Name="createDisplayTaskWidget">
<Documentation>
<UserDocu>Returns the display option task panel for a post processing edit task dialog.</UserDocu>
</Documentation>
</Methode>
</PythonExport>
</GenerateModel>

View File

@@ -0,0 +1,71 @@
/***************************************************************************
* Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
* *
* 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"
// clang-format off
#include <Gui/Control.h>
#include <Gui/PythonWrapper.h>
#include "ViewProviderFemPostFilter.h"
#include "TaskPostBoxes.h"
// inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml)
#include "ViewProviderFemPostFilterPy.h"
#include "ViewProviderFemPostFilterPy.cpp"
#include <Base/PyWrapParseTupleAndKeywords.h>
// clang-format on
using namespace FemGui;
// returns a string which represents the object e.g. when printed in python
std::string ViewProviderFemPostFilterPy::representation() const
{
return {"<ViewProviderFemPostFilter object>"};
}
PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args)
{
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
auto panel = new TaskPostDisplay(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;
}
PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
}
int ViewProviderFemPostFilterPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
{
return 0;
}

View File

@@ -343,7 +343,8 @@ bool ViewProviderFemPostFunction::setEdit(int ModNum)
}
else {
postDlg = new TaskDlgPost(this);
postDlg->appendBox(new TaskPostFunction(this));
auto panel = new TaskPostFunction(this);
postDlg->addTaskBox(panel->windowIcon().pixmap(32), panel);
Gui::Control().showDialog(postDlg);
}

View File

@@ -53,7 +53,6 @@
#endif
#include <App/Document.h>
#include <Base/Console.h>
#include <Gui/Application.h>
#include <Gui/Control.h>
#include <Gui/Document.h>
@@ -187,7 +186,7 @@ ViewProviderFemPostObject::ViewProviderFemPostObject()
LineWidth.setConstraints(&sizeRange);
PointSize.setConstraints(&sizeRange);
sPixmap = "fem-femmesh-from-shape";
sPixmap = "FEM_PostPipelineFromResult";
// create the subnodes which do the visualization work
m_transpType = new SoTransparencyType();
@@ -408,7 +407,9 @@ void ViewProviderFemPostObject::updateVtk()
}
m_currentAlgorithm->Update();
if (!isRestoring()) {
updateProperties();
}
update3D();
}
@@ -931,7 +932,9 @@ void ViewProviderFemPostObject::onChanged(const App::Property* prop)
}
if (prop == &Field && setupPipeline()) {
if (!isRestoring()) {
updateProperties();
}
WriteColorData(ResetColorBarRange);
}
else if (prop == &Component && setupPipeline()) {
@@ -1016,7 +1019,8 @@ bool ViewProviderFemPostObject::setEdit(int ModNum)
void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg)
{
assert(dlg->getView() == this);
dlg->appendBox(new TaskPostDisplay(this));
auto panel = new TaskPostDisplay(this);
dlg->addTaskBox(panel->windowIcon().pixmap(32), panel);
}
void ViewProviderFemPostObject::unsetEdit(int ModNum)

View File

@@ -114,27 +114,14 @@ public:
bool canDelete(App::DocumentObject* obj) const override;
virtual void onSelectionChanged(const Gui::SelectionChanges& sel);
/** @name Selection handling
* This group of methods do the selection handling.
* Here you can define how the selection for your ViewProvider
* works.
*/
//@{
// /// indicates if the ViewProvider use the new Selection model
// virtual bool useNewSelectionModel(void) const {return true;}
// /// return a hit element to the selection path or 0
// virtual std::string getElement(const SoDetail*) const;
// virtual SoDetail* getDetail(const char*) const;
// /// return the highlight lines for a given element or the whole shape
// virtual std::vector<Base::Vector3d> getSelectionShape(const char* Element) const;
// //@}
// setting up task dialogs
virtual void setupTaskDialog(TaskDlgPost* dlg);
protected:
void handleChangedPropertyName(Base::XMLReader& reader,
const char* typeName,
const char* propName) override;
virtual void setupTaskDialog(TaskDlgPost* dlg);
bool setupPipeline();
void updateVtk();
void setRangeOfColorBar(float min, float max);

View File

@@ -221,7 +221,8 @@ void ViewProviderFemPostPipeline::setupTaskDialog(TaskDlgPost* dlg)
// add the function box
assert(dlg->getView() == this);
ViewProviderFemPostObject::setupTaskDialog(dlg);
dlg->appendBox(new TaskPostFrames(this));
auto panel = new TaskPostFrames(this);
dlg->addTaskBox(panel->windowIcon().pixmap(32), panel);
}

View File

@@ -206,6 +206,9 @@ Gui::ToolBarItem* Workbench::setupToolBars() const
<< "FEM_PostFilterCutFunction"
<< "FEM_PostFilterClipRegion"
<< "FEM_PostFilterContours"
#ifdef FC_USE_VTK_PYTHON
<< "FEM_PostFilterGlyph"
#endif
<< "FEM_PostFilterDataAlongLine"
<< "FEM_PostFilterLinearizedStresses"
<< "FEM_PostFilterDataAtPoint"
@@ -355,6 +358,9 @@ Gui::MenuItem* Workbench::setupMenuBar() const
<< "FEM_PostFilterCutFunction"
<< "FEM_PostFilterClipRegion"
<< "FEM_PostFilterContours"
#ifdef FC_USE_VTK_PYTHON
<< "FEM_PostFilterGlyph"
#endif
<< "FEM_PostFilterDataAlongLine"
<< "FEM_PostFilterLinearizedStresses"
<< "FEM_PostFilterDataAtPoint"

View File

@@ -80,6 +80,11 @@ class FemWorkbench(Workbench):
False if FemGui.__name__ else True
False if femcommands.commands.__name__ else True
# check vtk version to potentially find missmatchs
from femguiutils.vtk_module_handling import vtk_module_handling
vtk_module_handling()
def GetClassName(self):
# see https://forum.freecad.org/viewtopic.php?f=10&t=43300
return "FemGui::Workbench"

View File

@@ -653,6 +653,22 @@ def makePostVtkFilterContours(doc, base_vtk_result, name="VtkFilterContours"):
return obj
def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"):
"""makePostVtkFilterGlyph(document, [name]):
creates a FEM post processing filter that visualizes vector fields with glyphs
"""
obj = doc.addObject("Fem::PostFilterPython", name)
from femobjects import post_glyphfilter
post_glyphfilter.PostGlyphFilter(obj)
base_vtk_result.addObject(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_glyphfilter
view_post_glyphfilter.VPPostGlyphFilter(obj.ViewObject)
return obj
def makePostVtkResult(doc, result_data, name="VtkResult"):
"""makePostVtkResult(document, base_result, [name]):
creates a FEM post processing result data (vtk based) to hold FEM results

View File

@@ -1217,6 +1217,21 @@ class _SolverZ88(CommandManager):
self.do_activated = "add_obj_on_gui_expand_noset_edit"
class _PostFilterGlyph(CommandManager):
"The FEM_PostFilterGlyph command definition"
def __init__(self):
super().__init__()
self.menutext = Qt.QT_TRANSLATE_NOOP("FEM_PostFilterGlyph", "Glyph filter")
self.accel = "F, G"
self.tooltip = Qt.QT_TRANSLATE_NOOP(
"FEM_PostFilterGlyph",
"Post processing filter that adds glyphs to the mesh vertices for vertex data visualization",
)
self.is_active = "with_vtk_selresult"
self.do_activated = "add_filter_set_edit"
# the string in add command will be the page name on FreeCAD wiki
FreeCADGui.addCommand("FEM_Analysis", _Analysis())
FreeCADGui.addCommand("FEM_ClippingPlaneAdd", _ClippingPlaneAdd())
@@ -1271,3 +1286,6 @@ FreeCADGui.addCommand("FEM_SolverElmer", _SolverElmer())
FreeCADGui.addCommand("FEM_SolverMystran", _SolverMystran())
FreeCADGui.addCommand("FEM_SolverRun", _SolverRun())
FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88())
if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph())

View File

@@ -34,6 +34,7 @@ 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
@@ -89,6 +90,8 @@ class CommandManager:
FreeCADGui.ActiveDocument is not None
and self.result_selected()
)
elif self.is_active == "with_vtk_selresult":
active = self.vtk_result_selected()
elif self.is_active == "with_part_feature":
active = FreeCADGui.ActiveDocument is not None and self.part_feature_selected()
elif self.is_active == "with_femmesh":
@@ -144,6 +147,8 @@ class CommandManager:
self.add_obj_on_gui_selobj_set_edit(self.__class__.__name__.lstrip("_"))
elif self.do_activated == "add_obj_on_gui_selobj_expand_noset_edit":
self.add_obj_on_gui_selobj_expand_noset_edit(self.__class__.__name__.lstrip("_"))
elif self.do_activated == "add_filter_set_edit":
self.add_filter_set_edit(self.__class__.__name__.lstrip("_"))
# in all other cases Activated is implemented it the command class
def results_present(self):
@@ -169,6 +174,13 @@ class CommandManager:
return True
return False
def vtk_result_selected(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) == 1 and sel[0].isDerivedFrom("Fem::FemPostObject"):
self.selobj = sel[0]
return True
return False
def part_feature_selected(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) == 1 and sel[0].isDerivedFrom("Part::Feature"):
@@ -363,3 +375,48 @@ class CommandManager:
)
# expand selobj in tree view
expandParentObject()
def add_filter_set_edit(self, filtertype):
# like add_obj_on_gui_selobj_noset_edit but the selection is kept
# and the selobj is expanded in the tree to see the added obj
# check if we should use python fitler
if vtk_compatibility_abort(True):
return
# Note: we know selobj is a FemPostObject as otherwise the command should not have been active
# We also assume the all filters are in PostGroups and not astray
group = None
if self.selobj.hasExtension("Fem::FemPostGroupExtension"):
group = self.selobj
else:
group = self.selobj.getParentPostGroup()
FreeCAD.ActiveDocument.openTransaction(f"Create Fem{filtertype}")
FreeCADGui.addModule("ObjectsFem")
FreeCADGui.doCommand(
"ObjectsFem.make{}("
"FreeCAD.ActiveDocument, FreeCAD.ActiveDocument.{})".format(filtertype, group.Name)
)
# set display and selection style to assure the user sees the new object
FreeCADGui.doCommand(
'FreeCAD.ActiveDocument.ActiveObject.ViewObject.DisplayMode = "Surface"'
)
FreeCADGui.doCommand(
'FreeCAD.ActiveDocument.ActiveObject.ViewObject.SelectionStyle = "BoundBox"'
)
# hide selected filter
FreeCADGui.doCommand(
"FreeCAD.ActiveDocument.{}.ViewObject.Visibility = False".format(self.selobj.Name)
)
# recompute, expand selobj in tree view
expandParentObject()
FreeCADGui.doCommand("FreeCAD.ActiveDocument.ActiveObject.recompute()")
# set edit
FreeCADGui.Selection.clearSelection()
FreeCADGui.doCommand(
"FreeCADGui.ActiveDocument.setEdit(FreeCAD.ActiveDocument.ActiveObject.Name)"
)

View File

@@ -0,0 +1,258 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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 *
# * *
# ***************************************************************************
"""Methods to verify if the python VTK module is the correct one
FreeCAD is linked with VTK libraries during its build process. To use the VTK
python module and pass objects between python and c++ the compiled module library
needs to be linked to the exact same vtk library as FreeCAD is. This is ensured by
installing VTK via linux app managers: All known distros install the python side
packages together with vtk libs. Libpack and other OS systems ensure this too.
However, if a vtk python package is installed manually, e.g. by "pip install vtk",
it could be found instead of the system module. This python API brings its own
set of VTK libs, and hence object passing in FreeCAD fails. (Note: import and
pure vtk python code still works, only passing to c++ fails)
This file provides functions that detect this situation and inform the user.
Additionally we try to find the correct module in the path and offer to use
it instead.
Note that this problem occurs with all "compiled binary" python APIs, also
with PySide. It is the users responsibility to handle his python path and keep
it clean/working. The functions provided here are a workaround only.
"""
__title__ = "FEM GUI vtk python module check"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
__user_input_received = False
def vtk_module_compatible():
# checks if the VTK library FreeCAD is build against is the one used by
# the python module
# make sure we do not contaminate the modules with vtk to not trick
# the check later
unload = not _vtk_is_loaded()
import Fem
from vtkmodules.vtkCommonCore import vtkVersion, vtkBitArray
# simple version check
if Fem.getVtkVersion() != vtkVersion.GetVTKVersion():
return False
# check binary compatibility
result = Fem.isVtkCompatible(vtkBitArray())
if unload:
# cleanup our own import
_unload_vtk_modules()
return result
def _vtk_is_loaded():
import sys
return any("vtkmodules" in module for module in sys.modules)
def _unload_vtk_modules():
# unloads all loaded vtk modules
# NOTE: does not remove any stored references in objects
import sys
for module in sys.modules.copy():
if "vtkmodules" in module:
del sys.modules[module]
def _find_compatible_module():
# Check all python path folders if they contain a vtk module
import Fem
import sys
# remove module from runtime
_unload_vtk_modules()
path = sys.path.copy()
for folder in reversed(path):
try:
# use a single folder as path and try to load vtk
sys.path = [folder]
if vtk_module_compatible():
# we do still unload, to let the user descide if he wants to use it
_unload_vtk_modules()
sys.path = path
return folder
except:
continue
# reset the correct path and indicate that we failed
sys.path = path
return None
def _load_vtk_from(folder):
import sys
path = sys.path
try:
sys.path = [folder]
import vtkmodules
finally:
sys.path = path
# If FreeCAD is build with VTK python support this function checks if the
# used python module is compatible with the c++ lib. Does inform the user
# if not so and offers the correct module, if available
#
# Note: Call this also from Python feature module, as on document load
# this can be loaded before initializing FEM workbench.
def vtk_module_handling():
import sys
import FreeCAD
if not "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
# no VTK python api support in FreeCAD
return
# only ask user once per session
global __user_input_received
if __user_input_received:
return
__user_input_received = True
loaded = _vtk_is_loaded()
# check if we are compatible
if not vtk_module_compatible():
if not FreeCAD.GuiUp:
FreeCAD.Console.PrintError(
"FEM: vtk python module is not compatible with internal vtk library"
)
return
import FreeCAD, Fem
from vtkmodules.vtkCommonCore import vtkVersion
import inspect
from PySide import QtGui
translate = FreeCAD.Qt.translate
path = inspect.getfile(vtkVersion)
path = path[: path.find("vtkmodules")]
message = translate(
"FEM",
(
"FreeCAD is linked to a different VTK library then the imported "
"VTK python module. This is incompatible and will lead to errors."
"\n\nWrong python module is imported from: \n{}"
),
).format(path)
buttons = QtGui.QMessageBox.Discard
# check if there is any compatible vtk module
compatible_module = _find_compatible_module()
if compatible_module:
# there is a compatible module of VTK available.
message += translate("FEM", "\n\nCorrect module found in: \n{}").format(
compatible_module
)
if not loaded:
# vtk was not loaded beforehand, therefore we can realistically reload
message += translate("FEM", "\n\nShould this module be loaded instead?")
buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
else:
message += translate(
"FEM",
(
"\n\nAs the wrong module was already loaded, a reload is not possible. "
"Restart FreeCAD to get the option for loading this module."
),
)
else:
message += translate(
"FEM", "\n\nNo matching module was found in the current python path."
)
# raise a dialog to the user
import FreeCADGui
button = QtGui.QMessageBox.critical(
FreeCADGui.getMainWindow(),
translate("FEM", "VTK module conflict"),
message,
buttons=buttons,
)
if button == QtGui.QMessageBox.Yes:
# try to reload the correct vtk module
_load_vtk_from(compatible_module)
# Returns if vtk python is incompatible and hence operations need to be aborted.
# If inform=True the user gets informed by dialog about incompatibilities
def vtk_compatibility_abort(inform=True):
if not vtk_module_compatible():
if inform:
# raise a dialog to the user that this functionality is not available
import FreeCAD
import FreeCADGui
from PySide import QtGui
translate = FreeCAD.Qt.translate
button = QtGui.QMessageBox.critical(
FreeCADGui.getMainWindow(),
translate("FEM", "VTK module conflict"),
translate(
"FEM", "This functionality is not available due to VTK python module conflict"
),
buttons=QtGui.QMessageBox.Discard,
)
return True
return False

View File

@@ -0,0 +1,271 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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 glyph filter"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_glyphfilter
# \ingroup FEM
# \brief Post processing filter creating glyphs for vector fields
import FreeCAD
# check vtk version to potentially find missmatchs
from femguiutils.vtk_module_handling import vtk_module_handling
vtk_module_handling()
# IMPORTANT: Never import vtk directly. Often vtk is compiled with different QT
# version than FreeCAD, and "import vtk" crashes by importing qt components.
# Always import the filter and data modules only.
from vtkmodules.vtkFiltersCore import vtkMaskPoints
from vtkmodules.vtkFiltersCore import vtkGlyph3D
import vtkmodules.vtkFiltersSources as vtkSources
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject):
"""
A post processing filter adding glyphs
"""
Type = "Fem::PostFilterPython"
def __init__(self, obj):
super().__init__(obj)
for prop in self._get_properties():
prop.add_to_object(obj)
self.__setupFilterPipeline(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyEnumeration",
name="Glyph",
group="Glyph",
doc="The form of the glyph",
value=["Arrow", "Cube"],
),
_PropHelper(
type="App::PropertyEnumeration",
name="OrientationData",
group="Glyph",
doc="Which vector field is used to orient the glyphs",
value=["None"],
),
_PropHelper(
type="App::PropertyEnumeration",
name="ScaleData",
group="Scale",
doc="Which data field is used to scale the glyphs",
value=["None"],
),
_PropHelper(
type="App::PropertyEnumeration",
name="VectorScaleMode",
group="Scale",
doc="If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components",
value=["Not a vector"],
),
_PropHelper(
type="App::PropertyFloatConstraint",
name="ScaleFactor",
group="Scale",
doc="A constant multiplier the glyphs are scaled with",
value=(1, 0, 1e12, 1e-12),
),
_PropHelper(
type="App::PropertyEnumeration",
name="MaskMode",
group="Masking",
doc="Which vertices are used as glyph locations",
value=["Use All", "Every Nth", "Uniform Samping"],
),
_PropHelper(
type="App::PropertyIntegerConstraint",
name="Stride",
group="Masking",
doc='Define the stride for "Every Nth" masking mode',
value=(2, 1, 999999999, 1),
),
_PropHelper(
type="App::PropertyIntegerConstraint",
name="MaxNumber",
group="Masking",
doc='Defines the maximal number of vertices used for "Uniform Sampling" masking mode',
value=(1000, 1, 999999999, 1),
),
]
return prop
def __setupMaskingFilter(self, obj, masking):
if obj.MaskMode == "Use All":
masking.RandomModeOff()
masking.SetOnRatio(1)
masking.SetMaximumNumberOfPoints(int(1e10))
elif obj.MaskMode == "Every Nth":
masking.RandomModeOff()
masking.SetOnRatio(obj.Stride)
masking.SetMaximumNumberOfPoints(int(1e10))
else:
masking.SetOnRatio(1)
masking.SetMaximumNumberOfPoints(obj.MaxNumber)
masking.RandomModeOn()
def __setupGlyphFilter(self, obj, glyph):
# scaling
if obj.ScaleData != "None":
glyph.ScalingOn()
if obj.ScaleData in obj.getInputVectorFields():
# make sure the vector mode is set correctly
if obj.VectorScaleMode == "Not a vector":
obj.VectorScaleMode = ["Scale by magnitude", "Scale by components"]
obj.VectorScaleMode = "Scale by magnitude"
if obj.VectorScaleMode == "Scale by magnitude":
glyph.SetScaleModeToScaleByVector()
else:
glyph.SetScaleModeToScaleByVectorComponents()
glyph.SetInputArrayToProcess(2, 0, 0, 0, obj.ScaleData)
else:
# scalar scaling mode
if obj.VectorScaleMode != "Not a vector":
obj.VectorScaleMode = ["Not a vector"]
glyph.SetInputArrayToProcess(2, 0, 0, 0, obj.ScaleData)
glyph.SetScaleModeToScaleByScalar()
else:
glyph.ScalingOff()
glyph.SetScaleFactor(obj.ScaleFactor)
# Orientation
if obj.OrientationData != "None":
glyph.OrientOn()
glyph.SetInputArrayToProcess(1, 0, 0, 0, obj.OrientationData)
else:
glyph.OrientOff()
def __setupFilterPipeline(self, obj):
# store of all algorithms for later access
# its map filter_name : [source, mask, glyph]
self._algorithms = {}
# create all vtkalgorithm combinations and set them as filter pipeline
sources = {"Arrow": vtkSources.vtkArrowSource, "Cube": vtkSources.vtkCubeSource}
for source_name in sources:
source = sources[source_name]()
masking = vtkMaskPoints()
self.__setupMaskingFilter(obj, masking)
glyph = vtkGlyph3D()
glyph.SetSourceConnection(source.GetOutputPort(0))
glyph.SetInputConnection(masking.GetOutputPort(0))
self.__setupGlyphFilter(obj, glyph)
self._algorithms[source_name] = [source, masking, glyph]
obj.addFilterPipeline(source_name, masking, glyph)
obj.setActiveFilterPipeline(obj.Glyph)
def onDocumentRestored(self, obj):
# resetup the pipeline
self.__setupFilterPipeline(obj)
def execute(self, obj):
# we check what new inputs
vector_fields = obj.getInputVectorFields()
all_fields = vector_fields + obj.getInputScalarFields()
vector_fields.sort()
all_fields.sort()
current_orient = obj.OrientationData
enumeration = ["None"] + vector_fields
obj.OrientationData = enumeration
if current_orient in enumeration:
obj.OrientationData = current_orient
current_scale = obj.ScaleData
enumeration = ["None"] + all_fields
obj.ScaleData = enumeration
if current_scale in enumeration:
obj.ScaleData = current_scale
# make sure parent class execute is called!
return False
def onChanged(self, obj, prop):
# check if we are setup already
if not hasattr(self, "_algorithms"):
return
if prop == "Glyph":
obj.setActiveFilterPipeline(obj.Glyph)
if prop == "MaskMode":
for filter in self._algorithms:
masking = self._algorithms[filter][1]
self.__setupMaskingFilter(obj, masking)
if prop == "Stride":
# if mode is use all stride setting needs to stay at one
if obj.MaskMode == "Every Nth":
for filter in self._algorithms:
masking = self._algorithms[filter][1]
masking.SetOnRatio(obj.Stride)
if prop == "MaxNumber":
if obj.MaskMode == "Uniform Sampling":
for filter in self._algorithms:
masking = self._algorithms[filter][1]
masking.SetMaximumNumberOfPoints(obj.MaxNumber)
if prop == "OrientationData" or prop == "ScaleData":
for filter in self._algorithms:
glyph = self._algorithms[filter][2]
self.__setupGlyphFilter(obj, glyph)
if prop == "ScaleFactor":
for filter in self._algorithms:
glyph = self._algorithms[filter][2]
glyph.SetScaleFactor(obj.ScaleFactor)

View File

@@ -0,0 +1,209 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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 glyph filter task panel for the document object"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package task_post_glyphfilter
# \ingroup FEM
# \brief task panel for post glyph filter object
from PySide import QtCore, QtGui
import FreeCAD
import FreeCADGui
from femguiutils import selection_widgets
from . import base_femtaskpanel
class _TaskPanel(base_femtaskpanel._BaseTaskPanel):
"""
The TaskPanel for editing properties of glyph filter
"""
def __init__(self, vobj):
super().__init__(vobj.Object)
# glyph parameter widget
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostGlyph.ui"
)
self.widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostFilterGlyph.svg"))
self.__init_widget()
# 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
# ###############
def __init_widget(self):
# set current values to ui
self._enumPropertyToCombobox(self.obj, "Glyph", self.widget.FormComboBox)
self._enumPropertyToCombobox(self.obj, "OrientationData", self.widget.OrientationComboBox)
self._enumPropertyToCombobox(self.obj, "ScaleData", self.widget.ScaleComboBox)
self._enumPropertyToCombobox(self.obj, "VectorScaleMode", self.widget.VectorModeComboBox)
self._enumPropertyToCombobox(self.obj, "MaskMode", self.widget.MaskModeComboBox)
self.widget.ScaleFactorBox.setValue(self.obj.ScaleFactor)
self.__slide_min = self.obj.ScaleFactor * 0.5
self.__slide_max = self.obj.ScaleFactor * 1.5
self.widget.ScaleSlider.setValue(50)
self.widget.StrideBox.setValue(self.obj.Stride)
self.widget.MaxBox.setValue(self.obj.MaxNumber)
self.__update_scaling_ui()
self.__update_masking_ui()
# connect all signals
self.widget.FormComboBox.currentTextChanged.connect(self._form_changed)
self.widget.OrientationComboBox.currentTextChanged.connect(self._orientation_changed)
self.widget.ScaleComboBox.currentTextChanged.connect(self._scale_data_changed)
self.widget.VectorModeComboBox.currentTextChanged.connect(self._scale_vector_mode_changed)
self.widget.ScaleFactorBox.valueChanged.connect(self._scale_factor_changed)
self.widget.ScaleSlider.valueChanged.connect(self._scale_slider_changed)
self.widget.MaskModeComboBox.currentTextChanged.connect(self._mask_mode_changed)
self.widget.StrideBox.valueChanged.connect(self._stride_changed)
self.widget.MaxBox.valueChanged.connect(self._max_number_changed)
def __update_scaling_ui(self):
enabled = self.widget.ScaleComboBox.currentIndex() != 0
self.widget.VectorModeComboBox.setEnabled(enabled)
self.widget.ScaleFactorBox.setEnabled(enabled)
self.widget.ScaleSlider.setEnabled(enabled)
def __update_masking_ui(self):
enabled = self.widget.MaskModeComboBox.currentIndex() != 0
self.widget.StrideBox.setEnabled(enabled)
self.widget.MaxBox.setEnabled(enabled)
# callbacks and logic
# ###################
def _form_changed(self, value):
self.obj.Glyph = value
self._recompute()
def _orientation_changed(self, value):
self.obj.OrientationData = value
self._recompute()
def _scale_data_changed(self, value):
self.obj.ScaleData = value
self._enumPropertyToCombobox(self.obj, "VectorScaleMode", self.widget.VectorModeComboBox)
self.__update_scaling_ui()
self._recompute()
def _scale_vector_mode_changed(self, value):
self.obj.VectorScaleMode = value
self._recompute()
def _scale_factor_changed(self, value):
# set slider
self.__slide_min = value * 0.5
self.__slide_max = value * 1.5
slider_value = (value - self.__slide_min) / (self.__slide_max - self.__slide_min) * 100.0
self.widget.ScaleSlider.blockSignals(True)
self.widget.ScaleSlider.setValue(slider_value)
self.widget.ScaleSlider.blockSignals(False)
self.obj.ScaleFactor = value
self._recompute()
def _scale_slider_changed(self, value):
# calculate value
# ( max - min )
# factor = min + ( slider_value x ------------- )
# 100
#
f = self.__slide_min + (value * (self.__slide_max - self.__slide_min) / 100)
# sync factor spin box
self.widget.ScaleFactorBox.blockSignals(True)
self.widget.ScaleFactorBox.setValue(f)
self.widget.ScaleFactorBox.blockSignals(False)
# set value
self.obj.ScaleFactor = f
self._recompute()
def _mask_mode_changed(self, value):
self.obj.MaskMode = value
self.__update_masking_ui()
self._recompute()
def _stride_changed(self, value):
self.obj.Stride = value
self._recompute()
def _max_number_changed(self, value):
self.obj.MaxNumber = value
self._recompute()

View File

@@ -87,7 +87,7 @@ def get_defmake_count(fem_vtk_post=True):
# we are not able to create VTK post objects
new_lines = []
for li in lines_defmake:
if "PostVtk" not in li:
if "Post" not in li:
new_lines.append(li)
lines_defmake = new_lines
return len(lines_defmake)

View File

@@ -81,9 +81,13 @@ class TestObjectCreate(unittest.TestCase):
# result children: mesh result --> 1
# post pipeline children: region, scalar, cut, wrap --> 5
# analysis itself is not in analysis group --> 1
# thus: -20
# vtk python post objects: glyph --> 1
self.assertEqual(len(doc.Analysis.Group), count_defmake - 20)
subtraction = 20
if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
subtraction += 1
self.assertEqual(len(doc.Analysis.Group), count_defmake - subtraction)
self.assertEqual(len(doc.Objects), count_defmake)
fcc_print(
@@ -1154,6 +1158,8 @@ def create_all_fem_objects_doc(doc):
ObjectsFem.makePostVtkFilterCutFunction(doc, vres)
ObjectsFem.makePostVtkFilterWarp(doc, vres)
ObjectsFem.makePostVtkFilterContours(doc, vres)
if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
ObjectsFem.makePostFilterGlyph(doc, vres)
analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc))
analysis.addObject(ObjectsFem.makeSolverCalculiX(doc))

View File

@@ -0,0 +1,86 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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 glyph filter ViewProvider for the document object"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package view_post_glyphfilter
# \ingroup FEM
# \brief view provider for post glyph filter object
import FreeCAD
import FreeCADGui
import FemGui
from PySide import QtGui
from femtaskpanels import task_post_glyphfilter
class VPPostGlyphFilter:
"""
A View Provider for the PostGlyphFilter object
"""
def __init__(self, vobj):
vobj.Proxy = self
def getIcon(self):
return ":/icons/FEM_PostFilterGlyph.svg"
def getDisplayModes(self, obj):
# Mandatory, as the ViewProviderPostFilterPython does not add any
# display modes. We can choose here any that is supported by it:
# "Outline", "Nodes", "Surface", "Surface with Edges",
# "Wireframe", "Wireframe (surface only)", "Nodes (surface only)"
# only surface makes sense for the glyphs
return ["Surface"]
def setDisplayMode(self, mode):
# the post object viewprovider implements the different display modes
# via vtk filter, not via masking modes. Hence we need to make sure
# to always stay in the "Default" masking mode, no matter the display mode
return "Default"
def setEdit(self, vobj, mode):
# make sure we see what we edit
vobj.show()
# build up the task panel
taskd = task_post_glyphfilter._TaskPanel(vobj)
# show it
FreeCADGui.Control.showDialog(taskd)
return True
def unsetEdit(self, vobj, mode):
FreeCADGui.Control.closeDialog()
return True
def dumps(self):
return None
def loads(self, state):
return None