Preprocessing: FreeCAD/OpenSCAD + Gmsh

Introduction

There are several open source tools for preparing 2D and 3D finite element meshes like Salome, FreeCAD, Gmsh, Netgen, etc. Most of them are GUI based geometrical modeling and meshing environments/tools but they also usually allow using their libraries in user scripts. Some of the above mentioned tools are handy for solid modeling, some of them are great for meshing. This tutorial shows how to combine solid geometry modeling functions provided by FreeCAD or OpenSCAD with meshing functions of Gmsh.

The collaboration of modeling, meshing and conversion tools and the workflow are illustrated in the following scheme.

_images/workflow.png

Creating geometry using FreeCAD

Functionalities of FreeCAD are accessible to Python and can be used to define geometrical models in simple Python scripts. There is a tutorial related to Python scripting in FreeCAD.

The first step in creating a Python script is to set up a path to the FreeCAD libraries and import all required modules:

1import sys
2FREECADPATH = '/usr/lib/freecad/lib/'
3sys.path.append(FREECADPATH)
4
5from FreeCAD import Base, newDocument
6import Part
7import Draft
8import ProfileLib.RegularPolygon as Poly

Now, a new empty FreeCAD document can be defined as:

doc = newDocument()

All new objects describing the geometry will be added to this document.

In the following lines a geometrical model of a screwdriver handle will be created. Let’s start by defining a sphere and a cylinder and join these objects into the one called uni:

 1radius = 0.01
 2height = 0.1
 3
 4cyl = doc.addObject("Part::Cylinder", "cyl")
 5cyl.Radius = radius
 6cyl.Height = height
 7
 8sph = doc.addObject("Part::Sphere", "sph")
 9sph.Radius = radius
10
11uni = doc.addObject("Part::MultiFuse", "uni")
12uni.Shapes = [cyl, sph]

Create a polygon, revolve it around the z-axis to create a solid and use the result as the cutting tool applied to uni object:

 1ske = doc.addObject('Sketcher::SketchObject', 'Sketch')
 2ske.Placement = Base.Placement(Base.Vector(0, 0, 0),
 3                               Base.Rotation(-0.707107, 0, 0, -0.707107))
 4Poly.makeRegularPolygon('Sketch', 5,
 5                        Base.Vector(-1.2 * radius, 0.9 * height, 0),
 6                        Base.Vector(-0.8 * radius, 0.9 * height, 0))
 7
 8cut = doc.addObject("PartDesign::Revolution", "Revolution")
 9cut.Sketch = ske
10cut.ReferenceAxis = (ske, ['V_Axis'])
11cut.Angle = 360.0
12
13dif = doc.addObject("Part::Cut", "dif")
14dif.Base = uni
15dif.Tool = cut

Create a cylinder, make a polar array of the cylinder objects and subtract it from the previous result:

 1cyl1 = doc.addObject("Part::Cylinder", "cyl1")
 2cyl1.Radius = 0.2 * radius
 3cyl1.Height = 1.1 * height
 4cyl1.Placement = Base.Placement(Base.Vector(-1.1 * radius, 0, -0.2 * height),
 5                                Base.Rotation(0, 0, 0, 1))
 6
 7arr = Draft.makeArray(cyl1, Base.Vector(1, 0, 0), Base.Vector(0, 1, 0), 2, 2)
 8arr.ArrayType = "polar"
 9arr.NumberPolar = 6
10
11dif2 = doc.addObject("Part::Cut", "dif2")
12dif2.Base = dif
13dif2.Tool = arr

Create a middle hole for the screwdriver metal part:

1cyl2 = doc.addObject("Part::Cylinder", "cyl2")
2cyl2.Radius = 0.3 * radius
3cyl2.Height = height
4
5dif3 = doc.addObject("Part::Cut", "dif3")
6dif3.Base = dif2
7dif3.Tool = cyl2

Finally, recompute the geometry, export the part to the STEP file and save the document in FreeCAD format (not really needed for subsequent mesh generation, but may be useful for visualization and geometry check):

1doc.recompute()
2
3Part.export([dif3], 'screwdriver_handle.step')
4
5doc.saveAs('screwdriver_handle.FCStd')

A finite element mesh can be generated directly in FreeCAD using MeshPart module:

1import MeshPart
2
3mesh = doc.addObject("Mesh::Feature", "Mesh")
4mesh.Mesh = MeshPart.meshFromShape(Shape=dif3.Shape, MaxLength=0.002)
5mesh.Mesh.write("./screwdriver_handle.bdf", "NAS", "mesh")

The meshing function of MeshPart module is limited to triangular grids so it is better to use Gmsh mesh generator which can provide triangular and quadrilateral meshes in 2D or tetrahedral and hexahedral meshes in 3D. Gmsh allows to control the meshing process through a wide range of parameters. Meshing by Gmsh will be described in section Gmsh - generating finite element mesh.

_images/sdh_freecad.png

