Source code for pygem.cad.cad_deformation

"""
Module for generic deformation for CAD file.
"""
import os
import numpy as np
from itertools import product
from OCC.Core.TopoDS import (TopoDS_Shape, topods_Wire, TopoDS_Compound,
                             topods_Face, topods_Edge, TopoDS_Face, TopoDS_Wire)
from OCC.Core.BRep import BRep_Builder
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import (TopAbs_EDGE, TopAbs_FACE, TopAbs_WIRE)
from OCC.Core.TopTools import TopTools_ListOfShape
from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeFace,
                                     BRepBuilderAPI_MakeWire,
                                     BRepBuilderAPI_MakeEdge,
                                     BRepBuilderAPI_NurbsConvert)
from OCC.Core.BRep import BRep_Tool, BRep_Tool_Curve
from OCC.Core.Geom import Geom_BSplineCurve, Geom_BSplineSurface
from OCC.Core.GeomConvert import (geomconvert_SurfaceToBSplineSurface,
                                  geomconvert_CurveToBSplineCurve,
                                  GeomConvert_CompCurveToBSplineCurve)
from OCC.Core.gp import gp_Pnt
from OCC.Core.BRepTools import breptools_OuterWire
from OCC.Core.IFSelect import IFSelect_RetDone
from OCC.Core.Interface import Interface_Static_SetCVal
from OCC.Core.STEPControl import (STEPControl_Writer, STEPControl_Reader,
                                  STEPControl_AsIs)
from OCC.Core.IGESControl import (IGESControl_Writer, IGESControl_Reader,
                                  IGESControl_Controller_Init)


