435 lines
20 KiB
Python
435 lines
20 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.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 re-use 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.Compound(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.Compound(self.pieces), [[self.pieces[iPiece] for iPiece in ilist] for ilist in self._pieces_from_source])
|