The example of screwdriver handle: screwdriver_handle.py.

There are two simple ways how to discover Python calls of FreeCAD functions. You can enable “show script commands in python console” in Edit->Preferences->General->Macro and the Python console by selecting View->Views->Python Console and all subsequent operations will be printed in the console as the Python code. The second way is to switch on the macro recording function (Macro->Macro recording ...) which generates a Python script (FCMacro file) containing all the code related to actions in the FreeCAD graphical interface.

Creating geometry using OpenSCAD

The alternative tool for solid geometrical modeling is OpenSCAD - “The Programmers Solid 3D CAD Modeller”. It has its own description language based on functional programming that is used to construct solid models using geometrical primitives similar to FreeCAD. Solid geometries can be exported to several file formats including STL and CSG. OpenSCAD allows solid modeling based on Constructive Solid Geometry (CSG) principles and extrusion of 2D objects into 3D. The model of a screwdriver handle presented in the previous section can be defined in OpenSCAD by the following code (screwdriver_handle.scad):

 1radius = 0.01;
 2height = 0.1;
 3$fn = 50;
 4
 5difference() {
 6  difference() {
 7    difference() {
 8      union() {
 9        cylinder(center=false, h=height, r=radius);
10        sphere(radius);
11      };
12      translate([0, 0, 0.9*height])
13        rotate_extrude()
14          polygon([[0.8*radius, 0], [1.8*radius, -0.577*radius], [1.8*radius, 0.577*radius]]);
15    }
16    cylinder(center=false, h=1.1*height, r=0.3*radius);
17  }
18  for (i = [1:6]) {
19    rotate([0, 0, 360/6*i])
20      translate([-1.1*radius, 0.0, -0.2*height])
21        cylinder(center=false, h=1.1*height, r=0.2*radius);
22  }
23}
_images/sdh_openscad.png

To generate a finite element mesh of the solid geometry the model must be exported to a suitable file format. OpenSCAD has limited export options, but by using FreeCAD import/export functions, it is possible to find a workaround. The OpenSCAD model can be exported to the CSG file format and FreeCAD can be used as a mesh converter to the STEP format:

 1import sys
 2sys.path.append('/usr/lib/freecad/lib/')
 3sys.path.append('/usr/lib/freecad/Mod/OpenSCAD/')
 4
 5import FreeCAD
 6import Part
 7import importCSG
 8
 9importCSG.open('screwdriver_handle.csg')
10Part.export([FreeCAD.ActiveDocument.Objects[-1]], 'screwdriver_handle.step')

Gmsh - generating finite element mesh

Gmsh can create finite element meshes using geometrical models imported from STEP, IGES and BRep files (has to be compiled with OpenCASCADE support).

The following GEO file imports screwdriver_handle.step file and defines a field controlling the mesh size (screwdriver_handle.geo):

1Merge "screwdriver_handle.step";
2
3Field[1] = MathEval;
4Field[1].F = "0.002";
5Background Field = 1;

Now, run Gmsh generator and export the mesh into the MSH format in which all surface and volumetric elements are stored:

gmsh -3 -format msh -o screwdriver_handle.msh screwdriver_handle.geo

By converting the MSH file into the VTK format using sfepy-convert:

sfepy-convert -d 3 screwdriver_handle.msh screwdriver_handle.vtk

the surface elements are discarded and only the volumetric mesh is preserved.

_images/sdh_mesh.png

Note: planar 2D meshes

To create a planar 2D mesh, such as

_images/circle_in_square.png

that can be described by this Gmsh code, the mesh generator can be called as follows:

gmsh -2 -format msh -o circle_in_square.msh circle_in_square.geo

This, however is not enough to create a truly 2D mesh - the created mesh vertices still have the third, z, component which is equal to zero. In order to remove the third component, use:

sfepy-convert --2d circle_in_square.msh circle_in_square.h5

Now, in the resulting circle_in_square.h5, each vertex has only two coordinates. Another way of generating the 2D mesh is to use the legacy VTK format as follows:

gmsh -2 -format vtk -o circle_in_square.vtk circle_in_square.geo
sfepy-convert circle_in_square.vtk circle_in_square.h5

This is due to the fact that the legacy VTK does not support 2D vertices and so the VTKMeshIO reader tries to detect the planar geometry by comparing the z components to zero - the --2d option of sfepy-convert is not needed in this case.

Multipart models

Meshing models composed of parts with different material groups is a little bit tricky task. But there are some more or less general ways of doing that. Here, the method using functions of Gmsh for periodic meshes will be shown.

The screwdriver handle example is extended by adding a screwdriver shank. The new part is composed of a cylinder trimmed at one end:

 1cyl3 = doc.addObject("Part::Cylinder", "cyl3")
 2cyl3.Radius = 0.3 * radius
 3cyl3.Height = height
 4cyl3.Placement = Base.Placement(Base.Vector(0, 0, height),
 5                                Base.Rotation(0, 0, 0, 1))
 6
 7tip1 = doc.addObject("Part::Box", "tip1")
 8tip1.Length = radius
 9tip1.Width = 2 * radius
