Part: boolean splitt tools, remove trailing whitspaces

This commit is contained in:
Bernd Hahnebach 2016-12-20 18:12:08 +01:00 committed by Yorik van Havre
parent 6378ad3aa7
commit 7c8b9a4b2c
7 changed files with 192 additions and 194 deletions

View File

@ -31,15 +31,15 @@ from .Utils import HashableShape, HashableShape_Deep, FrozenClass
class GeneralFuseResult(FrozenClass): class GeneralFuseResult(FrozenClass):
"""class GeneralFuseResult: helper object for obtaining info from results of """class GeneralFuseResult: helper object for obtaining info from results of
Part.Shape.generalFuse() method. Part.Shape.generalFuse() method.
Usage: Usage:
def myCustomFusionRoutine(list_of_shapes): def myCustomFusionRoutine(list_of_shapes):
generalFuse_return = list_of_shapes[0].generalFuse(list_of_shapes[1:]) generalFuse_return = list_of_shapes[0].generalFuse(list_of_shapes[1:])
ao = GeneralFuseResult(list_of_shapes, generalFuse_return) ao = GeneralFuseResult(list_of_shapes, generalFuse_return)
... (use attributes and methods of ao) ...""" ... (use attributes and methods of ao) ..."""
def __define_attributes(self): def __define_attributes(self):
self.gfa_return = None #stores the data returned by generalFuse, supplied to class constructor 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._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._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() self._freeze()
def __init__(self, source_shapes, generalFuse_return): def __init__(self, source_shapes, generalFuse_return):
self.__define_attributes() self.__define_attributes()
self.gfa_return = generalFuse_return self.gfa_return = generalFuse_return
self.source_shapes = source_shapes self.source_shapes = source_shapes
self.parse() self.parse()
def parse(self): 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. information. Fills in data structures.
It is called automatically by class constructor.""" It is called automatically by class constructor."""
#save things to be parsed and wipe out all other data #save things to be parsed and wipe out all other data
gfa_return = self.gfa_return gfa_return = self.gfa_return
source_shapes = self.source_shapes source_shapes = self.source_shapes
@ -76,10 +76,10 @@ def myCustomFusionRoutine(list_of_shapes):
self.gfa_return = gfa_return self.gfa_return = gfa_return
self.source_shapes = source_shapes self.source_shapes = source_shapes
# and start filling in data structures... # and start filling in data structures...
compound, map = self.gfa_return compound, map = self.gfa_return
self.pieces = compound.childShapes() self.pieces = compound.childShapes()
# create piece shape index # create piece shape index
for iPiece in range(len(self.pieces)): for iPiece in range(len(self.pieces)):
ha_piece = HashableShape(self.pieces[iPiece]) ha_piece = HashableShape(self.pieces[iPiece])
@ -87,20 +87,20 @@ def myCustomFusionRoutine(list_of_shapes):
self._piece_to_index[ha_piece] = iPiece self._piece_to_index[ha_piece] = iPiece
else: else:
raise ValueError("GeneralFuseAnalyzer.parse: duplicate piece shape detected.") raise ValueError("GeneralFuseAnalyzer.parse: duplicate piece shape detected.")
# create source shape index # create source shape index
for iSource in range(len(self.source_shapes)): for iSource in range(len(self.source_shapes)):
ha_source = HashableShape(self.source_shapes[iSource]) ha_source = HashableShape(self.source_shapes[iSource])
if not ha_source in self._source_to_index: if not ha_source in self._source_to_index:
self._source_to_index[ha_source] = iSource self._source_to_index[ha_source] = iSource
else: else:
raise ValueError("GeneralFuseAnalyzer.parse: duplicate source shape detected.") raise ValueError("GeneralFuseAnalyzer.parse: duplicate source shape detected.")
#test if map has missing entries #test if map has missing entries
map_needs_repairing = False map_needs_repairing = False
for iSource in range(len(map)): for iSource in range(len(map)):
if len(map[iSource]) == 0: if len(map[iSource]) == 0:
map_needs_repairing = True map_needs_repairing = True
if map_needs_repairing: if map_needs_repairing:
aggregate_types = set(["Wire","Shell","CompSolid","Compound"]) aggregate_types = set(["Wire","Shell","CompSolid","Compound"])
nonaggregate_types = set(["Vertex","Edge","Face","Solid"]) nonaggregate_types = set(["Vertex","Edge","Face","Solid"])
@ -108,15 +108,15 @@ def myCustomFusionRoutine(list_of_shapes):
types = set() types = set()
for piece in self.pieces: for piece in self.pieces:
types.add(piece.ShapeType) types.add(piece.ShapeType)
types_to_extract = types.intersection(nonaggregate_types) types_to_extract = types.intersection(nonaggregate_types)
extractor = lambda(sh):( extractor = lambda(sh):(
(sh.Vertexes if "Vertex" in types_to_extract else []) (sh.Vertexes if "Vertex" in types_to_extract else [])
+ (sh.Edges if "Edge" in types_to_extract else []) + (sh.Edges if "Edge" in types_to_extract else [])
+ (sh.Faces if "Face" in types_to_extract else []) + (sh.Faces if "Face" in types_to_extract else [])
+ (sh.Solids if "Solid" 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_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] aggregate_pieces = [sh for sh in self.pieces if sh.ShapeType in aggregate_types]
assert(len(aggregate_sources_indexes) == len(aggregate_pieces)) 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) #print "found piece {num} in compound {numc}".format(num= iPiece, numc= i_aggregate)
if not map[iSource][-1].isSame(self.pieces[iPiece]): if not map[iSource][-1].isSame(self.pieces[iPiece]):
map[iSource].append(self.pieces[iPiece]) map[iSource].append(self.pieces[iPiece])
# check the map was recovered successfully # check the map was recovered successfully
for iSource in range(len(map)): for iSource in range(len(map)):
if len(map[iSource]) == 0: if len(map[iSource]) == 0:
import FreeCAD as App 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._pieces_of_source = [[] for i in range(len(self.source_shapes))]
self._sources_of_piece = [[] for i in range(len(self.pieces))] self._sources_of_piece = [[] for i in range(len(self.pieces))]
assert(len(map) == len(self.source_shapes)) assert(len(map) == len(self.source_shapes))
@ -148,14 +148,14 @@ def myCustomFusionRoutine(list_of_shapes):
iPiece = self.indexOfPiece(piece) iPiece = self.indexOfPiece(piece)
self._sources_of_piece[iPiece].append(iSource) self._sources_of_piece[iPiece].append(iSource)
self._pieces_of_source[iSource].append(iPiece) self._pieces_of_source[iSource].append(iPiece)
def parse_elements(self): 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.""" Needed for splitAggregates; called automatically from splitAggregates."""
if len(self._element_to_source)>0: if len(self._element_to_source)>0:
return #already parsed. return #already parsed.
for iPiece in range(len(self.pieces)): for iPiece in range(len(self.pieces)):
piece = self.pieces[iPiece] piece = self.pieces[iPiece]
for element in piece.Vertexes + piece.Edges + piece.Faces + piece.Solids: 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])) self._element_to_source[el_h].update(set(self._sources_of_piece[iPiece]))
else: else:
self._element_to_source[el_h] = set(self._sources_of_piece[iPiece]) self._element_to_source[el_h] = set(self._sources_of_piece[iPiece])
def indexOfPiece(self, piece_shape): def indexOfPiece(self, piece_shape):
"indexOfPiece(piece_shape): returns index of piece_shape in list of pieces" "indexOfPiece(piece_shape): returns index of piece_shape in list of pieces"
return self._piece_to_index[HashableShape(piece_shape)] return self._piece_to_index[HashableShape(piece_shape)]
def indexOfSource(self, source_shape): def indexOfSource(self, source_shape):
"indexOfSource(source_shape): returns index of source_shape in list of arguments" "indexOfSource(source_shape): returns index of source_shape in list of arguments"
return self._source_to_index[HashableShape(source_shape)] return self._source_to_index[HashableShape(source_shape)]
def piecesFromSource(self, 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. given source shape.
Note: aggregate pieces (e.g. wire, shell, compound) always have only one source - the Note: aggregate pieces (e.g. wire, shell, compound) always have only one source - the
shape they came directly from. Only after executing splitAggregates and shape they came directly from. Only after executing splitAggregates and
explodeCompounds the source lists become completely populated.""" explodeCompounds the source lists become completely populated."""
ilist = self._pieces_of_source[self.indexOfSource(source_shape)] ilist = self._pieces_of_source[self.indexOfSource(source_shape)]
return [self.pieces[i] for i in ilist] return [self.pieces[i] for i in ilist]
def sourcesOfPiece(self, piece_shape): def sourcesOfPiece(self, piece_shape):
"""sourcesOfPiece(piece_shape): returns list of source shapes given piece came from. """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 Note: aggregate pieces (e.g. wire, shell, compound) always have only one source - the
shape they came directly from. Only after executing splitAggregates and shape they came directly from. Only after executing splitAggregates and
explodeCompounds the source lists become completely populated.""" explodeCompounds the source lists become completely populated."""
ilist = self._sources_of_piece[self.indexOfPiece(piece_shape)] ilist = self._sources_of_piece[self.indexOfPiece(piece_shape)]
return [self.source_shapes[i] for i in ilist] return [self.source_shapes[i] for i in ilist]
def largestOverlapCount(self): def largestOverlapCount(self):
"""largestOverlapCount(self): returns the largest overlap count. For example, if three """largestOverlapCount(self): returns the largest overlap count. For example, if three
spheres intersect and have some volume common to all three, largestOverlapCount spheres intersect and have some volume common to all three, largestOverlapCount
returns 3. returns 3.
Note: the return value may be incorrect if some of the pieces are wires/shells/ 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.""" compsolids/compounds. Please use explodeCompounds and splitAggregates before using this function."""
return max([len(ilist) for ilist in self._sources_of_piece]) return max([len(ilist) for ilist in self._sources_of_piece])
def splitAggregates(self, pieces_to_split = None): def splitAggregates(self, pieces_to_split = None):
"""splitAggregates(pieces_to_split = None): splits aggregate shapes (wires, shells, """splitAggregates(pieces_to_split = None): splits aggregate shapes (wires, shells,
compsolids) in pieces of GF result as cut by intersections. Also splits aggregates 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 inside compounds. After running this, 'self' is replaced with new data, where the
pieces_to_split are split. 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. None, all pieces will be split if possible.
Notes: Notes:
* this routine is very important to functioning of Connect on shells and wires. * this routine is very important to functioning of Connect on shells and wires.
* Warning: convoluted and slow.""" * Warning: convoluted and slow."""
if pieces_to_split is None: if pieces_to_split is None:
pieces_to_split = self.pieces pieces_to_split = self.pieces
pieces_to_split = [HashableShape(piece) for piece in pieces_to_split] pieces_to_split = [HashableShape(piece) for piece in pieces_to_split]
pieces_to_split = set(pieces_to_split) pieces_to_split = set(pieces_to_split)
self.parse_elements() self.parse_elements()
new_data = GeneralFuseReturnBuilder(self.source_shapes) new_data = GeneralFuseReturnBuilder(self.source_shapes)
changed = False changed = False
#split pieces that are not compounds.... #split pieces that are not compounds....
for iPiece in range(len(self.pieces)): for iPiece in range(len(self.pieces)):
piece = self.pieces[iPiece] piece = self.pieces[iPiece]
if HashableShape(piece) in pieces_to_split: if HashableShape(piece) in pieces_to_split:
new_pieces = self.makeSplitPieces(piece) 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: for new_piece in new_pieces:
new_data.addPiece(new_piece, self._sources_of_piece[iPiece]) new_data.addPiece(new_piece, self._sources_of_piece[iPiece])
else: else:
new_data.addPiece(piece, self._sources_of_piece[iPiece]) new_data.addPiece(piece, self._sources_of_piece[iPiece])
#split pieces inside compounds #split pieces inside compounds
#prepare index of existing pieces. #prepare index of existing pieces.
existing_pieces = new_data._piece_to_index.copy() existing_pieces = new_data._piece_to_index.copy()
@ -248,20 +248,20 @@ def myCustomFusionRoutine(list_of_shapes):
if ret is not None: if ret is not None:
changed = True changed = True
new_data.replacePiece(i_new_piece, ret) new_data.replacePiece(i_new_piece, ret)
if len(new_data.pieces) > len(self.pieces) or changed: if len(new_data.pieces) > len(self.pieces) or changed:
self.gfa_return = new_data.getGFReturn() self.gfa_return = new_data.getGFReturn()
self.parse() self.parse()
#else: #else:
#print "Nothing was split" #print "Nothing was split"
def _splitInCompound(self, compound, existing_pieces): 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. returns compound.
existing_pieces is a dict. Key is deep hash. Value is tuple (int, shape). It is 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 used to search for if this split piece was already generated, and re-use the old
one.""" one."""
changed = False changed = False
new_children = [] new_children = []
for piece in compound.childShapes(): for piece in compound.childShapes():
@ -274,7 +274,7 @@ def myCustomFusionRoutine(list_of_shapes):
changed = True changed = True
else: else:
new_pieces = self.makeSplitPieces(piece) 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: for new_piece in new_pieces:
hash = HashableShape_Deep(new_piece) hash = HashableShape_Deep(new_piece)
dummy,ex_piece = existing_pieces.get(hash, (None, None)) dummy,ex_piece = existing_pieces.get(hash, (None, None))
@ -289,14 +289,14 @@ def myCustomFusionRoutine(list_of_shapes):
else: else:
return None return None
def makeSplitPieces(self, shape): def makeSplitPieces(self, shape):
"""makeSplitPieces(self, shape): splits a shell, wire or compsolid into pieces where """makeSplitPieces(self, shape): splits a shell, wire or compsolid into pieces where
it intersects with other shapes. it intersects with other shapes.
Returns list of split pieces. If no splits were done, returns list containing the Returns list of split pieces. If no splits were done, returns list containing the
original shape.""" original shape."""
if shape.ShapeType == "Wire": if shape.ShapeType == "Wire":
bit_extractor = lambda(sh): sh.Edges bit_extractor = lambda(sh): sh.Edges
joint_extractor = lambda(sh): sh.Vertexes joint_extractor = lambda(sh): sh.Vertexes
@ -309,7 +309,7 @@ def myCustomFusionRoutine(list_of_shapes):
else: else:
#can't split the shape #can't split the shape
return [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 # 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 # FIXME: this is slow, and maybe can be optimized
splits = [] splits = []
@ -331,7 +331,7 @@ def myCustomFusionRoutine(list_of_shapes):
return [shape] return [shape]
from . import ShapeMerge from . import ShapeMerge
new_pieces = ShapeMerge.mergeShapes(bit_extractor(shape), split_connections= splits, bool_compsolid= True).childShapes() new_pieces = ShapeMerge.mergeShapes(bit_extractor(shape), split_connections= splits, bool_compsolid= True).childShapes()
if len(new_pieces) == 1: if len(new_pieces) == 1:
#shape was not split (split points found, but the shape remained in one piece). #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 return new_pieces
def explodeCompounds(self): def explodeCompounds(self):
"""explodeCompounds(): if any of self.pieces is a compound, the compound is exploded. """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 After running this, 'self' is filled with new data, where pieces are updated to
contain the stuff extracted from compounds.""" contain the stuff extracted from compounds."""
has_compounds = False has_compounds = False
for piece in self.pieces: for piece in self.pieces:
if piece.ShapeType == "Compound": if piece.ShapeType == "Compound":
@ -351,10 +351,10 @@ def myCustomFusionRoutine(list_of_shapes):
return return
from .Utils import compoundLeaves from .Utils import compoundLeaves
new_data = GeneralFuseReturnBuilder(self.source_shapes) new_data = GeneralFuseReturnBuilder(self.source_shapes)
new_data.hasher_class = HashableShape #deep hashing not needed here. new_data.hasher_class = HashableShape #deep hashing not needed here.
for iPiece in range(len(self.pieces)): for iPiece in range(len(self.pieces)):
piece = self.pieces[iPiece] piece = self.pieces[iPiece]
if piece.ShapeType == "Compound": if piece.ShapeType == "Compound":
@ -372,30 +372,30 @@ class GeneralFuseReturnBuilder(FrozenClass):
def __define_attributes(self): def __define_attributes(self):
self.pieces = [] 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._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._pieces_from_source = [] #list of list of ints
self.source_shapes = [] self.source_shapes = []
self.hasher_class = HashableShape_Deep self.hasher_class = HashableShape_Deep
self._freeze() self._freeze()
def __init__(self, source_shapes): def __init__(self, source_shapes):
self.__define_attributes() self.__define_attributes()
self.source_shapes = source_shapes self.source_shapes = source_shapes
self._pieces_from_source = [[] for i in range(len(source_shapes))] self._pieces_from_source = [[] for i in range(len(source_shapes))]
def addPiece(self, piece_shape, source_shape_index_list): 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.""" already exists, returns False, and only updates source<->piece map."""
ret = False ret = False
i_piece_existing = None i_piece_existing = None
hash = None hash = None
if piece_shape.ShapeType != "Compound": # do not catch duplicate compounds if piece_shape.ShapeType != "Compound": # do not catch duplicate compounds
hash = self.hasher_class(piece_shape) hash = self.hasher_class(piece_shape)
i_piece_existing, dummy = self._piece_to_index.get(hash, (None, None)) i_piece_existing, dummy = self._piece_to_index.get(hash, (None, None))
if i_piece_existing is None: if i_piece_existing is None:
#adding #adding
self.pieces.append(piece_shape) self.pieces.append(piece_shape)
@ -410,11 +410,11 @@ class GeneralFuseReturnBuilder(FrozenClass):
if not i_piece_existing in self._pieces_from_source[iSource]: if not i_piece_existing in self._pieces_from_source[iSource]:
self._pieces_from_source[iSource].append(i_piece_existing) self._pieces_from_source[iSource].append(i_piece_existing)
return ret return ret
def replacePiece(self, piece_index, new_shape): def replacePiece(self, piece_index, new_shape):
assert(self.pieces[piece_index].ShapeType == "Compound") assert(self.pieces[piece_index].ShapeType == "Compound")
assert(new_shape.ShapeType == "Compound") assert(new_shape.ShapeType == "Compound")
self.pieces[piece_index] = new_shape self.pieces[piece_index] = new_shape
def getGFReturn(self): def getGFReturn(self):
return (Part.Compound(self.pieces), [[self.pieces[iPiece] for iPiece in ilist] for ilist in self._pieces_from_source]) return (Part.Compound(self.pieces), [[self.pieces[iPiece] for iPiece in ilist] for ilist in self._pieces_from_source])

View File

@ -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.""" """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 #first, check if shapes can be compared by size
ShapeMerge.dimensionOfShapes(list_of_shapes) ShapeMerge.dimensionOfShapes(list_of_shapes)
rel_precision = 1e-8 rel_precision = 1e-8
#find it! #find it!
max_size = -1e100 # max size encountered so far max_size = -1e100 # max size encountered so far
count_max = 0 # number of shapes with size equal to max_size count_max = 0 # number of shapes with size equal to max_size
@ -56,40 +56,40 @@ def shapeOfMaxSize(list_of_shapes):
return shape_max return shape_max
def connect(list_of_shapes, tolerance = 0.0): def connect(list_of_shapes, tolerance = 0.0):
"""connect(list_of_shapes, tolerance = 0.0): connects solids (walled objects), shells and """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. 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 Compounds in list_of_shapes are automatically exploded, so self-intersecting compounds
are valid for connect.""" are valid for connect."""
# explode all compounds before GFA. # explode all compounds before GFA.
new_list_of_shapes = [] new_list_of_shapes = []
for sh in list_of_shapes: for sh in list_of_shapes:
new_list_of_shapes.extend( compoundLeaves(sh) ) new_list_of_shapes.extend( compoundLeaves(sh) )
list_of_shapes = new_list_of_shapes list_of_shapes = new_list_of_shapes
#test if shapes are compatible for connecting #test if shapes are compatible for connecting
dim = ShapeMerge.dimensionOfShapes(list_of_shapes) dim = ShapeMerge.dimensionOfShapes(list_of_shapes)
if dim == 0: if dim == 0:
raise TypeError("Cannot connect vertices!") raise TypeError("Cannot connect vertices!")
if len(list_of_shapes) < 2: if len(list_of_shapes) < 2:
return Part.makeCompound(list_of_shapes) return Part.makeCompound(list_of_shapes)
if not generalFuseIsAvailable(): #fallback to legacy if not generalFuseIsAvailable(): #fallback to legacy
result = list_of_shapes[0] result = list_of_shapes[0]
for i in range(1, len(list_of_shapes)): for i in range(1, len(list_of_shapes)):
result = connect_legacy(result, list_of_shapes[i], tolerance) result = connect_legacy(result, list_of_shapes[i], tolerance)
return result return result
pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance) pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance)
ao = GeneralFuseResult(list_of_shapes, (pieces, map)) ao = GeneralFuseResult(list_of_shapes, (pieces, map))
ao.splitAggregates() ao.splitAggregates()
#print len(ao.pieces)," pieces total" #print len(ao.pieces)," pieces total"
keepers = [] keepers = []
all_danglers = [] # debug all_danglers = [] # debug
#add all biggest dangling pieces #add all biggest dangling pieces
for src in ao.source_shapes: for src in ao.source_shapes:
danglers = [piece for piece in ao.piecesFromSource(src) if len(ao.sourcesOfPiece(piece)) == 1] 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): for ii in range(2, ao.largestOverlapCount()+1):
list_ii_pieces = [piece for piece in ao.pieces if len(ao.sourcesOfPiece(piece)) == ii] list_ii_pieces = [piece for piece in ao.pieces if len(ao.sourcesOfPiece(piece)) == ii]
keepers_2_add = [] keepers_2_add = []
for piece in list_ii_pieces: for piece in list_ii_pieces:
if ShapeMerge.isConnected(piece, touch_test_list): if ShapeMerge.isConnected(piece, touch_test_list):
keepers_2_add.append(piece) keepers_2_add.append(piece)
if len(keepers_2_add) == 0: if len(keepers_2_add) == 0:
break break
keepers.extend(keepers_2_add) keepers.extend(keepers_2_add)
touch_test_list = Part.Compound(keepers_2_add) touch_test_list = Part.Compound(keepers_2_add)
#merge, and we are done! #merge, and we are done!
#print len(keepers)," pieces to keep" #print len(keepers)," pieces to keep"
return ShapeMerge.mergeShapes(keepers) return ShapeMerge.mergeShapes(keepers)
def connect_legacy(shape1, shape2, tolerance = 0.0): def connect_legacy(shape1, shape2, tolerance = 0.0):
"""connect_legacy(shape1, shape2, tolerance = 0.0): alternative implementation of """connect_legacy(shape1, shape2, tolerance = 0.0): alternative implementation of
connect, without use of generalFuse. Slow. Provided for backwards compatibility, and connect, without use of generalFuse. Slow. Provided for backwards compatibility, and
for older OCC.""" for older OCC."""
if tolerance>0.0: if tolerance>0.0:
import FreeCAD as App import FreeCAD as App
App.Console.PrintWarning("connect_legacy does not support tolerance (yet).\n") App.Console.PrintWarning("connect_legacy does not support tolerance (yet).\n")
@ -134,8 +134,8 @@ def connect_legacy(shape1, shape2, tolerance = 0.0):
# (TODO) # (TODO)
def embed_legacy(shape_base, shape_tool, tolerance = 0.0): def embed_legacy(shape_base, shape_tool, tolerance = 0.0):
"""embed_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of """embed_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of
embed, without use of generalFuse. Slow. Provided for backwards compatibility, and embed, without use of generalFuse. Slow. Provided for backwards compatibility, and
for older OCC.""" for older OCC."""
if tolerance>0.0: if tolerance>0.0:
import FreeCAD as App import FreeCAD as App
@ -152,12 +152,12 @@ def embed_legacy(shape_base, shape_tool, tolerance = 0.0):
elif dim == 1: elif dim == 1:
result = ShapeMerge.mergeShapes(result.Edges) result = ShapeMerge.mergeShapes(result.Edges)
return result return result
def cutout_legacy(shape_base, shape_tool, tolerance = 0.0): def cutout_legacy(shape_base, shape_tool, tolerance = 0.0):
"""cutout_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of """cutout_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of
cutout, without use of generalFuse. Slow. Provided for backwards compatibility, and cutout, without use of generalFuse. Slow. Provided for backwards compatibility, and
for older OCC.""" for older OCC."""
if tolerance>0.0: if tolerance>0.0:
import FreeCAD as App import FreeCAD as App
App.Console.PrintWarning("cutout_legacy does not support tolerance (yet).\n") 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: for sh in shapes_base:
result.append(cutout(sh, shape_tool)) result.append(cutout(sh, shape_tool))
return Part.Compound(result) return Part.Compound(result)
shape_base = shapes_base[0] shape_base = shapes_base[0]
pieces = compoundLeaves(shape_base.cut(shape_tool)) pieces = compoundLeaves(shape_base.cut(shape_tool))
return shapeOfMaxSize(pieces) return shapeOfMaxSize(pieces)

