File size: 20,481 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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# 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.GeneralFuseResult module"
__author__ = "DeepSOIC"
__url__ = "https://www.freecad.org"
__doc__ = "Implementation of GeneralFuseResult class, which parses return of generalFuse."

import Part
from .Utils import HashableShape, HashableShape_Deep, FrozenClass


class GeneralFuseResult(FrozenClass):
    """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):
        # stores the data returned by generalFuse, supplied to class constructor
        self.gfa_return = None

        # pieces that resulted from intersetion routine. List of shapes (non-decorated).
        self.pieces = None
        # key = decorated shape. Value = index (int) into self.pieces
        self._piece_to_index = {}

        # list of shapes that was supplied to generalFuse (plus the self-shape). List of shapes (non-decorated)
        self.source_shapes = []
        # key = decorated shape. Value = index (int) into self.source_shapes
        self._source_to_index = {}

        # 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 source shapes (indexes) the piece came from, by index of piece. List of lists of ints.
        self._sources_of_piece = []

        # dictionary for finding, which source shapes did an element of pieces come from.
        # key = HashableShape (element). Value = set of ints
        self._element_to_source = {}

        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
        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
        self.__define_attributes()
        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])
            if not ha_piece in self._piece_to_index:
                self._piece_to_index[ha_piece] = iPiece
            else:
                raise ValueError("GeneralFuseAnalyzer.parse: duplicate piece shape detected.")
        # 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"])

            types = set()
            for piece in self.pieces:
                types.add(piece.ShapeType)

            types_to_extract = types.intersection(nonaggregate_types)

            def extractor(sh):
                return (
                    (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)
            for i_aggregate in range(len(aggregate_sources_indexes)):
                iSource = aggregate_sources_indexes[i_aggregate]
                if len(map[iSource]) == 0:  # recover only if info is actually missing
                    map[iSource] = [aggregate_pieces[i_aggregate]]
                    # search if any plain pieces are also in this aggregate piece. If yes, we need to add the piece to map.
                    for sh in extractor(aggregate_pieces[i_aggregate]):
                        hash = HashableShape(sh)
                        iPiece = self._piece_to_index.get(hash)
                        if iPiece is not None:
                            # 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
                        )
                    )

        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)
        for iSource in range(len(self.source_shapes)):
            list_pieces = map[iSource]
            for piece in list_pieces:
                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.
        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:
                el_h = HashableShape(element)
                if el_h in self._element_to_source:
                    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
        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
        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
        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.

        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
        pieces_to_split are split.

        '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.
        * 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
                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()
        for i_new_piece in range(len(new_data.pieces)):
            new_piece = new_data.pieces[i_new_piece]
            if HashableShape(new_piece) in pieces_to_split:
                if new_piece.ShapeType == "Compound":
                    ret = self._splitInCompound(new_piece, existing_pieces)
                    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
        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 reuse the old
        one."""

        changed = False
        new_children = []
        for piece in compound.childShapes():
            if piece.ShapeType == "Compound":
                subspl = self._splitInCompound(piece, existing_pieces)
                if subspl is None:
                    new_children.append(piece)
                else:
                    new_children.append(subspl)
                    changed = True
            else:
                new_pieces = self.makeSplitPieces(piece)
                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))
                    if ex_piece is not None:
                        new_children.append(ex_piece)
                        changed = True
                    else:
                        new_children.append(new_piece)
                        existing_pieces[hash] = (-1, new_piece)
        if changed:
            return Part.makeCompound(new_children)
        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
        original shape."""

        if shape.ShapeType == "Wire":
            bit_extractor = lambda sh: sh.Edges
            joint_extractor = lambda sh: sh.Vertexes
        elif shape.ShapeType == "Shell":
            bit_extractor = lambda sh: sh.Faces
            joint_extractor = lambda sh: sh.Edges
        elif shape.ShapeType == "CompSolid":
            bit_extractor = lambda sh: sh.Solids
            joint_extractor = lambda sh: sh.Faces
        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 = []
        for joint in joint_extractor(shape):
            joint_overlap_count = len(self._element_to_source[HashableShape(joint)])
            if joint_overlap_count > 1:
                # find elements in pieces that are connected to joint
                for bit in bit_extractor(self.gfa_return[0]):
                    for joint_bit in joint_extractor(bit):
                        if joint_bit.isSame(joint):
                            # bit is connected to joint!
                            bit_overlap_count = len(self._element_to_source[HashableShape(bit)])
                            assert bit_overlap_count <= joint_overlap_count
                            if bit_overlap_count < joint_overlap_count:
                                if len(splits) == 0 or splits[-1] is not joint:
                                    splits.append(joint)
        if len(splits) == 0:
            # shape was not split - no split points found
            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).
            return [shape]
        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
        contain the stuff extracted from compounds."""

        has_compounds = False
        for piece in self.pieces:
            if piece.ShapeType == "Compound":
                has_compounds = True
        if not has_compounds:
            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":
                for child in compoundLeaves(piece):
                    new_data.addPiece(child, self._sources_of_piece[iPiece])
            else:
                new_data.addPiece(piece, self._sources_of_piece[iPiece])

        self.gfa_return = new_data.getGFReturn()
        self.parse()


class GeneralFuseReturnBuilder(FrozenClass):
    "GeneralFuseReturnBuilder: utility class used by splitAggregates to build fake return of generalFuse, for re-parsing."

    def __define_attributes(self):
        self.pieces = []
        # key = hasher_class(shape). Value = (index_into_self_dot_pieces, shape).
        # Note that GeneralFuseResult uses this item directly.
        self._piece_to_index = {}

        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
        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)
            i_piece_existing = len(self.pieces) - 1
            if hash is not None:
                self._piece_to_index[hash] = (
                    i_piece_existing,
                    piece_shape,
                )
            ret = True
        else:
            # re-adding
            ret = False
        for iSource in source_shape_index_list:
            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.makeCompound(self.pieces),
            [[self.pieces[iPiece] for iPiece in ilist] for ilist in self._pieces_from_source],
        )