| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | __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): |
| | |
| | self.gfa_return = None |
| |
|
| | |
| | self.pieces = None |
| | |
| | self._piece_to_index = {} |
| |
|
| | |
| | self.source_shapes = [] |
| | |
| | self._source_to_index = {} |
| |
|
| | |
| | self._pieces_of_source = [] |
| | |
| | self._sources_of_piece = [] |
| |
|
| | |
| | |
| | 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.""" |
| |
|
| | |
| | gfa_return = self.gfa_return |
| | source_shapes = self.source_shapes |
| | self.__define_attributes() |
| | self.gfa_return = gfa_return |
| | self.source_shapes = source_shapes |
| | |
| |
|
| | compound, map = self.gfa_return |
| | self.pieces = compound.childShapes() |
| |
|
| | |
| | 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.") |
| | |
| | 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.") |
| |
|
| | |
| | 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: |
| | map[iSource] = [aggregate_pieces[i_aggregate]] |
| | |
| | for sh in extractor(aggregate_pieces[i_aggregate]): |
| | hash = HashableShape(sh) |
| | iPiece = self._piece_to_index.get(hash) |
| | if iPiece is not None: |
| | |
| | if not map[iSource][-1].isSame(self.pieces[iPiece]): |
| | map[iSource].append(self.pieces[iPiece]) |
| |
|
| | |
| | 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 |
| |
|
| | 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 |
| |
|
| | |
| | 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]) |
| |
|
| | |
| | |
| | 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() |
| | |
| | |
| |
|
| | 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: |
| | |
| | return [shape] |
| |
|
| | |
| | |
| | |
| | splits = [] |
| | for joint in joint_extractor(shape): |
| | joint_overlap_count = len(self._element_to_source[HashableShape(joint)]) |
| | if joint_overlap_count > 1: |
| | |
| | for bit in bit_extractor(self.gfa_return[0]): |
| | for joint_bit in joint_extractor(bit): |
| | if joint_bit.isSame(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: |
| | |
| | return [shape] |
| |
|
| | from . import ShapeMerge |
| |
|
| | new_pieces = ShapeMerge.mergeShapes( |
| | bit_extractor(shape), split_connections=splits, bool_compsolid=True |
| | ).childShapes() |
| | if len(new_pieces) == 1: |
| | |
| | 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 |
| |
|
| | 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 = [] |
| | |
| | |
| | self._piece_to_index = {} |
| |
|
| | self._pieces_from_source = [] |
| | 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": |
| | hash = self.hasher_class(piece_shape) |
| | i_piece_existing, dummy = self._piece_to_index.get(hash, (None, None)) |
| |
|
| | if i_piece_existing is None: |
| | |
| | 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: |
| | |
| | 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], |
| | ) |
| |
|