View File

@ -32,12 +32,12 @@ from .Utils import HashableShape
def findSharedElements(shape_list, element_extractor): def findSharedElements(shape_list, element_extractor):
if len(shape_list) < 2: if len(shape_list) < 2:
raise ValueError("findSharedElements: at least two shapes must be provided (have {num})".format(num= len(shape_list))) raise ValueError("findSharedElements: at least two shapes must be provided (have {num})".format(num= len(shape_list)))
all_elements = [] #list of sets of HashableShapes all_elements = [] #list of sets of HashableShapes
for shape in shape_list: for shape in shape_list:
all_elements.append(set( all_elements.append(set(
[HashableShape(sh) for sh in element_extractor(shape)] [HashableShape(sh) for sh in element_extractor(shape)]
)) ))
shared_elements = None shared_elements = None
for elements in all_elements: for elements in all_elements:
if shared_elements is None: if shared_elements is None:
@ -56,22 +56,22 @@ def isConnected(shape1, shape2, shape_dim = -1):
return len(findSharedElements([shape1, shape2], extractor))>0 return len(findSharedElements([shape1, shape2], extractor))>0
def splitIntoGroupsBySharing(list_of_shapes, element_extractor, split_connections = []): def splitIntoGroupsBySharing(list_of_shapes, element_extractor, split_connections = []):
"""splitIntoGroupsBySharing(list_of_shapes, element_type, split_connections = []): find, """splitIntoGroupsBySharing(list_of_shapes, element_type, split_connections = []): find,
which shapes in list_of_shapes are connected into groups by sharing elements. 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. 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. 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.""" enumerate shapes of a group."""
split_connections = set([HashableShape(element) for element in split_connections]) 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. 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. # new group is created. If connected, shape is added to groups, and the groups are joined.
for shape in list_of_shapes: for shape in list_of_shapes:
shape_elements = set([HashableShape(element) for element in element_extractor(shape)]) 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. #shape bridges a gap between some groups. Join them into one.
#rebuilding list of groups. First, add the new "supergroup", then add the rest #rebuilding list of groups. First, add the new "supergroup", then add the rest
groups_new = [] groups_new = []
supergroup = (list(),set()) supergroup = (list(),set())
for iGroup in connected_to: for iGroup in connected_to:
supergroup[0].extend( groups[iGroup][0] )# merge lists of shapes supergroup[0].extend( groups[iGroup][0] )# merge lists of shapes
supergroup[1].update( groups[iGroup][1] )# merge lists of elements supergroup[1].update( groups[iGroup][1] )# merge lists of elements
groups_new.append(supergroup) groups_new.append(supergroup)
for iGroup in range(len(groups)): for iGroup in range(len(groups)):
if not iGroup in connected_to: #fixme: inefficient! if not iGroup in connected_to: #fixme: inefficient!
groups_new.append(groups[iGroup]) groups_new.append(groups[iGroup])
groups = groups_new groups = groups_new
connected_to = [0] 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) # 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: if len(connected_to) > 0:
iGroup = connected_to[0] iGroup = connected_to[0]
@ -111,16 +111,16 @@ def splitIntoGroupsBySharing(list_of_shapes, element_extractor, split_connection
else: else:
newgroup = ([shape], shape_elements) newgroup = ([shape], shape_elements)
groups.append(newgroup) groups.append(newgroup)
# done. Discard unnecessary data and return result. # done. Discard unnecessary data and return result.
return [shapes for shapes,elements in groups] return [shapes for shapes,elements in groups]
def mergeSolids(list_of_solids_compsolids, flag_single = False, split_connections = [], bool_compsolid = False): 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 """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 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 single solid. If flag_single is False, the output is a compound containing all
resulting solids. resulting solids.
Note. CompSolids are treated as lists of solids - i.e., merged into solids.""" Note. CompSolids are treated as lists of solids - i.e., merged into solids."""
solids = [] solids = []
@ -128,7 +128,7 @@ def mergeSolids(list_of_solids_compsolids, flag_single = False, split_connection
solids.extend(sh.Solids) solids.extend(sh.Solids)
if flag_single: if flag_single:
cs = Part.CompSolid(solids) cs = Part.CompSolid(solids)
return cs if bool_compsolid else Part.makeSolid(cs) return cs if bool_compsolid else Part.makeSolid(cs)
else: else:
if len(solids)==0: if len(solids)==0:
return Part.Compound([]) return Part.Compound([])
@ -148,7 +148,7 @@ def mergeShells(list_of_faces_shells, flag_single = False, split_connections = [
else: else:
groups = splitIntoGroupsBySharing(faces, lambda(sh): sh.Edges, split_connections) groups = splitIntoGroupsBySharing(faces, lambda(sh): sh.Edges, split_connections)
return Part.makeCompound([Part.Shell(group) for group in groups]) return Part.makeCompound([Part.Shell(group) for group in groups])
def mergeWires(list_of_edges_wires, flag_single = False, split_connections = []): def mergeWires(list_of_edges_wires, flag_single = False, split_connections = []):
edges = [] edges = []
for sh in list_of_edges_wires: for sh in list_of_edges_wires:
@ -158,30 +158,30 @@ def mergeWires(list_of_edges_wires, flag_single = False, split_connections = [])
else: else:
groups = splitIntoGroupsBySharing(edges, lambda(sh): sh.Vertexes, split_connections) groups = splitIntoGroupsBySharing(edges, lambda(sh): sh.Vertexes, split_connections)
return Part.makeCompound([Part.Wire(Part.getSortedClusters(group)[0]) for group in groups]) return Part.makeCompound([Part.Wire(Part.getSortedClusters(group)[0]) for group in groups])
def mergeVertices(list_of_vertices, flag_single = False, split_connections = []): def mergeVertices(list_of_vertices, flag_single = False, split_connections = []):
# no comprehensive support, just following the footprint of other mergeXXX() # no comprehensive support, just following the footprint of other mergeXXX()
return Part.makeCompound(removeDuplicates(list_of_vertices)) return Part.makeCompound(removeDuplicates(list_of_vertices))
def mergeShapes(list_of_shapes, flag_single = False, split_connections = [], bool_compsolid = False): 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): """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. into solids or compsolids.
list_of_shapes: shapes to merge. Shapes must share elements in order to be merged. 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). If True, return is the single piece (e.g. a shell).
split_connections: list of shapes that are excluded when searching for connections. 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 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. split. If flag_single is True, this argument is ignored.
bool_compsolid: determines behavior when dealing with solids/compsolids. If True, bool_compsolid: determines behavior when dealing with solids/compsolids. If True,
result is compsolid/compound of compsolids. If False, all touching solids and result is compsolid/compound of compsolids. If False, all touching solids and
compsolids are unified into single solids. If not merging solids/compsolids, this compsolids are unified into single solids. If not merging solids/compsolids, this
argument is ignored.""" argument is ignored."""
if len(list_of_shapes)==0: if len(list_of_shapes)==0:
return Part.Compound([]) return Part.Compound([])
args = [list_of_shapes, flag_single, split_connections] args = [list_of_shapes, flag_single, split_connections]
@ -209,11 +209,11 @@ def removeDuplicates(list_of_shapes):
new_list.append(sh) new_list.append(sh)
hashes.add(hash) hashes.add(hash)
return new_list return new_list
def dimensionOfShapes(list_of_shapes): 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.""" in the list. If dimension of shapes varies, TypeError is raised."""
dimensions = [["Vertex"], ["Edge","Wire"], ["Face","Shell"], ["Solid","CompSolid"]] dimensions = [["Vertex"], ["Edge","Wire"], ["Face","Shell"], ["Solid","CompSolid"]]
dim = -1 dim = -1
for sh in list_of_shapes: for sh in list_of_shapes:
@ -224,4 +224,4 @@ def dimensionOfShapes(list_of_shapes):
dim = iDim dim = iDim
if iDim != dim: 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)) 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 return dim

