File size: 10,875 Bytes
985c397 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | # SPDX-License-Identifier: LGPL-2.1-or-later
# /***************************************************************************
# * 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.makeShell(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
|