10tip1.Height = 3 * radius
11tip1.Placement = Base.Placement(Base.Vector(0, -radius, 1.71 * height),
12                                Base.Rotation(Base.Vector(0, 1, 0), -10),
13                                Base.Vector(0, 0, 3 * radius))
14
15tip2 = doc.addObject("Part::Mirroring", "tip2")
16tip2.Source = tip1
17tip2.Normal = (1, 0, 0)
18
19tip3 = doc.addObject("Part::MultiFuse", "tip3")
20tip3.Shapes = [tip1, tip2]
21
22dif4 = doc.addObject("Part::Cut", "dif4")
23dif4.Base = cyl3
24dif4.Tool = tip3
25
26uni2 = doc.addObject("Part::MultiFuse", "uni2")
27uni2.Shapes = [cyl2, dif4]

The handle and shank are exported to the STEP file as two separated parts:

1doc.recompute()
2
3Part.export([dif3, uni2], 'screwdriver_full.step')
4doc.saveAs('screwdriver_full.FCStd')

The full screwdriver example (handle + shank): screwdriver_full.py.

To create a coincidence mesh on the handle and shank interface, it is necessary to identify the interface surfaces and declare them to be periodic in the GEO file. The identification has to be done manually in the Gmsh graphical interface.

_images/sdf_gmsh1.png _images/sdf_gmsh2.png

The input file for Gmsh is than as follows (screwdriver_full.geo):

 1Merge "screwdriver_full.step";
 2
 3Periodic Surface 5 {7} = 26 {67};
 4Periodic Surface 3 {6, 2, -6, 7} = 27 {68, 69, -68, 67};
 5
 6Physical Volume(1) = {1};
 7Physical Volume(2) = {2};
 8
 9Field[1] = MathEval;
10Field[1].F = "0.0015";
11Background Field = 1;

where the first pair of periodic surfaces corresponds to the common circle faces (bottom of the shank) and the second pair to the common cylindrical surfaces. See Gmsh Reference manual for details on periodic meshing.

Using the above stated GEO file, Gmsh creates a mesh containing duplicate vertices on the handle/shank interface. These duplicate vertices can be removed during the conversion to the VTK format by giving --merge (or just -m) argument to convert_mesh.py script:

sfepy-convert -m screwdriver_full.msh screwdriver_full.vtk

In order to extract the cells by the physical groups use the conversion script with --save-per-mat argument:

sfepy-convert --save-per-mat screwdriver_full.vtk screwdriver.vtk

It produces screwdriver.vtk contaning the original mesh and screwdriver_matid_1.vtk, screwdriver_matid_2.vtk files containing only the cells of a given physical group and all vertices of the original mesh.

_images/sdf_mesh.png

When using OpenSCAD, define the full screwdriver geometry as (screwdriver_full.scad):

 1radius = 0.01;
 2height = 0.1;
 3$fn = 50;
 4
 5module tip() {
 6  rotate([0, -10, 0])
 7    translate([0, -radius, -3*radius])
 8      cube([radius, 2*radius, 3*radius], center=false);
 9}
10
11difference() {
12  difference() {
13    difference() {
14      union() {
15        cylinder(center=false, h=height, r=radius);
16        sphere(radius);
17      };
18      translate([0, 0, 0.9*height])
19        rotate_extrude()
20          polygon([[0.8*radius, 0], [1.8*radius, -0.577*radius], [1.8*radius, 0.577*radius]]);
21    }
22    cylinder(center=false, h=height, r=0.3*radius);
23  }
24  for (i = [1:6]) {
25    rotate([0, 0, 360/6*i])
26      translate([-1.1*radius, 0.0, -0.2*height])
27        cylinder(center=false, h=1.1*height, r=0.2*radius);
28  }
29}
30
31union() {
32  difference() {
33    translate([0, 0, height])
34      cylinder(center=false, h=height, r=0.3*radius);
35    translate([0, 0, 1.71*height + 3*radius])
36      union() {
37        tip();
38        mirror ([1, 0, 0]) tip();
39      }
40  }
41  cylinder(center=false, h=height, r=0.3*radius);
42}

and convert the CSG file to the STEP file by:

1importCSG.open('screwdriver_full.csg')
2top_group = FreeCAD.ActiveDocument.Objects[-1]
3Part.export(top_group.OutList, 'screwdriver_full.step')

Since the different tools for geometry definition have been used, the numbering of geometric objects may differ and the surface and edge numbers have to be changed in the GEO file:

Periodic Surface 5 {6} = 26 {66};
Periodic Surface 3 {5, 2, -5, 6} = 27 {67, 68, -67, 66};

Note: The numbering of objects may vary between FreeCAD, OpenSCAD and Gmsh versions.