View File

@ -33,16 +33,16 @@ from . import Utils
import FreeCAD import FreeCAD
def booleanFragments(list_of_shapes, mode, tolerance = 0.0): def booleanFragments(list_of_shapes, mode, tolerance = 0.0):
"""booleanFragments(list_of_shapes, mode, tolerance = 0.0): functional part of """booleanFragments(list_of_shapes, mode, tolerance = 0.0): functional part of
BooleanFragments feature. It's just result of generalFuse plus a bit of BooleanFragments feature. It's just result of generalFuse plus a bit of
post-processing. post-processing.
mode is a string. It can be "Standard", "Split" or "CompSolid". mode is a string. It can be "Standard", "Split" or "CompSolid".
"Standard" - return generalFuse as is. "Standard" - return generalFuse as is.
"Split" - wires and shells will be split at intersections. "Split" - wires and shells will be split at intersections.
"CompSolid" - solids will be extracted from result of generelFuse, and compsolids will "CompSolid" - solids will be extracted from result of generelFuse, and compsolids will
be made from them; all other stuff is discarded.""" be made from them; all other stuff is discarded."""
pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance) pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance)
if mode == "Standard": if mode == "Standard":
return pieces return pieces
@ -61,15 +61,15 @@ def booleanFragments(list_of_shapes, mode, tolerance = 0.0):
raise ValueError("Unknown mode: {mode}".format(mode= mode)) raise ValueError("Unknown mode: {mode}".format(mode= mode))
def slice(base_shape, tool_shapes, mode, tolerance = 0.0): 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. Slice feature. Splits base_shape into pieces based on intersections with tool_shapes.
mode is a string. It can be "Standard", "Split" or "CompSolid". mode is a string. It can be "Standard", "Split" or "CompSolid".
"Standard" - return like generalFuse: edges, faces and solids are split, but wires, "Standard" - return like generalFuse: edges, faces and solids are split, but wires,
shells, compsolids get extra segments but remain in one piece. shells, compsolids get extra segments but remain in one piece.
"Split" - wires and shells will be split at intersections, too. "Split" - wires and shells will be split at intersections, too.
"CompSolid" - slice a solid and glue it back together to make a compsolid""" "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 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: if len(shapes) < 2:
raise ValueError("No slicing objects supplied!") raise ValueError("No slicing objects supplied!")

View File

@ -123,7 +123,7 @@ class ViewProviderBooleanFragments:
return True return True
def cmdCreateBooleanFragmentsFeature(name, mode): 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".""" BooleanFragments feature (GFA). Mode can be "Standard", "Split", or "CompSolid"."""
sel = FreeCADGui.Selection.getSelectionEx() sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create Boolean Fragments") FreeCAD.ActiveDocument.openTransaction("Create Boolean Fragments")
@ -250,7 +250,7 @@ class ViewProviderSlice:
return True return True
def cmdCreateSliceFeature(name, mode): 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".""" Slice feature. Mode can be "Standard", "Split", or "CompSolid"."""
sel = FreeCADGui.Selection.getSelectionEx() sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create Slice") FreeCAD.ActiveDocument.openTransaction("Create Slice")
@ -378,7 +378,7 @@ class ViewProviderXOR:
return True return True
def cmdCreateXORFeature(name): 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".""" XOR feature (GFA). Mode can be "Standard", "Split", or "CompSolid"."""
sel = FreeCADGui.Selection.getSelectionEx() sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create Boolean XOR") FreeCAD.ActiveDocument.openTransaction("Create Boolean XOR")

View File

@ -40,7 +40,7 @@ class HashableShape(object):
class HashableShape_Deep(object): class HashableShape_Deep(object):
"""Similar to HashableShape, except that the things the shape is composed of are compared. """Similar to HashableShape, except that the things the shape is composed of are compared.
Example: Example:
>>> wire2 = Part.Wire(wire1.childShapes()) >>> wire2 = Part.Wire(wire1.childShapes())
>>> wire2.isSame(wire1) >>> wire2.isSame(wire1)
@ -48,29 +48,29 @@ Example:
>>> HashableShape_Deep(wire2) == HashableShape_Deep(wire1) >>> HashableShape_Deep(wire2) == HashableShape_Deep(wire1)
True # <--- made of same set of elements True # <--- made of same set of elements
""" """
def __init__(self, shape): def __init__(self, shape):
self.Shape = shape self.Shape = shape
self.hash = 0 self.hash = 0
for el in shape.childShapes(): for el in shape.childShapes():
self.hash = self.hash ^ el.hashCode() self.hash = self.hash ^ el.hashCode()
def __eq__(self, other): def __eq__(self, other):
# avoiding extensive comparison for now. Just doing a few extra tests should reduce the already-low chances of false-positives # 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 self.hash == other.hash:
if len(self.Shape.childShapes()) == len(other.Shape.childShapes()): if len(self.Shape.childShapes()) == len(other.Shape.childShapes()):
if self.Shape.ShapeType == other.Shape.ShapeType: if self.Shape.ShapeType == other.Shape.ShapeType:
return True return True
return False return False
def __hash__(self): def __hash__(self):
return self.hash return self.hash
def compoundLeaves(shape_or_compound): def compoundLeaves(shape_or_compound):
"""compoundLeaves(shape_or_compound): extracts all non-compound shapes from a nested 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.""" returned list."""
if shape_or_compound.ShapeType == "Compound": if shape_or_compound.ShapeType == "Compound":
leaves = [] leaves = []
for child in shape_or_compound.childShapes(): for child in shape_or_compound.childShapes():
@ -78,23 +78,23 @@ def compoundLeaves(shape_or_compound):
return leaves return leaves
else: else:
return [shape_or_compound] return [shape_or_compound]
def upgradeToAggregateIfNeeded(list_of_shapes, types = None): def upgradeToAggregateIfNeeded(list_of_shapes, types = None):
"""upgradeToAggregateIfNeeded(list_of_shapes, types = None): upgrades non-aggregate type """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 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. type shapes. Returns the new list. Recursively traverses into compounds.
aggregate shape types are Wire, Shell, CompSolid aggregate shape types are Wire, Shell, CompSolid
non-aggregate shape types are Vertex, Edge, Face, Solid 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. contained shapes.
Examples: Examples:
list_of_shapes contains only faces -> nothing happens list_of_shapes contains only faces -> nothing happens
list_of_shapes contains faces and shells -> faces are converted to shells list_of_shapes contains faces and shells -> faces are converted to shells
'types' argument is needed for recursive traversal. Do not supply.""" 'types' argument is needed for recursive traversal. Do not supply."""
import Part import Part
if types is None: if types is None:
types = set() types = set()
@ -127,4 +127,3 @@ class FrozenClass(object):
def _unfreeze(self): def _unfreeze(self):
self.__isfrozen = False self.__isfrozen = False

View File

@ -24,7 +24,7 @@
__title__ = "BOPTools package" __title__ = "BOPTools package"
__url__ = "http://www.freecadweb.org" __url__ = "http://www.freecadweb.org"
__doc__ = """BOPTools Package (part of FreeCAD). Routines that power Connect, Embed, Cutout, __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""" BOP-like operations"""
## @package BOPTools ## @package BOPTools
@ -57,14 +57,14 @@ def reloadAll():
import FreeCAD import FreeCAD
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
addCommands() addCommands()
def addCommands(): def addCommands():
"addCommands(): add all GUI commands of BOPTools package to FreeCAD command manager." "addCommands(): add all GUI commands of BOPTools package to FreeCAD command manager."
JoinFeatures.addCommands() JoinFeatures.addCommands()
SplitFeatures.addCommands() SplitFeatures.addCommands()
def generalFuseIsAvailable(): 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.""" True if Part.OCC_VERSION >= 6.9.0."""
import Part import Part
if not hasattr(Part, "OCC_VERSION"): if not hasattr(Part, "OCC_VERSION"):