[docs]class CADDeformation(): """ Base class for applting deformation to CAD geometries. :param int u_knots_to_add: the number of knots to add to the NURBS surfaces along `u` direction before the deformation. This parameter is useful whenever the gradient of the imposed deformation present spatial scales that are smaller than the local distance among the original poles of the surface/curve. Enriching the poles will allow for a more accurate application of the deformation, and might also reduce possible mismatches between bordering faces. On the orther hand, it might result in higher computational cost and bigger output files. Default is 0. :param int v_knots_to_add: the number of knots to add to the NURBS surfaces along `v` direction before the deformation. This parameter is useful whenever the gradient of the imposed deformation present spatial scales that are smaller than the local distance among the original poles of the surface/curve. Enriching the poles will allow for a more accurate application of the deformation, and might also reduce possible mismatches between bordering faces. On the orther hand, it might result in higher computational cost and bigger output files. Default is 0. :param int t_knots_to_add: the number of knots to add to the NURBS curves before the deformation. This parameter is useful whenever the gradient of the imposed deformation present spatial scales that are smaller than the local distance among the original poles of the surface/curve. Enriching the poles will allow for a more accurate application of the deformation, and might also reduce possible mismatches between bordering faces. On the orther hand, it might result in higher computational cost and bigger output files. Default is 0. :param float tolerance: the tolerance involved in several internal operations of the procedure (joining wires in a single curve before deformation and placing new poles on curves and surfaces). Change the default value only if the input file scale is significantly different form mm, making some of the aforementioned operations fail. Default is 0.0001. :cvar int u_knots_to_add: the number of knots to add to the NURBS surfaces along `u` direction before the deformation. :cvar int v_knots_to_add: the number of knots to add to the NURBS surfaces along `v` direction before the deformation. :cvar int t_knots_to_add: the number of knots to add to the NURBS curves before the deformation. :cvar float tolerance: the tolerance involved in several internal operations of the procedure (joining wires in a single curve before deformation and placing new poles on curves and surfaces). """ def __init__(self, u_knots_to_add=0, v_knots_to_add=0, t_knots_to_add=0, tolerance=1e-4): self.u_knots_to_add = u_knots_to_add self.v_knots_to_add = v_knots_to_add self.t_knots_to_add = t_knots_to_add self.tolerance = tolerance
[docs] @staticmethod def read_shape(filename): """ Static method to load the `topoDS_Shape` from a file. Supported extensions are: ".iges", ".step". :param str filename: the name of the file containing the geometry. Example: >>> from pygem.cad import CADDeformation as CAD >>> shape = CAD.read_shape('example.iges') """ file_extension = os.path.splitext(filename)[1] av_readers = { '.step': STEPControl_Reader, '.iges': IGESControl_Reader, } reader_class = av_readers.get(file_extension) if reader_class is None: raise ValueError('Unable to open the input file') reader = reader_class() return_reader = reader.ReadFile(filename) # check status if return_reader == IFSelect_RetDone: return_transfer = reader.TransferRoots() if return_transfer: # load all shapes in one shape = reader.OneShape() return shape else: raise RuntimeError("Shapes not loaded.") else: raise RuntimeError("Cannot read the file.")
[docs] @staticmethod def write_shape(filename, shape): """ Static method to save a `topoDS_Shape` object to a file. Supported extensions are: ".iges", ".step". :param str filename: the name of the file where the shape will be saved. Example: >>> from pygem.cad import CADDeformation as CAD >>> CAD.read_shape('example.step', my_shape) """ def write_iges(filename, shape): """ IGES writer """ IGESControl_Controller_Init() writer = IGESControl_Writer() writer.AddShape(shape) writer.Write(filename) def write_step(filename, shape): """ STEP writer """ step_writer = STEPControl_Writer() # Changes write schema to STEP standard AP203 # It is considered the most secure standard for STEP. # *According to PythonOCC documentation (http://www.pythonocc.org/) Interface_Static_SetCVal("write.step.schema", "AP203") Interface_Static_SetCVal('write.surfacecurve.mode','0') step_writer.Transfer(shape, STEPControl_AsIs) step_writer.Write(filename) file_extension = os.path.splitext(filename)[1] av_writers = { '.step': write_step, '.iges': write_iges, } writer = av_writers.get(file_extension) if writer is None: raise ValueError('Unable to open the output file') writer(filename, shape)
[docs] def _bspline_surface_from_face(self, face): """ Private method that takes a TopoDS_Face and transforms it into a Bspline_Surface. :param TopoDS_Face face: the TopoDS_Face to be converted :rtype: Geom_BSplineSurface """ if not isinstance(face, TopoDS_Face): raise TypeError("face must be a TopoDS_Face") # TopoDS_Face converted to Nurbs nurbs_face = topods_Face(BRepBuilderAPI_NurbsConvert(face).Shape()) # GeomSurface obtained from Nurbs face surface = BRep_Tool.Surface(nurbs_face) # surface is now further converted to a bspline surface bspline_surface = geomconvert_SurfaceToBSplineSurface(surface) return bspline_surface
[docs] def _bspline_curve_from_wire(self, wire): """ Private method that takes a TopoDS_Wire and transforms it into a Bspline_Curve. :param TopoDS_Wire wire: the TopoDS_Face to be converted :rtype: Geom_BSplineSurface """ if not isinstance(wire, TopoDS_Wire): raise TypeError("wire must be a TopoDS_Wire") # joining all the wire edges in a single curve here # composite curve builder (can only join Bspline curves) composite_curve_builder = GeomConvert_CompCurveToBSplineCurve() # iterator to edges in the TopoDS_Wire edge_explorer = TopExp_Explorer(wire, TopAbs_EDGE) while edge_explorer.More(): # getting the edge from the iterator edge = topods_Edge(edge_explorer.Current()) # edge can be joined only if it is not degenerated (zero length) if BRep_Tool.Degenerated(edge): edge_explorer.Next() continue # the edge must be converted to Nurbs edge nurbs_converter = BRepBuilderAPI_NurbsConvert(edge) nurbs_converter.Perform(edge) nurbs_edge = topods_Edge(nurbs_converter.Shape()) # here we extract the underlying curve from the Nurbs edge nurbs_curve = BRep_Tool_Curve(nurbs_edge)[0] # we convert the Nurbs curve to Bspline curve bspline_curve = geomconvert_CurveToBSplineCurve(nurbs_curve) # we can now add the Bspline curve to the composite wire curve composite_curve_builder.Add(bspline_curve, self.tolerance) edge_explorer.Next() # GeomCurve obtained by the builder after edges are joined comp_curve = composite_curve_builder.BSplineCurve() return comp_curve
[docs] def _enrich_curve_knots(self, bsp_curve): """ Private method that adds `self.t_knots_to_add` poles to the passed curve. :param Geom_BSplineCurve bsp_curve: the NURBS curve to enrich """ if not isinstance(bsp_curve, Geom_BSplineCurve): raise TypeError("bsp_curve must be a Geom_BSplineCurve") # number of knots is enriched here, if required, to # enhance precision # start parameter of composite curve first_param = bsp_curve.FirstParameter() # end parameter of composite curve last_param = bsp_curve.LastParameter() for i in range(self.t_knots_to_add): bsp_curve.InsertKnot(first_param+ \ i*(last_param-first_param)/self.t_knots_to_add, 1, \ self.tolerance)
[docs] def _enrich_surface_knots(self, bsp_surface): """ Private method that adds `self.u_knots_to_add` and `self.v_knots_to_add` knots to the input surface `bsp_surface`, in u and v direction respectively. :param Geom_BSplineCurve bsp_surface: the NURBS surface to enrich """ if not isinstance(bsp_surface, Geom_BSplineSurface): raise TypeError("bsp_surface must be a Geom_BSplineSurface") # we will add the prescribed amount of nodes # both along u and v parametric directions # bounds (in surface parametric space) of the surface bounds = bsp_surface.Bounds() for i in range(self.u_knots_to_add): bsp_surface.InsertUKnot(bounds[0]+ \ i*(bounds[1]-bounds[0])/self.u_knots_to_add, 1, self.tolerance) for i in range(self.v_knots_to_add): bsp_surface.InsertVKnot(bounds[2]+ \ i*(bounds[3]-bounds[2])/self.v_knots_to_add, 1, self.tolerance)
[docs] def _pole_get_components(self, pole): """ Extract component from gp_Pnt """ return pole.X(), pole.Y(), pole.Z()
[docs] def _pole_set_components(self, components): """ Return a gp_Pnt with the passed components """ assert len(components) == 3 return gp_Pnt(*components)
[docs] def _deform_bspline_curve(self, bsp_curve): """ Private method that deforms the control points of `bsp_curve` using the inherited method. All the changes are performed in place. :param Geom_Bspline_Curve bsp_curve: the curve to deform """ if not isinstance(bsp_curve, Geom_BSplineCurve): raise TypeError("bsp_curve must be a Geom_BSplineCurve") # we first extract the poles of the curve poles number n_poles = bsp_curve.NbPoles() # array containing the poles coordinates poles_coordinates = np.zeros(shape=(n_poles, 3)) # cycle over the poles to get their coordinates for pole_id in range(n_poles): # gp_Pnt corresponding to the pole pole = bsp_curve.Pole(pole_id + 1) poles_coordinates[pole_id, :] = self._pole_get_components(pole) # the new poles positions are computed through FFD new_pts = super().__call__(poles_coordinates) # the Bspline curve is now looped again to # set the poles positions to new_points for pole_id in range(n_poles): new_pole = self._pole_set_components(new_pts[pole_id, :]) bsp_curve.SetPole(pole_id + 1, new_pole)
[docs] def _deform_bspline_surface(self, bsp_surface): """ Private method that deforms the control points of `surface` using the inherited method. All the changes are performed in place. :param Geom_Bspline_Surface bsp_surface: the surface to deform """ if not isinstance(bsp_surface, Geom_BSplineSurface): raise TypeError("bsp_surface must be a Geom_BSplineSurface") # we first extract the poles of the curve # number of poles in u direction n_poles_u = bsp_surface.NbUPoles() # number of poles in v direction n_poles_v = bsp_surface.NbVPoles() # array which will contain the coordinates of the poles poles_coordinates = np.zeros(shape=(n_poles_u * n_poles_v, 3)) # cycle over the poles to get their coordinates pole_ids = product(range(n_poles_u), range(n_poles_v)) for pole_id, (u, v) in enumerate(pole_ids): pole = bsp_surface.Pole(u + 1, v + 1) poles_coordinates[pole_id, :] = self._pole_get_components(pole) # the new poles positions are computed through FFD new_pts = super().__call__(poles_coordinates) # the surface is now looped again to set the new poles positions pole_ids = product(range(n_poles_u), range(n_poles_v)) for pole_id, (u, v) in enumerate(pole_ids): new_pole = self._pole_set_components(new_pts[pole_id, :]) bsp_surface.SetPole(u + 1, v + 1, new_pole)
[docs] def __call__(self, obj, dst=None): """ This method performs the deformation on the CAD geometry. If `obj` is a TopoDS_Shape, the method returns a TopoDS_Shape containing the deformed geometry. If `obj` is a filename, the method deforms the geometry contained in the file and writes the deformed shape to `dst` (which has to be set). :param obj: the input geometry. :type obj: str or TopoDS_Shape :param str dst: if `obj` is a string containing the input filename, `dst` refers to the file where the deformed geometry is saved. """ # Manage input if isinstance(obj, str): # if a input filename is passed if dst is None: raise ValueError( 'Source file is provided, but no destination specified') shape = self.read_shape(obj) elif isinstance(obj, TopoDS_Shape): shape = obj # Maybe do we need to handle also Compound? else: raise TypeError #create compound to store modified faces compound_builder = BRep_Builder() compound = TopoDS_Compound() compound_builder.MakeCompound(compound) # cycle on the faces to get the control points # iterator to faces (TopoDS_Shape) contained in the shape faces_explorer = TopExp_Explorer(shape, TopAbs_FACE) while faces_explorer.More(): # performing some conversions to get the right # format (BSplineSurface) # TopoDS_Face obtained from iterator face = topods_Face(faces_explorer.Current()) # performing some conversions to get the right # format (BSplineSurface) bspline_surface = self._bspline_surface_from_face(face) # add the required amount of poles in u and v directions self._enrich_surface_knots(bspline_surface) # deform the Bspline surface through FFD self._deform_bspline_surface(bspline_surface) # through moving the control points, we now changed the SURFACE # underlying FACE we are processing. we now need to obtain the # curves (actually, the WIRES) that define the bounds of the # surface and TRIM the surface with them, to obtain the new FACE #we now start really looping on the wires #we will create a single curve joining all the edges in the wire # the curve must be a bspline curve so we need to make conversions # through all the way # list that will contain the (single) outer wire of the face outer_wires = [] # list that will contain all the inner wires (holes) of the face inner_wires = [] # iterator to loop over TopoDS_Wire in the original (undeformed) # face wire_explorer = TopExp_Explorer(face, TopAbs_WIRE) while wire_explorer.More(): # wire obtained from the iterator wire = topods_Wire(wire_explorer.Current()) # getting a bpline curve joining all the edges of the wire composite_curve = self._bspline_curve_from_wire(wire) # adding all the required knots to the Bspline curve self._enrich_curve_knots(composite_curve) # deforming the Bspline curve through FFD self._deform_bspline_curve(composite_curve) # the GeomCurve corresponding to the whole edge has now # been deformed. Now we must make it become an proper # wire # list of shapes (needed by the wire generator) shapes_list = TopTools_ListOfShape() # edge (to be converted to wire) obtained from the modified # Bspline curve modified_composite_edge = \ BRepBuilderAPI_MakeEdge(composite_curve).Edge() # modified edge is added to shapes_list shapes_list.Append(modified_composite_edge) # wire builder wire_maker = BRepBuilderAPI_MakeWire() wire_maker.Add(shapes_list) # deformed wire is finally obtained modified_wire = wire_maker.Wire() # now, the wire can be outer or inner. we store the outer # and (possible) inner ones in different lists # this is because we first need to trim the surface # using the outer wire, and then we can trim it # with the wires corresponding to all the holes. # the wire order is important, in the trimming process if wire == breptools_OuterWire(face): outer_wires.append(modified_wire) else: inner_wires.append(modified_wire) wire_explorer.Next() # so once we finished looping on all the wires to modify them, # we first use the only outer one to trim the surface # face builder object face_maker = BRepBuilderAPI_MakeFace(bspline_surface, outer_wires[0]) # and then add all other inner wires for the holes for inner_wire in inner_wires: face_maker.Add(inner_wire) # finally, we get our trimmed face with all its holes trimmed_modified_face = face_maker.Face() # trimmed_modified_face is added to the modified faces compound compound_builder.Add(compound, trimmed_modified_face) # and move to the next face faces_explorer.Next() ## END SURFACES ################################################# if isinstance(dst, str): # if a input filename is passed # save the shape exactly to the filename, aka `dst` self.write_shape(dst, compound) else: return compound