Fem: Implement basic python filter functionality and glyph example

This commit is contained in:
Stefan Tröger
2025-02-13 19:35:10 +01:00
parent b3a3b13603
commit 491923e41e
33 changed files with 1793 additions and 166 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,15 @@ macro(SetupSalomeSMESH)
endif()
set(BUILD_FEM_VTK ON)
# check if PythonWrapperCore was found (vtk 9 only)
if (${VTK_WrappingPythonCore_FOUND})
add_compile_definitions(BUILD_FEM_VTK_WRAPPER)
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

@@ -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();
algo->Update();
if(!algo) {
return nullptr;
}
if (Frame.getValue()>0) {
algo->UpdateTimeStep(Frame.getValue());
}
else {
algo->Update();
}
return vtkDataSet::SafeDownCast(algo->GetOutputDataObject(0));
}
@@ -251,6 +285,44 @@ 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,15 @@ 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 +94,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 filters 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 filters 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 filters 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,193 @@
/***************************************************************************
* 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 BUILD_FEM_VTK_WRAPPER
#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 BUILD_FEM_VTK_WRAPPER
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 BUILD_FEM_VTK_WRAPPER
// 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,10 @@
#include <vtkXMLUnstructuredGridReader.h>
#endif
#ifdef BUILD_FEM_VTK_WRAPPER
#include <vtkPythonUtil.h>
#endif
#include <App/Application.h>
#include <App/DocumentObject.h>
#include <Base/Console.h>
@@ -162,12 +166,26 @@ 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 BUILD_FEM_VTK_WRAPPER
//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*/)
{}
{
throw Base::NotImplementedError();
}
App::Property* PropertyPostDataObject::Copy() const
{

View File

@@ -202,6 +202,7 @@ SET(FemObjects_SRCS
femobjects/mesh_netgen.py
femobjects/mesh_region.py
femobjects/mesh_result.py
femobjects/post_glyphfilter.py
femobjects/result_mechanical.py
femobjects/solver_calculix.py
femobjects/solver_ccxtools.py
@@ -604,6 +605,7 @@ SET(FemGuiTaskPanels_SRCS
femtaskpanels/task_mesh_group.py
femtaskpanels/task_mesh_region.py
femtaskpanels/task_mesh_netgen.py
femtaskpanels/task_post_glyphfilter.py
femtaskpanels/task_result_mechanical.py
femtaskpanels/task_solver_calculix.py
femtaskpanels/task_solver_ccxtools.py
@@ -654,6 +656,7 @@ SET(FemGuiViewProvider_SRCS
femviewprovider/view_mesh_netgen.py
femviewprovider/view_mesh_region.py
femviewprovider/view_mesh_result.py
femviewprovider/view_post_glyphfilter.py
femviewprovider/view_result_mechanical.py
femviewprovider/view_solver_calculix.py
femviewprovider/view_solver_ccxtools.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,19 @@ 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 +298,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 +308,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 +321,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 +333,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 +349,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 +396,16 @@ 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"),
: 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,15 @@ void TaskPostFrames::applyPythonCode()
// ***************************************************************************
// Branch
TaskPostBranch::TaskPostBranch(ViewProviderFemPostBranchFilter* view, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostBranchFilter"),
tr("Branch behaviour"),
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 +620,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 +1040,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 +1395,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 +1406,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 +1554,16 @@ void TaskPostClip::onInsideOutToggled(bool val)
// ***************************************************************************
// contours filter
TaskPostContours::TaskPostContours(ViewProviderFemPostContours* view, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterContours"),
tr("Contours filter options"),
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 +1708,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 +1719,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 +1846,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 +1970,16 @@ void TaskPostScalarClip::onInsideOutToggled(bool val)
// ***************************************************************************
// warp vector filter
TaskPostWarpVector::TaskPostWarpVector(ViewProviderFemPostWarpVector* view, QWidget* parent)
: TaskPostBox(view,
: TaskPostWidget(view,
Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"),
tr("Warp options"),
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 +2144,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,
const QPixmap& icon,
const QString& title,
QWidget* parent = nullptr);
~TaskPostBox() override;
TaskPostWidget(Gui::ViewProviderDocumentObject* view,
const QPixmap& icon,
const QString& title = QString(),
QWidget* parent = nullptr);
~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,12 +312,13 @@ private:
// branch
class ViewProviderFemPostBranchFilter;
class TaskPostBranch: public TaskPostBox
class TaskPostBranch: public TaskPostWidget
{
Q_OBJECT
public:
explicit TaskPostBranch(ViewProviderFemPostBranchFilter* view, QWidget* parent = nullptr);
explicit TaskPostBranch(ViewProviderFemPostBranchFilter* view,
QWidget* parent = nullptr);
~TaskPostBranch() override;
void applyPythonCode() override;
@@ -326,7 +328,6 @@ private:
void onModeIndexChanged(int);
void onOutputIndexChanged(int);
QWidget* proxy;
std::unique_ptr<Ui_TaskPostBranch> ui;
};
@@ -334,7 +335,7 @@ private:
// data along line filter
class ViewProviderFemPostDataAlongLine;
class TaskPostDataAlongLine: public TaskPostBox
class TaskPostDataAlongLine: public TaskPostWidget
{
Q_OBJECT
@@ -362,7 +363,6 @@ private:
private:
std::string Plot();
std::string ObjectVisible();
QWidget* proxy;
std::unique_ptr<Ui_TaskPostDataAlongLine> ui;
DataAlongLineMarker* marker;
};
@@ -372,7 +372,7 @@ private:
// data at point filter
class ViewProviderFemPostDataAtPoint;
class TaskPostDataAtPoint: public TaskPostBox
class TaskPostDataAtPoint: public TaskPostWidget
{
Q_OBJECT
@@ -400,7 +400,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 +408,7 @@ private:
// clip filter
class ViewProviderFemPostClip;
class TaskPostClip: public TaskPostBox
class TaskPostClip: public TaskPostWidget
{
Q_OBJECT
@@ -435,7 +434,6 @@ private:
void collectImplicitFunctions();
// App::PropertyLink* m_functionProperty;
QWidget* proxy;
std::unique_ptr<Ui_TaskPostClip> ui;
FunctionWidget* fwidget;
};
@@ -445,7 +443,7 @@ private:
// contours filter
class ViewProviderFemPostContours;
class TaskPostContours: public TaskPostBox
class TaskPostContours: public TaskPostWidget
{
Q_OBJECT
@@ -464,7 +462,6 @@ private:
void onRelaxationChanged(double v);
private:
QWidget* proxy;
std::unique_ptr<Ui_TaskPostContours> ui;
bool blockVectorUpdate = false;
void updateFields();
@@ -475,7 +472,7 @@ private:
// cut filter
class ViewProviderFemPostCut;
class TaskPostCut: public TaskPostBox
class TaskPostCut: public TaskPostWidget
{
Q_OBJECT
@@ -499,7 +496,6 @@ private:
void collectImplicitFunctions();
// App::PropertyLink* m_functionProperty;
QWidget* proxy;
std::unique_ptr<Ui_TaskPostCut> ui;
FunctionWidget* fwidget;
};
@@ -509,7 +505,7 @@ private:
// scalar clip filter
class ViewProviderFemPostScalarClip;
class TaskPostScalarClip: public TaskPostBox
class TaskPostScalarClip: public TaskPostWidget
{
Q_OBJECT
@@ -527,7 +523,6 @@ private:
void onInsideOutToggled(bool val);
private:
QWidget* proxy;
std::unique_ptr<Ui_TaskPostScalarClip> ui;
};
@@ -536,7 +531,7 @@ private:
// warp vector filter
class ViewProviderFemPostWarpVector;
class TaskPostWarpVector: public TaskPostBox
class TaskPostWarpVector: public TaskPostWidget
{
Q_OBJECT
@@ -555,7 +550,6 @@ private:
void onVectorCurrentIndexChanged(int idx);
private:
QWidget* proxy;
std::unique_ptr<Ui_TaskPostWarpVector> ui;
};
@@ -564,7 +558,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,39 @@
#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 GuiExport ViewProviderFeaturePythonT<FemGui::ViewProviderFemPostFilterPythonBase>;
}
// ***************************************************************************
// in the following, the different filters sorted alphabetically
// ***************************************************************************
@@ -54,7 +83,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 +132,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 +154,8 @@ 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 +177,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 +197,8 @@ 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 +220,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 +243,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 +279,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,36 @@
#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();
updateProperties();
if (!isRestoring()) {
updateProperties();
}
update3D();
}
@@ -931,7 +932,9 @@ void ViewProviderFemPostObject::onChanged(const App::Property* prop)
}
if (prop == &Field && setupPipeline()) {
updateProperties();
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,20 +114,8 @@ 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,

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 BUILD_FEM_VTK_WRAPPER
<< "FEM_PostFilterGlyph"
#endif
<< "FEM_PostFilterDataAlongLine"
<< "FEM_PostFilterLinearizedStresses"
<< "FEM_PostFilterDataAtPoint"

View File

@@ -652,6 +652,19 @@ def makePostVtkFilterContours(doc, base_vtk_result, name="VtkFilterContours"):
base_vtk_result.addObject(obj)
return obj
def makePostVtkFilterGlyph(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]):
@@ -669,7 +682,6 @@ def makePostVtkResult(doc, result_data, name="VtkResult"):
obj.ViewObject.DisplayMode = "Surface"
return obj
# ********* solver objects ***********************************************************************
def makeEquationDeformation(doc, base_solver=None, name="Deformation"):
"""makeEquationDeformation(document, [base_solver], [name]):

View File

@@ -1216,6 +1216,17 @@ class _SolverZ88(CommandManager):
self.is_active = "with_analysis"
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"
# the string in add command will be the page name on FreeCAD wiki
FreeCADGui.addCommand("FEM_Analysis", _Analysis())
@@ -1271,3 +1282,4 @@ FreeCADGui.addCommand("FEM_SolverElmer", _SolverElmer())
FreeCADGui.addCommand("FEM_SolverMystran", _SolverMystran())
FreeCADGui.addCommand("FEM_SolverRun", _SolverRun())
FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88())
FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph())

View File

@@ -89,6 +89,10 @@ 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 +148,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":
self.add_filter(self.__class__.__name__.lstrip("_"))
# in all other cases Activated is implemented it the command class
def results_present(self):
@@ -169,6 +175,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 +376,33 @@ class CommandManager:
)
# expand selobj in tree view
expandParentObject()
def add_filter(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
# 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)
)
# expand selobj in tree view
expandParentObject()

View File

@@ -0,0 +1,267 @@
# ***************************************************************************
# * 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
# 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,212 @@
# ***************************************************************************
# * 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.__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.
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

@@ -1154,6 +1154,7 @@ def create_all_fem_objects_doc(doc):
ObjectsFem.makePostVtkFilterCutFunction(doc, vres)
ObjectsFem.makePostVtkFilterWarp(doc, vres)
ObjectsFem.makePostVtkFilterContours(doc, vres)
ObjectsFem.makePostVtkFilterGlyph(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