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: 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])
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."""
#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)

View File

@ -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
return dim

View File

@ -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!")

View File

@ -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")

View File

@ -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

View File

@ -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"):