Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 48 additions & 64 deletions bladex/blade.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

from .export import (make_compound, sew_shapes, to_solid, write_iges,
write_stl)
from .intepolatedface import InterpolatedFace

class Blade(object):
Expand Down Expand Up @@ -782,65 +784,47 @@ def _write_blade_errors(self, upper_face, lower_face, errors):
output_string += '\n'
f.write(output_string)

def generate_solid(self):
def shell(self, sewing_tolerance=1e-2):
"""
Generate a solid blade assembling the upper face, lower face, tip and
root using the BRepBuilderAPI_MakeSolid algorithm.
Sew the four boundary faces into a single shape.

:param int max_deg: Define the maximal U degree of generated surface.
Default value is 1
:raises RuntimeError: if the assembling of the solid blade is not
completed successfully
:param float sewing_tolerance: tolerance used by the sewing algorithm
:return: a shell when the faces close, otherwise the sewed compound
:rtype: OCC.Core.TopoDS.TopoDS_Shape
"""
from OCC.Display.SimpleGui import init_display
from OCC.Core.TopoDS import TopoDS_Shell
import OCC.Core.TopoDS
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Sewing, \
BRepBuilderAPI_MakeSolid

faces = [
self.upper_face, self.lower_face, self.tip_face, self.root_face
]

sewer = BRepBuilderAPI_Sewing(1e-7)
for face in faces:
sewer.Add(face)
sewer.Perform()
return sew_shapes(
[self.upper_face, self.lower_face, self.root_face,
self.tip_face], sewing_tolerance)

result_shell = sewer.SewedShape()
solid_maker = BRepBuilderAPI_MakeSolid()
solid_maker.Add(OCC.Core.TopoDS.topods.Shell(result_shell))
def generate_solid(self):
"""
Generate a solid blade from its boundary faces.

if not solid_maker.IsDone():
raise RuntimeError('Unsuccessful assembling of solid blade')
result_solid = solid_maker.Solid()
If the four faces do not close into a single shell, the sewed shape is
returned instead: IGES and STL accept both, and this keeps the export
working on degenerate geometries (e.g. a tip collapsing to a point).

return result_solid
:return: the blade as a solid, or as the sewed shape when it cannot be
closed into a single shell
:rtype: OCC.Core.TopoDS.TopoDS_Shape
"""
return to_solid(self.shell())

def export_stl(self, filename, linear_deflection=0.1):
def export_stl(self, filename, linear_deflection=0.05,
angular_deflection=0.3):
"""
Generate and export the .STL file for the entire blade.
This method requires PythonOCC (7.4.0) to be installed.

:param string filename: path (with the file extension) where to store
the .stl CAD for the blade
:param float linear_deflection: relative linear deflection used for the
tessellation; lower values give smoother surfaces
:param float angular_deflection: angular deflection (in radians) used
for the tessellation; lower values give smoother surfaces
"""
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Sewing
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Extend.DataExchange import write_stl_file
from OCC.Core.StlAPI import StlAPI_Writer

sewer = BRepBuilderAPI_Sewing(1e-2)
sewer.Add(self.upper_face)
sewer.Add(self.lower_face)
sewer.Add(self.root_face)
sewer.Add(self.tip_face)
sewer.Perform()
sewed_shape = sewer.SewedShape()

triangulation = BRepMesh_IncrementalMesh(sewed_shape, linear_deflection, True)
triangulation.Perform()

writer = StlAPI_Writer()
writer.SetASCIIMode(False)
writer.Write(sewed_shape, filename)
write_stl(self.shell(), filename, linear_deflection,
angular_deflection)

def _generate_leading_edge_curves(self):
"""
Expand Down Expand Up @@ -894,27 +878,27 @@ def _generate_leading_edge_curves(self):
self.upper_le_edge = BRepBuilderAPI_MakeEdge(upper_curve.Curve()).Edge()
self.lower_le_edge = BRepBuilderAPI_MakeEdge(lower_curve.Curve()).Edge()

def export_iges(self, filename, include_le_curves=False):
def export_iges(self, filename, include_le_curves=False,
sewing_tolerance=1e-2):
"""
Generate and export the .IGES file for the entire blade.
This method requires PythonOCC (7.4.0) to be installed.
"""
from OCC.Core.IGESControl import IGESControl_Writer

The export starts from the sewn shell of the four boundary faces, so
that IGES and STL share the same B-Rep and the sewing tolerance can be
tuned. The leading-edge curves are optional.

:param string filename: path (with the file extension) where to store
the .iges CAD for the blade
:param bool include_le_curves: if True, the upper and lower leading
edge curves are exported together with the body
:param float sewing_tolerance: tolerance used to sew the faces
:raises RuntimeError: if the IGES file cannot be written
"""
shapes = [self.shell(sewing_tolerance)]
if include_le_curves:
self._generate_leading_edge_curves()

