From 7c8b9a4b2c0dfa0c7704fcbb71c282ca9925e93f Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Tue, 20 Dec 2016 18:12:08 +0100 Subject: [PATCH] Part: boolean splitt tools, remove trailing whitspaces --- src/Mod/Part/BOPTools/GeneralFuseResult.py | 180 ++++++++++----------- src/Mod/Part/BOPTools/JoinAPI.py | 55 ++++--- src/Mod/Part/BOPTools/ShapeMerge.py | 80 ++++----- src/Mod/Part/BOPTools/SplitAPI.py | 26 +-- src/Mod/Part/BOPTools/SplitFeatures.py | 6 +- src/Mod/Part/BOPTools/Utils.py | 31 ++-- src/Mod/Part/BOPTools/__init__.py | 8 +- 7 files changed, 192 insertions(+), 194 deletions(-) diff --git a/src/Mod/Part/BOPTools/GeneralFuseResult.py b/src/Mod/Part/BOPTools/GeneralFuseResult.py index b5b34dcd5..6bc4a9752 100644 --- a/src/Mod/Part/BOPTools/GeneralFuseResult.py +++ b/src/Mod/Part/BOPTools/GeneralFuseResult.py @@ -31,15 +31,15 @@ from .Utils import HashableShape, HashableShape_Deep, FrozenClass class GeneralFuseResult(FrozenClass): - """class GeneralFuseResult: helper object for obtaining info from results of - Part.Shape.generalFuse() method. - + """class GeneralFuseResult: helper object for obtaining info from results of + Part.Shape.generalFuse() method. + Usage: def myCustomFusionRoutine(list_of_shapes): generalFuse_return = list_of_shapes[0].generalFuse(list_of_shapes[1:]) ao = GeneralFuseResult(list_of_shapes, generalFuse_return) ... (use attributes and methods of ao) ...""" - + def __define_attributes(self): self.gfa_return = None #stores the data returned by generalFuse, supplied to class constructor @@ -51,24 +51,24 @@ def myCustomFusionRoutine(list_of_shapes): self._pieces_of_source = [] #list of pieces (indexes) generated from a source shape, by index of source shape. List of lists of ints. self._sources_of_piece = [] #list of source shapes (indexes) the piece came from, by index of piece. List of lists of ints. - - self._element_to_source = {} #dictionary for finding, which source shapes did an element of pieces come from. key = HashableShape (element). Value = set of ints - + + self._element_to_source = {} #dictionary for finding, which source shapes did an element of pieces come from. key = HashableShape (element). Value = set of ints + self._freeze() - + def __init__(self, source_shapes, generalFuse_return): self.__define_attributes() - + self.gfa_return = generalFuse_return self.source_shapes = source_shapes self.parse() def parse(self): - """Parses the result of generalFuse recorded into self.gfa_return. Recovers missing + """Parses the result of generalFuse recorded into self.gfa_return. Recovers missing information. Fills in data structures. - + It is called automatically by class constructor.""" - + #save things to be parsed and wipe out all other data gfa_return = self.gfa_return source_shapes = self.source_shapes @@ -76,10 +76,10 @@ def myCustomFusionRoutine(list_of_shapes): self.gfa_return = gfa_return self.source_shapes = source_shapes # and start filling in data structures... - + compound, map = self.gfa_return self.pieces = compound.childShapes() - + # create piece shape index for iPiece in range(len(self.pieces)): ha_piece = HashableShape(self.pieces[iPiece]) @@ -87,20 +87,20 @@ def myCustomFusionRoutine(list_of_shapes): self._piece_to_index[ha_piece] = iPiece else: raise ValueError("GeneralFuseAnalyzer.parse: duplicate piece shape detected.") - # create source shape index + # create source shape index for iSource in range(len(self.source_shapes)): ha_source = HashableShape(self.source_shapes[iSource]) if not ha_source in self._source_to_index: self._source_to_index[ha_source] = iSource else: raise ValueError("GeneralFuseAnalyzer.parse: duplicate source shape detected.") - + #test if map has missing entries map_needs_repairing = False for iSource in range(len(map)): if len(map[iSource]) == 0: map_needs_repairing = True - + if map_needs_repairing: aggregate_types = set(["Wire","Shell","CompSolid","Compound"]) nonaggregate_types = set(["Vertex","Edge","Face","Solid"]) @@ -108,15 +108,15 @@ def myCustomFusionRoutine(list_of_shapes): types = set() for piece in self.pieces: types.add(piece.ShapeType) - + types_to_extract = types.intersection(nonaggregate_types) - extractor = lambda(sh):( + extractor = lambda(sh):( (sh.Vertexes if "Vertex" in types_to_extract else []) + (sh.Edges if "Edge" in types_to_extract else []) + (sh.Faces if "Face" in types_to_extract else []) + (sh.Solids if "Solid" in types_to_extract else []) ) - + aggregate_sources_indexes = [self.indexOfSource(sh) for sh in self.source_shapes if sh.ShapeType in aggregate_types] aggregate_pieces = [sh for sh in self.pieces if sh.ShapeType in aggregate_types] assert(len(aggregate_sources_indexes) == len(aggregate_pieces)) @@ -132,13 +132,13 @@ def myCustomFusionRoutine(list_of_shapes): #print "found piece {num} in compound {numc}".format(num= iPiece, numc= i_aggregate) if not map[iSource][-1].isSame(self.pieces[iPiece]): map[iSource].append(self.pieces[iPiece]) - + # check the map was recovered successfully for iSource in range(len(map)): if len(map[iSource]) == 0: import FreeCAD as App - App.Console.PrintWarning("Map entry {num} is empty. Source-to-piece correspondence information is probably incomplete.".format(num= iSource)) - + App.Console.PrintWarning("Map entry {num} is empty. Source-to-piece correspondence information is probably incomplete.".format(num= iSource)) + self._pieces_of_source = [[] for i in range(len(self.source_shapes))] self._sources_of_piece = [[] for i in range(len(self.pieces))] assert(len(map) == len(self.source_shapes)) @@ -148,14 +148,14 @@ def myCustomFusionRoutine(list_of_shapes): iPiece = self.indexOfPiece(piece) self._sources_of_piece[iPiece].append(iSource) self._pieces_of_source[iSource].append(iPiece) - + def parse_elements(self): - """Fills element-to-source map. Potentially slow, so separated from general parse. + """Fills element-to-source map. Potentially slow, so separated from general parse. Needed for splitAggregates; called automatically from splitAggregates.""" - + if len(self._element_to_source)>0: return #already parsed. - + for iPiece in range(len(self.pieces)): piece = self.pieces[iPiece] for element in piece.Vertexes + piece.Edges + piece.Faces + piece.Solids: @@ -164,79 +164,79 @@ def myCustomFusionRoutine(list_of_shapes): self._element_to_source[el_h].update(set(self._sources_of_piece[iPiece])) else: self._element_to_source[el_h] = set(self._sources_of_piece[iPiece]) - + def indexOfPiece(self, piece_shape): "indexOfPiece(piece_shape): returns index of piece_shape in list of pieces" return self._piece_to_index[HashableShape(piece_shape)] def indexOfSource(self, source_shape): "indexOfSource(source_shape): returns index of source_shape in list of arguments" return self._source_to_index[HashableShape(source_shape)] - + def piecesFromSource(self, source_shape): - """piecesFromSource(source_shape): returns list of pieces (shapes) that came from + """piecesFromSource(source_shape): returns list of pieces (shapes) that came from given source shape. - - Note: aggregate pieces (e.g. wire, shell, compound) always have only one source - the - shape they came directly from. Only after executing splitAggregates and + + Note: aggregate pieces (e.g. wire, shell, compound) always have only one source - the + shape they came directly from. Only after executing splitAggregates and explodeCompounds the source lists become completely populated.""" - + ilist = self._pieces_of_source[self.indexOfSource(source_shape)] return [self.pieces[i] for i in ilist] - + def sourcesOfPiece(self, piece_shape): """sourcesOfPiece(piece_shape): returns list of source shapes given piece came from. - - Note: aggregate pieces (e.g. wire, shell, compound) always have only one source - the - shape they came directly from. Only after executing splitAggregates and + + Note: aggregate pieces (e.g. wire, shell, compound) always have only one source - the + shape they came directly from. Only after executing splitAggregates and explodeCompounds the source lists become completely populated.""" ilist = self._sources_of_piece[self.indexOfPiece(piece_shape)] return [self.source_shapes[i] for i in ilist] - + def largestOverlapCount(self): - """largestOverlapCount(self): returns the largest overlap count. For example, if three - spheres intersect and have some volume common to all three, largestOverlapCount - returns 3. - + """largestOverlapCount(self): returns the largest overlap count. For example, if three + spheres intersect and have some volume common to all three, largestOverlapCount + returns 3. + Note: the return value may be incorrect if some of the pieces are wires/shells/ compsolids/compounds. Please use explodeCompounds and splitAggregates before using this function.""" - + return max([len(ilist) for ilist in self._sources_of_piece]) - + def splitAggregates(self, pieces_to_split = None): - """splitAggregates(pieces_to_split = None): splits aggregate shapes (wires, shells, - compsolids) in pieces of GF result as cut by intersections. Also splits aggregates - inside compounds. After running this, 'self' is replaced with new data, where the + """splitAggregates(pieces_to_split = None): splits aggregate shapes (wires, shells, + compsolids) in pieces of GF result as cut by intersections. Also splits aggregates + inside compounds. After running this, 'self' is replaced with new data, where the pieces_to_split are split. - - 'pieces_to_split': list of shapes (from self.pieces), that are to be processed. If + + 'pieces_to_split': list of shapes (from self.pieces), that are to be processed. If None, all pieces will be split if possible. - - Notes: - * this routine is very important to functioning of Connect on shells and wires. + + Notes: + * this routine is very important to functioning of Connect on shells and wires. * Warning: convoluted and slow.""" - + if pieces_to_split is None: pieces_to_split = self.pieces pieces_to_split = [HashableShape(piece) for piece in pieces_to_split] pieces_to_split = set(pieces_to_split) - + self.parse_elements() new_data = GeneralFuseReturnBuilder(self.source_shapes) changed = False - + #split pieces that are not compounds.... for iPiece in range(len(self.pieces)): piece = self.pieces[iPiece] - + if HashableShape(piece) in pieces_to_split: new_pieces = self.makeSplitPieces(piece) - changed = changed or len(new_pieces)>1 + changed = changed or len(new_pieces)>1 for new_piece in new_pieces: new_data.addPiece(new_piece, self._sources_of_piece[iPiece]) else: new_data.addPiece(piece, self._sources_of_piece[iPiece]) - + #split pieces inside compounds #prepare index of existing pieces. existing_pieces = new_data._piece_to_index.copy() @@ -248,20 +248,20 @@ def myCustomFusionRoutine(list_of_shapes): if ret is not None: changed = True new_data.replacePiece(i_new_piece, ret) - + if len(new_data.pieces) > len(self.pieces) or changed: self.gfa_return = new_data.getGFReturn() self.parse() #else: #print "Nothing was split" - + def _splitInCompound(self, compound, existing_pieces): - """Splits aggregates inside compound. Returns None if nothing is split, otherwise + """Splits aggregates inside compound. Returns None if nothing is split, otherwise returns compound. - existing_pieces is a dict. Key is deep hash. Value is tuple (int, shape). It is - used to search for if this split piece was already generated, and re-use the old + existing_pieces is a dict. Key is deep hash. Value is tuple (int, shape). It is + used to search for if this split piece was already generated, and re-use the old one.""" - + changed = False new_children = [] for piece in compound.childShapes(): @@ -274,7 +274,7 @@ def myCustomFusionRoutine(list_of_shapes): changed = True else: new_pieces = self.makeSplitPieces(piece) - changed = changed or len(new_pieces)>1 + changed = changed or len(new_pieces)>1 for new_piece in new_pieces: hash = HashableShape_Deep(new_piece) dummy,ex_piece = existing_pieces.get(hash, (None, None)) @@ -289,14 +289,14 @@ def myCustomFusionRoutine(list_of_shapes): else: return None - + def makeSplitPieces(self, shape): - """makeSplitPieces(self, shape): splits a shell, wire or compsolid into pieces where - it intersects with other shapes. - - Returns list of split pieces. If no splits were done, returns list containing the + """makeSplitPieces(self, shape): splits a shell, wire or compsolid into pieces where + it intersects with other shapes. + + Returns list of split pieces. If no splits were done, returns list containing the original shape.""" - + if shape.ShapeType == "Wire": bit_extractor = lambda(sh): sh.Edges joint_extractor = lambda(sh): sh.Vertexes @@ -309,7 +309,7 @@ def myCustomFusionRoutine(list_of_shapes): else: #can't split the shape return [shape] - + # for each joint, test if all bits it's connected to are from same number of sources. If not, this is a joint for splitting # FIXME: this is slow, and maybe can be optimized splits = [] @@ -331,7 +331,7 @@ def myCustomFusionRoutine(list_of_shapes): return [shape] from . import ShapeMerge - + new_pieces = ShapeMerge.mergeShapes(bit_extractor(shape), split_connections= splits, bool_compsolid= True).childShapes() if len(new_pieces) == 1: #shape was not split (split points found, but the shape remained in one piece). @@ -339,10 +339,10 @@ def myCustomFusionRoutine(list_of_shapes): return new_pieces def explodeCompounds(self): - """explodeCompounds(): if any of self.pieces is a compound, the compound is exploded. - After running this, 'self' is filled with new data, where pieces are updated to + """explodeCompounds(): if any of self.pieces is a compound, the compound is exploded. + After running this, 'self' is filled with new data, where pieces are updated to contain the stuff extracted from compounds.""" - + has_compounds = False for piece in self.pieces: if piece.ShapeType == "Compound": @@ -351,10 +351,10 @@ def myCustomFusionRoutine(list_of_shapes): return from .Utils import compoundLeaves - + new_data = GeneralFuseReturnBuilder(self.source_shapes) new_data.hasher_class = HashableShape #deep hashing not needed here. - + for iPiece in range(len(self.pieces)): piece = self.pieces[iPiece] if piece.ShapeType == "Compound": @@ -372,30 +372,30 @@ class GeneralFuseReturnBuilder(FrozenClass): def __define_attributes(self): self.pieces = [] self._piece_to_index = {} # key = hasher_class(shape). Value = (index_into_self_dot_pieces, shape). Note that GeneralFuseResult uses this item directly. - + self._pieces_from_source = [] #list of list of ints self.source_shapes = [] - + self.hasher_class = HashableShape_Deep - + self._freeze() - + def __init__(self, source_shapes): self.__define_attributes() self.source_shapes = source_shapes self._pieces_from_source = [[] for i in range(len(source_shapes))] - + def addPiece(self, piece_shape, source_shape_index_list): - """addPiece(piece_shape, source_shape_index_list): adds a piece. If the piece + """addPiece(piece_shape, source_shape_index_list): adds a piece. If the piece already exists, returns False, and only updates source<->piece map.""" - + ret = False i_piece_existing = None hash = None if piece_shape.ShapeType != "Compound": # do not catch duplicate compounds hash = self.hasher_class(piece_shape) i_piece_existing, dummy = self._piece_to_index.get(hash, (None, None)) - + if i_piece_existing is None: #adding self.pieces.append(piece_shape) @@ -410,11 +410,11 @@ class GeneralFuseReturnBuilder(FrozenClass): if not i_piece_existing in self._pieces_from_source[iSource]: self._pieces_from_source[iSource].append(i_piece_existing) return ret - + def replacePiece(self, piece_index, new_shape): assert(self.pieces[piece_index].ShapeType == "Compound") assert(new_shape.ShapeType == "Compound") self.pieces[piece_index] = new_shape - + def getGFReturn(self): - return (Part.Compound(self.pieces), [[self.pieces[iPiece] for iPiece in ilist] for ilist in self._pieces_from_source]) \ No newline at end of file + return (Part.Compound(self.pieces), [[self.pieces[iPiece] for iPiece in ilist] for ilist in self._pieces_from_source]) diff --git a/src/Mod/Part/BOPTools/JoinAPI.py b/src/Mod/Part/BOPTools/JoinAPI.py index e6f5b95ff..ea5705081 100644 --- a/src/Mod/Part/BOPTools/JoinAPI.py +++ b/src/Mod/Part/BOPTools/JoinAPI.py @@ -36,9 +36,9 @@ def shapeOfMaxSize(list_of_shapes): """shapeOfMaxSize(list_of_shapes): finds the shape that has the largest mass in the list and returns it. The shapes in the list must be of same dimension.""" #first, check if shapes can be compared by size ShapeMerge.dimensionOfShapes(list_of_shapes) - + rel_precision = 1e-8 - + #find it! max_size = -1e100 # max size encountered so far count_max = 0 # number of shapes with size equal to max_size @@ -56,40 +56,40 @@ def shapeOfMaxSize(list_of_shapes): return shape_max def connect(list_of_shapes, tolerance = 0.0): - """connect(list_of_shapes, tolerance = 0.0): connects solids (walled objects), shells and - wires by throwing off small parts that result when splitting them at intersections. - - Compounds in list_of_shapes are automatically exploded, so self-intersecting compounds + """connect(list_of_shapes, tolerance = 0.0): connects solids (walled objects), shells and + wires by throwing off small parts that result when splitting them at intersections. + + Compounds in list_of_shapes are automatically exploded, so self-intersecting compounds are valid for connect.""" - + # explode all compounds before GFA. new_list_of_shapes = [] for sh in list_of_shapes: new_list_of_shapes.extend( compoundLeaves(sh) ) list_of_shapes = new_list_of_shapes - + #test if shapes are compatible for connecting dim = ShapeMerge.dimensionOfShapes(list_of_shapes) if dim == 0: raise TypeError("Cannot connect vertices!") - + if len(list_of_shapes) < 2: return Part.makeCompound(list_of_shapes) - + if not generalFuseIsAvailable(): #fallback to legacy result = list_of_shapes[0] for i in range(1, len(list_of_shapes)): result = connect_legacy(result, list_of_shapes[i], tolerance) return result - + pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance) ao = GeneralFuseResult(list_of_shapes, (pieces, map)) ao.splitAggregates() #print len(ao.pieces)," pieces total" - + keepers = [] all_danglers = [] # debug - + #add all biggest dangling pieces for src in ao.source_shapes: danglers = [piece for piece in ao.piecesFromSource(src) if len(ao.sourcesOfPiece(piece)) == 1] @@ -103,24 +103,24 @@ def connect(list_of_shapes, tolerance = 0.0): for ii in range(2, ao.largestOverlapCount()+1): list_ii_pieces = [piece for piece in ao.pieces if len(ao.sourcesOfPiece(piece)) == ii] keepers_2_add = [] - for piece in list_ii_pieces: + for piece in list_ii_pieces: if ShapeMerge.isConnected(piece, touch_test_list): keepers_2_add.append(piece) if len(keepers_2_add) == 0: break keepers.extend(keepers_2_add) touch_test_list = Part.Compound(keepers_2_add) - - + + #merge, and we are done! #print len(keepers)," pieces to keep" return ShapeMerge.mergeShapes(keepers) - + def connect_legacy(shape1, shape2, tolerance = 0.0): - """connect_legacy(shape1, shape2, tolerance = 0.0): alternative implementation of - connect, without use of generalFuse. Slow. Provided for backwards compatibility, and + """connect_legacy(shape1, shape2, tolerance = 0.0): alternative implementation of + connect, without use of generalFuse. Slow. Provided for backwards compatibility, and for older OCC.""" - + if tolerance>0.0: import FreeCAD as App App.Console.PrintWarning("connect_legacy does not support tolerance (yet).\n") @@ -134,8 +134,8 @@ def connect_legacy(shape1, shape2, tolerance = 0.0): # (TODO) def embed_legacy(shape_base, shape_tool, tolerance = 0.0): - """embed_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of - embed, without use of generalFuse. Slow. Provided for backwards compatibility, and + """embed_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of + embed, without use of generalFuse. Slow. Provided for backwards compatibility, and for older OCC.""" if tolerance>0.0: import FreeCAD as App @@ -152,12 +152,12 @@ def embed_legacy(shape_base, shape_tool, tolerance = 0.0): elif dim == 1: result = ShapeMerge.mergeShapes(result.Edges) return result - + def cutout_legacy(shape_base, shape_tool, tolerance = 0.0): - """cutout_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of - cutout, without use of generalFuse. Slow. Provided for backwards compatibility, and + """cutout_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of + cutout, without use of generalFuse. Slow. Provided for backwards compatibility, and for older OCC.""" - + if tolerance>0.0: import FreeCAD as App App.Console.PrintWarning("cutout_legacy does not support tolerance (yet).\n") @@ -168,8 +168,7 @@ def cutout_legacy(shape_base, shape_tool, tolerance = 0.0): for sh in shapes_base: result.append(cutout(sh, shape_tool)) return Part.Compound(result) - + shape_base = shapes_base[0] pieces = compoundLeaves(shape_base.cut(shape_tool)) return shapeOfMaxSize(pieces) - diff --git a/src/Mod/Part/BOPTools/ShapeMerge.py b/src/Mod/Part/BOPTools/ShapeMerge.py index 134aa98dd..dd9fa3436 100644 --- a/src/Mod/Part/BOPTools/ShapeMerge.py +++ b/src/Mod/Part/BOPTools/ShapeMerge.py @@ -32,12 +32,12 @@ from .Utils import HashableShape def findSharedElements(shape_list, element_extractor): if len(shape_list) < 2: raise ValueError("findSharedElements: at least two shapes must be provided (have {num})".format(num= len(shape_list))) - + all_elements = [] #list of sets of HashableShapes for shape in shape_list: all_elements.append(set( [HashableShape(sh) for sh in element_extractor(shape)] - )) + )) shared_elements = None for elements in all_elements: if shared_elements is None: @@ -56,22 +56,22 @@ def isConnected(shape1, shape2, shape_dim = -1): return len(findSharedElements([shape1, shape2], extractor))>0 def splitIntoGroupsBySharing(list_of_shapes, element_extractor, split_connections = []): - """splitIntoGroupsBySharing(list_of_shapes, element_type, split_connections = []): find, - which shapes in list_of_shapes are connected into groups by sharing elements. - + """splitIntoGroupsBySharing(list_of_shapes, element_type, split_connections = []): find, + which shapes in list_of_shapes are connected into groups by sharing elements. + element_extractor: function that takes shape as input, and returns list of shapes. - - split_connections: list of shapes to exclude when testing for connections. Use to + + split_connections: list of shapes to exclude when testing for connections. Use to split groups on purpose. - - return: list of lists of shapes. Top-level list is list of groups; bottom level lists + + return: list of lists of shapes. Top-level list is list of groups; bottom level lists enumerate shapes of a group.""" - + split_connections = set([HashableShape(element) for element in split_connections]) - + groups = [] #list of tuples (shapes,elements). Shapes is a list of plain shapes. Elements is a set of HashableShapes - all elements of shapes in the group, excluding split_connections. - - # add shapes to the list of groups, one by one. If not connected to existing groups, + + # add shapes to the list of groups, one by one. If not connected to existing groups, # new group is created. If connected, shape is added to groups, and the groups are joined. for shape in list_of_shapes: shape_elements = set([HashableShape(element) for element in element_extractor(shape)]) @@ -90,19 +90,19 @@ def splitIntoGroupsBySharing(list_of_shapes, element_extractor, split_connection #shape bridges a gap between some groups. Join them into one. #rebuilding list of groups. First, add the new "supergroup", then add the rest groups_new = [] - + supergroup = (list(),set()) for iGroup in connected_to: supergroup[0].extend( groups[iGroup][0] )# merge lists of shapes supergroup[1].update( groups[iGroup][1] )# merge lists of elements groups_new.append(supergroup) - + for iGroup in range(len(groups)): if not iGroup in connected_to: #fixme: inefficient! groups_new.append(groups[iGroup]) groups = groups_new connected_to = [0] - + # add shape to the group it is connected to (if to many, the groups should have been unified by the above code snippet) if len(connected_to) > 0: iGroup = connected_to[0] @@ -111,16 +111,16 @@ def splitIntoGroupsBySharing(list_of_shapes, element_extractor, split_connection else: newgroup = ([shape], shape_elements) groups.append(newgroup) - + # done. Discard unnecessary data and return result. return [shapes for shapes,elements in groups] def mergeSolids(list_of_solids_compsolids, flag_single = False, split_connections = [], bool_compsolid = False): - """mergeSolids(list_of_solids, flag_single = False): merges touching solids that share - faces. If flag_single is True, it is assumed that all solids touch, and output is a - single solid. If flag_single is False, the output is a compound containing all + """mergeSolids(list_of_solids, flag_single = False): merges touching solids that share + faces. If flag_single is True, it is assumed that all solids touch, and output is a + single solid. If flag_single is False, the output is a compound containing all resulting solids. - + Note. CompSolids are treated as lists of solids - i.e., merged into solids.""" solids = [] @@ -128,7 +128,7 @@ def mergeSolids(list_of_solids_compsolids, flag_single = False, split_connection solids.extend(sh.Solids) if flag_single: cs = Part.CompSolid(solids) - return cs if bool_compsolid else Part.makeSolid(cs) + return cs if bool_compsolid else Part.makeSolid(cs) else: if len(solids)==0: return Part.Compound([]) @@ -148,7 +148,7 @@ def mergeShells(list_of_faces_shells, flag_single = False, split_connections = [ else: groups = splitIntoGroupsBySharing(faces, lambda(sh): sh.Edges, split_connections) return Part.makeCompound([Part.Shell(group) for group in groups]) - + def mergeWires(list_of_edges_wires, flag_single = False, split_connections = []): edges = [] for sh in list_of_edges_wires: @@ -158,30 +158,30 @@ def mergeWires(list_of_edges_wires, flag_single = False, split_connections = []) else: groups = splitIntoGroupsBySharing(edges, lambda(sh): sh.Vertexes, split_connections) return Part.makeCompound([Part.Wire(Part.getSortedClusters(group)[0]) for group in groups]) - + def mergeVertices(list_of_vertices, flag_single = False, split_connections = []): # no comprehensive support, just following the footprint of other mergeXXX() return Part.makeCompound(removeDuplicates(list_of_vertices)) def mergeShapes(list_of_shapes, flag_single = False, split_connections = [], bool_compsolid = False): """mergeShapes(list_of_shapes, flag_single = False, split_connections = [], bool_compsolid = False): - merges list of edges/wires into wires, faces/shells into shells, solids/compsolids + merges list of edges/wires into wires, faces/shells into shells, solids/compsolids into solids or compsolids. - + list_of_shapes: shapes to merge. Shapes must share elements in order to be merged. - - flag_single: assume all shapes in list are connected. If False, return is a compound. + + flag_single: assume all shapes in list are connected. If False, return is a compound. If True, return is the single piece (e.g. a shell). - - split_connections: list of shapes that are excluded when searching for connections. - This can be used for example to split a wire in two by supplying vertices where to + + split_connections: list of shapes that are excluded when searching for connections. + This can be used for example to split a wire in two by supplying vertices where to split. If flag_single is True, this argument is ignored. - - bool_compsolid: determines behavior when dealing with solids/compsolids. If True, - result is compsolid/compound of compsolids. If False, all touching solids and - compsolids are unified into single solids. If not merging solids/compsolids, this + + bool_compsolid: determines behavior when dealing with solids/compsolids. If True, + result is compsolid/compound of compsolids. If False, all touching solids and + compsolids are unified into single solids. If not merging solids/compsolids, this argument is ignored.""" - + if len(list_of_shapes)==0: return Part.Compound([]) args = [list_of_shapes, flag_single, split_connections] @@ -209,11 +209,11 @@ def removeDuplicates(list_of_shapes): new_list.append(sh) hashes.add(hash) return new_list - + def dimensionOfShapes(list_of_shapes): - """dimensionOfShapes(list_of_shapes): returns dimension (0D, 1D, 2D, or 3D) of shapes + """dimensionOfShapes(list_of_shapes): returns dimension (0D, 1D, 2D, or 3D) of shapes in the list. If dimension of shapes varies, TypeError is raised.""" - + dimensions = [["Vertex"], ["Edge","Wire"], ["Face","Shell"], ["Solid","CompSolid"]] dim = -1 for sh in list_of_shapes: @@ -224,4 +224,4 @@ def dimensionOfShapes(list_of_shapes): dim = iDim if iDim != dim: raise TypeError("Shapes are of different dimensions ({t1} and {t2}), and cannot be merged or compared.".format(t1= list_of_shapes[0].ShapeType, t2= sht)) - return dim \ No newline at end of file + return dim diff --git a/src/Mod/Part/BOPTools/SplitAPI.py b/src/Mod/Part/BOPTools/SplitAPI.py index af488d535..d155dbcd0 100644 --- a/src/Mod/Part/BOPTools/SplitAPI.py +++ b/src/Mod/Part/BOPTools/SplitAPI.py @@ -33,16 +33,16 @@ from . import Utils import FreeCAD def booleanFragments(list_of_shapes, mode, tolerance = 0.0): - """booleanFragments(list_of_shapes, mode, tolerance = 0.0): functional part of - BooleanFragments feature. It's just result of generalFuse plus a bit of + """booleanFragments(list_of_shapes, mode, tolerance = 0.0): functional part of + BooleanFragments feature. It's just result of generalFuse plus a bit of post-processing. - + mode is a string. It can be "Standard", "Split" or "CompSolid". - "Standard" - return generalFuse as is. - "Split" - wires and shells will be split at intersections. - "CompSolid" - solids will be extracted from result of generelFuse, and compsolids will + "Standard" - return generalFuse as is. + "Split" - wires and shells will be split at intersections. + "CompSolid" - solids will be extracted from result of generelFuse, and compsolids will be made from them; all other stuff is discarded.""" - + pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance) if mode == "Standard": return pieces @@ -61,15 +61,15 @@ def booleanFragments(list_of_shapes, mode, tolerance = 0.0): raise ValueError("Unknown mode: {mode}".format(mode= mode)) def slice(base_shape, tool_shapes, mode, tolerance = 0.0): - """slice(base_shape, tool_shapes, mode, tolerance = 0.0): functional part of + """slice(base_shape, tool_shapes, mode, tolerance = 0.0): functional part of Slice feature. Splits base_shape into pieces based on intersections with tool_shapes. - + mode is a string. It can be "Standard", "Split" or "CompSolid". - "Standard" - return like generalFuse: edges, faces and solids are split, but wires, - shells, compsolids get extra segments but remain in one piece. - "Split" - wires and shells will be split at intersections, too. + "Standard" - return like generalFuse: edges, faces and solids are split, but wires, + shells, compsolids get extra segments but remain in one piece. + "Split" - wires and shells will be split at intersections, too. "CompSolid" - slice a solid and glue it back together to make a compsolid""" - + shapes = [base_shape] + [Part.Compound([tool_shape]) for tool_shape in tool_shapes] # hack: putting tools into compounds will prevent contamination of result with pieces of tools if len(shapes) < 2: raise ValueError("No slicing objects supplied!") diff --git a/src/Mod/Part/BOPTools/SplitFeatures.py b/src/Mod/Part/BOPTools/SplitFeatures.py index 035f7c4e4..1d3db0a1f 100644 --- a/src/Mod/Part/BOPTools/SplitFeatures.py +++ b/src/Mod/Part/BOPTools/SplitFeatures.py @@ -123,7 +123,7 @@ class ViewProviderBooleanFragments: return True def cmdCreateBooleanFragmentsFeature(name, mode): - """cmdCreateBooleanFragmentsFeature(name, mode): implementation of GUI command to create + """cmdCreateBooleanFragmentsFeature(name, mode): implementation of GUI command to create BooleanFragments feature (GFA). Mode can be "Standard", "Split", or "CompSolid".""" sel = FreeCADGui.Selection.getSelectionEx() FreeCAD.ActiveDocument.openTransaction("Create Boolean Fragments") @@ -250,7 +250,7 @@ class ViewProviderSlice: return True def cmdCreateSliceFeature(name, mode): - """cmdCreateSliceFeature(name, mode): implementation of GUI command to create + """cmdCreateSliceFeature(name, mode): implementation of GUI command to create Slice feature. Mode can be "Standard", "Split", or "CompSolid".""" sel = FreeCADGui.Selection.getSelectionEx() FreeCAD.ActiveDocument.openTransaction("Create Slice") @@ -378,7 +378,7 @@ class ViewProviderXOR: return True def cmdCreateXORFeature(name): - """cmdCreateXORFeature(name): implementation of GUI command to create + """cmdCreateXORFeature(name): implementation of GUI command to create XOR feature (GFA). Mode can be "Standard", "Split", or "CompSolid".""" sel = FreeCADGui.Selection.getSelectionEx() FreeCAD.ActiveDocument.openTransaction("Create Boolean XOR") diff --git a/src/Mod/Part/BOPTools/Utils.py b/src/Mod/Part/BOPTools/Utils.py index f615edad7..aaa64a6a1 100644 --- a/src/Mod/Part/BOPTools/Utils.py +++ b/src/Mod/Part/BOPTools/Utils.py @@ -40,7 +40,7 @@ class HashableShape(object): class HashableShape_Deep(object): """Similar to HashableShape, except that the things the shape is composed of are compared. - + Example: >>> wire2 = Part.Wire(wire1.childShapes()) >>> wire2.isSame(wire1) @@ -48,29 +48,29 @@ Example: >>> HashableShape_Deep(wire2) == HashableShape_Deep(wire1) True # <--- made of same set of elements """ - + def __init__(self, shape): self.Shape = shape self.hash = 0 for el in shape.childShapes(): self.hash = self.hash ^ el.hashCode() - + def __eq__(self, other): # avoiding extensive comparison for now. Just doing a few extra tests should reduce the already-low chances of false-positives if self.hash == other.hash: if len(self.Shape.childShapes()) == len(other.Shape.childShapes()): if self.Shape.ShapeType == other.Shape.ShapeType: return True - return False - + return False + def __hash__(self): return self.hash def compoundLeaves(shape_or_compound): """compoundLeaves(shape_or_compound): extracts all non-compound shapes from a nested compound. - Note: shape_or_compound may be a non-compound; then, it is the only thing in the + Note: shape_or_compound may be a non-compound; then, it is the only thing in the returned list.""" - + if shape_or_compound.ShapeType == "Compound": leaves = [] for child in shape_or_compound.childShapes(): @@ -78,23 +78,23 @@ def compoundLeaves(shape_or_compound): return leaves else: return [shape_or_compound] - + def upgradeToAggregateIfNeeded(list_of_shapes, types = None): - """upgradeToAggregateIfNeeded(list_of_shapes, types = None): upgrades non-aggregate type - shapes to aggregate-type shapes if the list has a mix of aggregate and non-aggregate + """upgradeToAggregateIfNeeded(list_of_shapes, types = None): upgrades non-aggregate type + shapes to aggregate-type shapes if the list has a mix of aggregate and non-aggregate type shapes. Returns the new list. Recursively traverses into compounds. - + aggregate shape types are Wire, Shell, CompSolid non-aggregate shape types are Vertex, Edge, Face, Solid - Compounds are something special: they are recursively traversed to upgrade the + Compounds are something special: they are recursively traversed to upgrade the contained shapes. - + Examples: list_of_shapes contains only faces -> nothing happens list_of_shapes contains faces and shells -> faces are converted to shells - + 'types' argument is needed for recursive traversal. Do not supply.""" - + import Part if types is None: types = set() @@ -127,4 +127,3 @@ class FrozenClass(object): def _unfreeze(self): self.__isfrozen = False - diff --git a/src/Mod/Part/BOPTools/__init__.py b/src/Mod/Part/BOPTools/__init__.py index c8cbced1f..468ea8839 100644 --- a/src/Mod/Part/BOPTools/__init__.py +++ b/src/Mod/Part/BOPTools/__init__.py @@ -24,7 +24,7 @@ __title__ = "BOPTools package" __url__ = "http://www.freecadweb.org" __doc__ = """BOPTools Package (part of FreeCAD). Routines that power Connect, Embed, Cutout, -BooleanFragments, Slice and XOR features of Part Workbench. Useful for other custom +BooleanFragments, Slice and XOR features of Part Workbench. Useful for other custom BOP-like operations""" ## @package BOPTools @@ -57,14 +57,14 @@ def reloadAll(): import FreeCAD if FreeCAD.GuiUp: addCommands() - + def addCommands(): "addCommands(): add all GUI commands of BOPTools package to FreeCAD command manager." JoinFeatures.addCommands() SplitFeatures.addCommands() - + def generalFuseIsAvailable(): - """generalFuseIsAvailable(): returns True if FreeCAD's Part.Shape.generalFuse is functional. + """generalFuseIsAvailable(): returns True if FreeCAD's Part.Shape.generalFuse is functional. True if Part.OCC_VERSION >= 6.9.0.""" import Part if not hasattr(Part, "OCC_VERSION"):