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