iges_writer = IGESControl_Writer()
iges_writer.AddShape(self.upper_face)
iges_writer.AddShape(self.lower_face)
iges_writer.AddShape(self.root_face)
iges_writer.AddShape(self.tip_face)

if include_le_curves:
iges_writer.AddShape(self.upper_le_edge)
iges_writer.AddShape(self.lower_le_edge)

iges_writer.Write(filename)
shapes.extend([self.upper_le_edge, self.lower_le_edge])
write_iges(make_compound(shapes), filename)

@staticmethod
def _check_string(filename):
Expand Down
124 changes: 124 additions & 0 deletions bladex/export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
Shared CAD export utilities.

Everything needed to turn OCC faces into IGES/STL files:
compounds, sewing, solid assembly, tessellation and file writing. Keeping
these operations in one place avoids each class reinventing its own variant.
"""
from OCC.Core.BRep import BRep_Builder
from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeSolid,
BRepBuilderAPI_Sewing)
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.IGESControl import IGESControl_Writer
from OCC.Core.StlAPI import StlAPI_Writer
from OCC.Core.TopAbs import TopAbs_COMPOUND, TopAbs_SHELL
from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Iterator, topods


def make_compound(shapes):
"""Group a list of shapes into a single TopoDS_Compound."""
builder = BRep_Builder()
comp = TopoDS_Compound()
builder.MakeCompound(comp)

for shape in shapes:
if shape is None:
raise ValueError("make_compound: shape is None")
builder.Add(comp, shape)

return comp


def sew_shapes(shapes, tolerance=1e-2):
"""Sew a list of faces/shapes into a single shape (shell or compound)."""
sewer = BRepBuilderAPI_Sewing(tolerance)
for shape in shapes:
if shape is None:
raise ValueError(
"sew_shapes: shape is None (call Blade.build first)")
sewer.Add(shape)
sewer.Perform()
return sewer.SewedShape()


def to_solid(shape):
"""Convert `shape` to a solid when it contains exactly one shell.

If the sewing did not produce a single shell (degenerate geometry, open
edges, ...) the input shape is returned unchanged: IGES and STL accept it,
while the forced cast to TopoDS_Shell used before raised
Standard_TypeMismatch and made the export fail.
"""
shell = _single_shell(shape)
if shell is None:
return shape

maker = BRepBuilderAPI_MakeSolid()
maker.Add(shell)
if not maker.IsDone():
return shape
return maker.Solid()


def _single_shell(shape):
"""Return the shell of `shape` when there is exactly one, None otherwise.

Compounds are inspected recursively, so a compound wrapping a compound
that wraps a single shell still resolves to that shell. Any other
sub-shape (a face, an edge, a second body) makes the shape ineligible, so
that nothing is silently dropped when converting to a solid.
"""
if shape.ShapeType() == TopAbs_SHELL:
return topods.Shell(shape)

if shape.ShapeType() != TopAbs_COMPOUND:
return None

shells = []
iterator = TopoDS_Iterator(shape)
while iterator.More():
child = iterator.Value()
if child.ShapeType() == TopAbs_SHELL:
shells.append(topods.Shell(child))
elif child.ShapeType() == TopAbs_COMPOUND:
nested = _single_shell(child)
if nested is None:
return None
shells.append(nested)
else:
return None
iterator.Next()
return shells[0] if len(shells) == 1 else None


def mesh(shape,
linear_deflection=0.05,
angular_deflection=0.3,
is_relative=True):
"""Tessellate `shape` with the incremental mesh algorithm."""
triangulation = BRepMesh_IncrementalMesh(shape, linear_deflection,
is_relative, angular_deflection,
True)
triangulation.Perform()


def write_stl(shape,
filename,
linear_deflection=0.05,
angular_deflection=0.3,
is_relative=True):
"""Write a binary STL file and check the writer status."""
mesh(shape, linear_deflection, angular_deflection, is_relative)

writer = StlAPI_Writer()
writer.SetASCIIMode(False)
if not writer.Write(shape, filename):
raise RuntimeError("Unsuccessful writing of STL file: " + filename)


def write_iges(shape, filename):
"""Write an IGES file and check the writer status."""
writer = IGESControl_Writer()
writer.AddShape(shape)
if not writer.Write(filename):
raise RuntimeError("Unsuccessful writing of IGES file: " + filename)
Loading