233 lines
10 KiB
Python
233 lines
10 KiB
Python
#/***************************************************************************
|
|
# * Copyright (c) 2016 Victor Titov (DeepSOIC) <vv.titov@gmail.com> *
|
|
# * *
|
|
# * This file is part of the FreeCAD CAx development system. *
|
|
# * *
|
|
# * This library is free software; you can redistribute it and/or *
|
|
# * modify it under the terms of the GNU Library General Public *
|
|
# * License as published by the Free Software Foundation; either *
|
|
# * version 2 of the License, or (at your option) any later version. *
|
|
# * *
|
|
# * This library is distributed in the hope that it will be useful, *
|
|
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
# * GNU Library General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Library General Public *
|
|
# * License along with this library; see the file COPYING.LIB. If not, *
|
|
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
|
|
# * Suite 330, Boston, MA 02111-1307, USA *
|
|
# * *
|
|
# ***************************************************************************/
|
|
|
|
__title__="BOPTools.ShapeMerge module"
|
|
__author__ = "DeepSOIC"
|
|
__url__ = "https://www.freecad.org"
|
|
__doc__ = "Tools for merging shapes with shared elements. Useful for final processing of results of Part.Shape.generalFuse()."
|
|
|
|
import Part
|
|
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:
|
|
shared_elements = elements
|
|
else:
|
|
shared_elements.intersection_update(elements)
|
|
return [el.Shape for el in shared_elements]
|
|
|
|
def isConnected(shape1, shape2, shape_dim = -1):
|
|
if shape_dim == -1:
|
|
shape_dim = dimensionOfShapes([shape1, shape2])
|
|
extractor = {0: None,
|
|
1: (lambda sh: sh.Vertexes),
|
|
2: (lambda sh: sh.Edges),
|
|
3: (lambda sh: sh.Faces) }[shape_dim]
|
|
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.
|
|
|
|
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 groups on purpose.
|
|
|
|
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,
|
|
# 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)])
|
|
shape_elements.difference_update(split_connections)
|
|
#search if shape is connected to any groups
|
|
connected_to = []
|
|
not_in_connected_to = []
|
|
for iGroup in range(len(groups)):
|
|
connected = False
|
|
for element in shape_elements:
|
|
if element in groups[iGroup][1]:
|
|
connected_to.append(iGroup)
|
|
connected = True
|
|
break
|
|
else:
|
|
# `break` not invoked, so `connected` is false
|
|
not_in_connected_to.append(iGroup)
|
|
|
|
# test if we need to join groups
|
|
if len(connected_to)>1:
|
|
#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)
|
|
|
|
l_groups = len(groups)
|
|
groups_new.extend([groups[i_group] \
|
|
for i_group in not_in_connected_to \
|
|
if i_group < l_groups])
|
|
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]
|
|
groups[iGroup][0].append(shape)
|
|
groups[iGroup][1].update( shape_elements )
|
|
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
|
|
resulting solids.
|
|
|
|
Note. CompSolids are treated as lists of solids - i.e., merged into solids."""
|
|
|
|
solids = []
|
|
for sh in list_of_solids_compsolids:
|
|
solids.extend(sh.Solids)
|
|
if flag_single:
|
|
cs = Part.CompSolid(solids)
|
|
return cs if bool_compsolid else Part.makeSolid(cs)
|
|
else:
|
|
if len(solids)==0:
|
|
return Part.Compound([])
|
|
groups = splitIntoGroupsBySharing(solids, lambda sh: sh.Faces, split_connections)
|
|
if bool_compsolid:
|
|
merged_solids = [Part.CompSolid(group) for group in groups]
|
|
else:
|
|
merged_solids = [Part.makeSolid(Part.CompSolid(group)) for group in groups]
|
|
return Part.makeCompound(merged_solids)
|
|
|
|
def mergeShells(list_of_faces_shells, flag_single = False, split_connections = []):
|
|
faces = []
|
|
for sh in list_of_faces_shells:
|
|
faces.extend(sh.Faces)
|
|
if flag_single:
|
|
return Part.makeShell(faces)
|
|
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:
|
|
edges.extend(sh.Edges)
|
|
if flag_single:
|
|
return Part.Wire(edges)
|
|
else:
|
|
groups = splitIntoGroupsBySharing(edges, lambda sh: sh.Vertexes, split_connections)
|
|
return Part.makeCompound([Part.Wire(Part.sortEdges(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
|
|
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.
|
|
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. 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
|
|
argument is ignored."""
|
|
|
|
if len(list_of_shapes)==0:
|
|
return Part.Compound([])
|
|
args = [list_of_shapes, flag_single, split_connections]
|
|
dim = dimensionOfShapes(list_of_shapes)
|
|
if dim == 0:
|
|
return mergeVertices(*args)
|
|
elif dim == 1:
|
|
return mergeWires(*args)
|
|
elif dim == 2:
|
|
return mergeShells(*args)
|
|
elif dim == 3:
|
|
args.append(bool_compsolid)
|
|
return mergeSolids(*args)
|
|
else:
|
|
assert(dim >= 0 and dim <= 3)
|
|
|
|
def removeDuplicates(list_of_shapes):
|
|
hashes = set()
|
|
new_list = []
|
|
for sh in list_of_shapes:
|
|
hash = HashableShape(sh)
|
|
if hash in hashes:
|
|
pass
|
|
else:
|
|
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
|
|
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:
|
|
sht = sh.ShapeType
|
|
for iDim in range(len(dimensions)):
|
|
if sht in dimensions[iDim]:
|
|
if dim == -1:
|
|
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
|