Merge pull request #229 from DeepSOIC/BOPTools1

BOP tools (Part wb): new joinfeatures, and shape splitting tools/compsolid creation tools
This commit is contained in:
wwmayer 2016-07-29 22:11:38 +02:00 committed by GitHub
commit a14794b4d5
19 changed files with 2843 additions and 166 deletions

View File

@ -221,6 +221,19 @@ PyMODINIT_FUNC initPart()
PyModule_AddObject(partModule, "BRepOffsetAPI", brepModule);
Base::Interpreter().addType(&Part::BRepOffsetAPI_MakePipeShellPy::Type,brepModule,"MakePipeShell");
try{
//import all submodules of BOPTools, to make them easy to browse in Py console.
//It's done in this weird manner instead of bt.caMemberFunction("importAll"),
//because the latter crashed when importAll failed with exception.
Base::Interpreter().runString("__import__('BOPTools').importAll()");
Py::Object bt = Base::Interpreter().runStringObject("__import__('BOPTools')");
module.setAttr(std::string("BOPTools"),bt);
} catch (Base::PyException &err){
Base::Console().Error("Failed to import BOPTools package:\n");
err.ReportException();
}
Part::TopoShape ::init();
Part::PropertyPartShape ::init();
Part::PropertyGeometryList ::init();

View File

@ -292,6 +292,14 @@ SET(Part_Scripts
AttachmentEditor/FrozenClass.py
AttachmentEditor/TaskAttachmentEditor.py
AttachmentEditor/TaskAttachmentEditor.ui
BOPTools/__init__.py
BOPTools/GeneralFuseResult.py
BOPTools/JoinAPI.py
BOPTools/JoinFeatures.py
BOPTools/ShapeMerge.py
BOPTools/SplitAPI.py
BOPTools/SplitFeatures.py
BOPTools/Utils.py
)
add_library(Part SHARED ${Part_SRCS})

View File

@ -0,0 +1,420 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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__ = "http://www.freecadweb.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 #stores the data returned by generalFuse, supplied to class constructor
self.pieces = None #pieces that resulted from intersetion routine. List of shapes (non-decorated).
self._piece_to_index = {} # key = decorated shape. Value = index (int) into self.pieces
self.source_shapes = [] # list of shapes that was supplied to generalFuse (plus the self-shape). List of shapes (non-decorated)
self._source_to_index = {} # key = decorated shape. Value = index (int) into self.source_shapes
self._pieces_of_source = [] #list of pieces (indexes) generated from a source shape, by index of source shape. List of lists of ints.
self._sources_of_piece = [] #list of source shapes (indexes) the piece came from, by index of piece. List of lists of ints.
self._element_to_source = {} #dictionary for finding, which source shapes did an element of pieces come from. key = HashableShape (element). Value = set of ints
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)
extractor = lambda(sh):(
(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 = []
self._piece_to_index = {} # key = hasher_class(shape). Value = (index_into_self_dot_pieces, shape). Note that GeneralFuseResult uses this item directly.
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])

View File

@ -0,0 +1,175 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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.JoinAPI module"
__author__ = "DeepSOIC"
__url__ = "http://www.freecadweb.org"
__doc__ = "JoinFeatures functions that operate on shapes."
import Part
from . import ShapeMerge
from . import generalFuseIsAvailable
from .GeneralFuseResult import GeneralFuseResult
from .Utils import compoundLeaves
def shapeOfMaxSize(list_of_shapes):
"""shapeOfMaxSize(list_of_shapes): finds the shape that has the largest mass in the list and returns it. The shapes in the list must be of same dimension."""
#first, check if shapes can be compared by size
ShapeMerge.dimensionOfShapes(list_of_shapes)
rel_precision = 1e-8
#find it!
max_size = -1e100 # max size encountered so far
count_max = 0 # number of shapes with size equal to max_size
shape_max = None # shape of max_size
for sh in list_of_shapes:
v = abs(Part.cast_to_shape(sh).Mass)
if v > max_size*(1 + rel_precision) :
max_size = v
shape_max = sh
count_max = 1
elif (1-rel_precision) * max_size <= v and v <= (1+rel_precision) * max_size :
count_max = count_max + 1
if count_max > 1 :
raise ValueError("There is more than one largest piece!")
return shape_max
def connect(list_of_shapes, tolerance = 0.0):
"""connect(list_of_shapes, tolerance = 0.0): connects solids (walled objects), shells and
wires by throwing off small parts that result when splitting them at intersections.
Compounds in list_of_shapes are automatically exploded, so self-intersecting compounds
are valid for connect."""
# explode all compounds before GFA.
new_list_of_shapes = []
for sh in list_of_shapes:
new_list_of_shapes.extend( compoundLeaves(sh) )
list_of_shapes = new_list_of_shapes
#test if shapes are compatible for connecting
dim = ShapeMerge.dimensionOfShapes(list_of_shapes)
if dim == 0:
raise TypeError("Cannot connect vertices!")
if len(list_of_shapes) < 2:
return Part.makeCompound(list_of_shapes)
if not generalFuseIsAvailable(): #fallback to legacy
result = list_of_shapes[0]
for i in range(1, len(list_of_shapes)):
result = connect_legacy(result, list_of_shapes[i], tolerance)
return result
pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance)
ao = GeneralFuseResult(list_of_shapes, (pieces, map))
ao.splitAggregates()
#print len(ao.pieces)," pieces total"
keepers = []
all_danglers = [] # debug
#add all biggest dangling pieces
for src in ao.source_shapes:
danglers = [piece for piece in ao.piecesFromSource(src) if len(ao.sourcesOfPiece(piece)) == 1]
all_danglers.extend(danglers)
largest = shapeOfMaxSize(danglers)
if largest is not None:
keepers.append(largest)
touch_test_list = Part.Compound(keepers)
#add all intersection pieces that touch danglers, triple intersection pieces that touch duals, and so on
for ii in range(2, ao.largestOverlapCount()+1):
list_ii_pieces = [piece for piece in ao.pieces if len(ao.sourcesOfPiece(piece)) == ii]
keepers_2_add = []
for piece in list_ii_pieces:
if ShapeMerge.isConnected(piece, touch_test_list):
keepers_2_add.append(piece)
if len(keepers_2_add) == 0:
break
keepers.extend(keepers_2_add)
touch_test_list = Part.Compound(keepers_2_add)
#merge, and we are done!
#print len(keepers)," pieces to keep"
return ShapeMerge.mergeShapes(keepers)
def connect_legacy(shape1, shape2, tolerance = 0.0):
"""connect_legacy(shape1, shape2, tolerance = 0.0): alternative implementation of
connect, without use of generalFuse. Slow. Provided for backwards compatibility, and
for older OCC."""
if tolerance>0.0:
import FreeCAD as App
App.Console.PrintWarning("connect_legacy does not support tolerance (yet).\n")
cut1 = shape1.cut(shape2)
cut1 = shapeOfMaxSize(cut1.childShapes())
cut2 = shape2.cut(shape1)
cut2 = shapeOfMaxSize(cut2.childShapes())
return cut1.multiFuse([cut2, shape2.common(shape1)])
#def embed(shape_base, shape_tool, tolerance = 0.0):
# (TODO)
def embed_legacy(shape_base, shape_tool, tolerance = 0.0):
"""embed_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of
embed, without use of generalFuse. Slow. Provided for backwards compatibility, and
for older OCC."""
if tolerance>0.0:
import FreeCAD as App
App.Console.PrintWarning("embed_legacy does not support tolerance (yet).\n")
# using legacy implementation, except adding support for shells
pieces = compoundLeaves(shape_base.cut(shape_tool))
piece = shapeOfMaxSize(pieces)
result = piece.fuse(shape_tool)
dim = ShapeMerge.dimensionOfShapes(pieces)
if dim == 2:
# fusing shells returns shells that are still split. Reassemble them
result = ShapeMerge.mergeShapes(result.Faces)
elif dim == 1:
result = ShapeMerge.mergeShapes(result.Edges)
return result
def cutout_legacy(shape_base, shape_tool, tolerance = 0.0):
"""cutout_legacy(shape_base, shape_tool, tolerance = 0.0): alternative implementation of
cutout, without use of generalFuse. Slow. Provided for backwards compatibility, and
for older OCC."""
if tolerance>0.0:
import FreeCAD as App
App.Console.PrintWarning("cutout_legacy does not support tolerance (yet).\n")
#if base is multi-piece, work on per-piece basis
shapes_base = compoundLeaves(shape_base)
if len(shapes_base) > 1:
result = []
for sh in shapes_base:
result.append(cutout(sh, shape_tool))
return Part.Compound(result)
shape_base = shapes_base[0]
pieces = compoundLeaves(shape_base.cut(shape_tool))
return shapeOfMaxSize(pieces)

View File

@ -0,0 +1,386 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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.JoinFeatures module"
__author__ = "DeepSOIC"
__url__ = "http://www.freecadweb.org"
__doc__ = "Implementation of document objects (features) for connect, ebmed and cutout operations."
from . import JoinAPI
import FreeCAD
import Part
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
# -------------------------- common stuff --------------------------------------------------
#-------------------------- translation-related code ----------------------------------------
#Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!"
#http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
try:
_fromUtf8 = QtCore.QString.fromUtf8
except Exception:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except NameError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
#--------------------------/translation-related code ----------------------------------------
def getParamRefine():
return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Part/Boolean").GetBool("RefineModel")
def cmdCreateJoinFeature(name, mode):
"cmdCreateJoinFeature(name, mode): generalized implementaion of GUI commands."
sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create "+mode)
FreeCADGui.addModule("BOPTools.JoinFeatures")
FreeCADGui.doCommand("j = BOPTools.JoinFeatures.make{mode}(name = '{name}')".format(mode= mode, name= name))
if mode == "Embed" or mode == "Cutout":
FreeCADGui.doCommand("j.Base = App.ActiveDocument."+sel[0].Object.Name)
FreeCADGui.doCommand("j.Tool = App.ActiveDocument."+sel[1].Object.Name)
elif mode == "Connect":
FreeCADGui.doCommand("j.Objects = {sel}".format(
sel= "[" + ", ".join(["App.ActiveDocument."+so.Object.Name for so in sel]) + "]"
))
else:
raise ValueError("cmdCreateJoinFeature: Unexpected mode {mode}".format(mode= repr(mode)))
try:
FreeCADGui.doCommand("j.Proxy.execute(j)")
FreeCADGui.doCommand("j.purgeTouched()")
except Exception as err:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures","Computing the result failed with an error: \n\n{err}\n\n Click 'Continue' to create the feature anyway, or 'Abort' to cancel.", None)
.format(err= err.message))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
btnOK = mb.addButton(_translate("Part_JoinFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
mb.setDefaultButton(btnOK)
mb.exec_()
if mb.clickedButton() is btnAbort:
FreeCAD.ActiveDocument.abortTransaction()
return
FreeCADGui.doCommand("for obj in j.ViewObject.Proxy.claimChildren():\n"
" obj.ViewObject.hide()")
FreeCAD.ActiveDocument.commitTransaction()
def getIconPath(icon_dot_svg):
return ":/icons/" + icon_dot_svg
# -------------------------- /common stuff --------------------------------------------------
# -------------------------- Connect --------------------------------------------------
def makeConnect(name):
'''makeConnect(name): makes an Connect object.'''
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
FeatureConnect(obj)
if FreeCAD.GuiUp:
ViewProviderConnect(obj.ViewObject)
return obj
class FeatureConnect:
"The PartJoinFeature object"
def __init__(self,obj):
obj.addProperty("App::PropertyLinkList","Objects","Connect","Object to be connectded.")
obj.addProperty("App::PropertyBool","Refine","Connect","True = refine resulting shape. False = output as is.")
obj.Refine = getParamRefine()
obj.addProperty("App::PropertyLength","Tolerance","Connect","Tolerance when intersecting (fuzzy value). In addition to tolerances of the shapes.")
obj.Proxy = self
def execute(self,selfobj):
rst = JoinAPI.connect([obj.Shape for obj in selfobj.Objects], selfobj.Tolerance)
if selfobj.Refine:
rst = rst.removeSplitter()
selfobj.Shape = rst
class ViewProviderConnect:
"A View Provider for the Part Connect feature"
def __init__(self,vobj):
vobj.Proxy = self
def getIcon(self):
return getIconPath("Part_JoinConnect.svg")
def attach(self, vobj):
self.ViewObject = vobj
self.Object = vobj.Object
def setEdit(self,vobj,mode):
return False
def unsetEdit(self,vobj,mode):
return
def __getstate__(self):
return None
def __setstate__(self,state):
return None
def claimChildren(self):
return self.Object.Objects
def onDelete(self, feature, subelements):
try:
for obj in self.claimChildren():
obj.ViewObject.show()
except Exception as err:
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
return True
class CommandConnect:
"Command to create Connect feature"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_JoinConnect.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_JoinFeatures","Connect objects"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_JoinFeatures","Part_JoinConnect: Fuses objects, taking care to preserve voids.")}
def Activated(self):
if len(FreeCADGui.Selection.getSelectionEx()) >= 1 :
cmdCreateJoinFeature(name = "Connect", mode = "Connect")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures", "Select at least two objects, or one or more compounds, first!", None))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
# -------------------------- /Connect --------------------------------------------------
# -------------------------- Embed --------------------------------------------------
def makeEmbed(name):
'''makeEmbed(name): makes an Embed object.'''
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
FeatureEmbed(obj)
if FreeCAD.GuiUp:
ViewProviderEmbed(obj.ViewObject)
return obj
class FeatureEmbed:
"The Part Embed object"
def __init__(self,obj):
obj.addProperty("App::PropertyLink","Base","Embed","Object to embed into.")
obj.addProperty("App::PropertyLink","Tool","Embed","Object to be embedded.")
obj.addProperty("App::PropertyBool","Refine","Embed","True = refine resulting shape. False = output as is.")
obj.Refine = getParamRefine()
obj.addProperty("App::PropertyLength","Tolerance","Embed","Tolerance when intersecting (fuzzy value). In addition to tolerances of the shapes.")
obj.Proxy = self
def execute(self,selfobj):
rst = JoinAPI.embed_legacy(selfobj.Base.Shape, selfobj.Tool.Shape, selfobj.Tolerance)
if selfobj.Refine:
rst = rst.removeSplitter()
selfobj.Shape = rst
class ViewProviderEmbed:
"A View Provider for the Part Embed feature"
def __init__(self,vobj):
vobj.Proxy = self
def getIcon(self):
return getIconPath("Part_JoinEmbed.svg")
def attach(self, vobj):
self.ViewObject = vobj
self.Object = vobj.Object
def setEdit(self,vobj,mode):
return False
def unsetEdit(self,vobj,mode):
return
def __getstate__(self):
return None
def __setstate__(self,state):
return None
def claimChildren(self):
return [self.Object.Base, self.Object.Tool]
def onDelete(self, feature, subelements):
try:
self.Object.Base.ViewObject.show()
self.Object.Tool.ViewObject.show()
except Exception as err:
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
return True
class CommandEmbed:
"Command to create Part Embed feature"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_JoinEmbed.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_JoinFeatures","Embed object"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_JoinFeatures","Fuses one object into another, taking care to preserve voids.")}
def Activated(self):
if len(FreeCADGui.Selection.getSelectionEx()) == 2 :
cmdCreateJoinFeature(name = "Embed", mode = "Embed")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures","Select base object, then the object to embed, and invoke this tool.", None))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
# -------------------------- /Embed --------------------------------------------------
# -------------------------- Cutout --------------------------------------------------
def makeCutout(name):
'''makeCutout(name): makes an Cutout object.'''
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
FeatureCutout(obj)
if FreeCAD.GuiUp:
ViewProviderCutout(obj.ViewObject)
return obj
class FeatureCutout:
"The Part Cutout object"
def __init__(self,obj):
obj.addProperty("App::PropertyLink","Base","Cutout","Object to be cut.")
obj.addProperty("App::PropertyLink","Tool","Cutout","Object to make cutout for.")
obj.addProperty("App::PropertyBool","Refine","Cutout","True = refine resulting shape. False = output as is.")
obj.Refine = getParamRefine()
obj.addProperty("App::PropertyLength","Tolerance","Cutout","Tolerance when intersecting (fuzzy value). In addition to tolerances of the shapes.")
obj.Proxy = self
def execute(self,selfobj):
rst = JoinAPI.cutout_legacy(selfobj.Base.Shape, selfobj.Tool.Shape, selfobj.Tolerance)
if selfobj.Refine:
rst = rst.removeSplitter()
selfobj.Shape = rst
class ViewProviderCutout:
"A View Provider for the Part Cutout feature"
def __init__(self,vobj):
vobj.Proxy = self
def getIcon(self):
return getIconPath("Part_JoinCutout.svg")
def attach(self, vobj):
self.ViewObject = vobj
self.Object = vobj.Object
def setEdit(self,vobj,mode):
return False
def unsetEdit(self,vobj,mode):
return
def __getstate__(self):
return None
def __setstate__(self,state):
return None
def claimChildren(self):
return [self.Object.Base, self.Object.Tool]
def onDelete(self, feature, subelements):
try:
self.Object.Base.ViewObject.show()
self.Object.Tool.ViewObject.show()
except Exception as err:
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
return True
class CommandCutout:
"Command to create PartJoinFeature in Cutout mode"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_JoinCutout.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_JoinFeatures","Cutout for object"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_JoinFeatures","Makes a cutout in one object to fit another object.")}
def Activated(self):
if len(FreeCADGui.Selection.getSelectionEx()) == 2 :
cmdCreateJoinFeature(name = "Cutout", mode = "Cutout")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures","Select the object to make a cutout in, then the object that should fit into the cutout, and invoke this tool.", None))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
# -------------------------- /Cutout --------------------------------------------------
def addCommands():
FreeCADGui.addCommand('Part_JoinCutout',CommandCutout())
FreeCADGui.addCommand('Part_JoinEmbed',CommandEmbed())
FreeCADGui.addCommand('Part_JoinConnect',CommandConnect())

View File

@ -0,0 +1,227 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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__ = "http://www.freecadweb.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 = []
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
# 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)
for iGroup in range(len(groups)):
if not iGroup in connected_to: #fixme: inefficient!
groups_new.append(groups[iGroup])
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.Shell(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.getSortedClusters(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

View File

@ -0,0 +1,103 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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.SplitAPI module"
__author__ = "DeepSOIC"
__url__ = "http://www.freecadweb.org"
__doc__ = "Split functions that operate on list_of_shapes."
import Part
from . import ShapeMerge
from .GeneralFuseResult import GeneralFuseResult
from . import Utils
import FreeCAD
def booleanFragments(list_of_shapes, mode, tolerance = 0.0):
"""booleanFragments(list_of_shapes, mode, tolerance = 0.0): functional part of
BooleanFragments feature. It's just result of generalFuse plus a bit of
post-processing.
mode is a string. It can be "Standard", "Split" or "CompSolid".
"Standard" - return generalFuse as is.
"Split" - wires and shells will be split at intersections.
"CompSolid" - solids will be extracted from result of generelFuse, and compsolids will
be made from them; all other stuff is discarded."""
pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance)
if mode == "Standard":
return pieces
elif mode == "CompSolid":
solids = pieces.Solids
if len(solids) < 1:
raise ValueError("No solids in the result. Can't make CompSolid.")
elif len(solids) == 1:
FreeCAD.Console.PrintWarning("Part_BooleanFragments: only one solid in the result, generating trivial compsolid.")
return ShapeMerge.mergeSolids(solids, bool_compsolid= True)
elif mode == "Split":
gr = GeneralFuseResult(list_of_shapes, (pieces,map))
gr.splitAggregates()
return Part.Compound(gr.pieces)
else:
raise ValueError("Unknown mode: {mode}".format(mode= mode))
def slice(base_shape, tool_shapes, mode, tolerance = 0.0):
"""slice(base_shape, tool_shapes, mode, tolerance = 0.0): functional part of
Slice feature. Splits base_shape into pieces based on intersections with tool_shapes.
mode is a string. It can be "Standard", "Split" or "CompSolid".
"Standard" - return like generalFuse: edges, faces and solids are split, but wires,
shells, compsolids get extra segments but remain in one piece.
"Split" - wires and shells will be split at intersections, too.
"CompSolid" - slice a solid and glue it back together to make a compsolid"""
shapes = [base_shape] + [Part.Compound([tool_shape]) for tool_shape in tool_shapes] # hack: putting tools into compounds will prevent contamination of result with pieces of tools
if len(shapes) < 2:
raise ValueError("No slicing objects supplied!")
pieces, map = shapes[0].generalFuse(shapes[1:], tolerance)
gr = GeneralFuseResult(shapes, (pieces,map))
if mode == "Standard":
result = gr.piecesFromSource(shapes[0])
elif mode == "CompSolid":
solids = Part.Compound(gr.piecesFromSource(shapes[0])).Solids
if len(solids) < 1:
raise ValueError("No solids in the result. Can't make compsolid.")
elif len(solids) == 1:
FreeCAD.Console.PrintWarning("Part_Slice: only one solid in the result, generating trivial compsolid.")
result = ShapeMerge.mergeSolids(solids, bool_compsolid= True).childShapes()
elif mode == "Split":
gr.splitAggregates(gr.piecesFromSource(shapes[0]))
result = gr.piecesFromSource(shapes[0])
return result[0] if len(result) == 1 else Part.Compound(result)
def xor(list_of_shapes, tolerance = 0.0):
"""xor(list_of_shapes, tolerance = 0.0): boolean XOR operation."""
list_of_shapes = Utils.upgradeToAggregateIfNeeded(list_of_shapes)
pieces, map = list_of_shapes[0].generalFuse(list_of_shapes[1:], tolerance)
gr = GeneralFuseResult(list_of_shapes, (pieces,map))
gr.explodeCompounds()
gr.splitAggregates()
pieces_to_keep = []
for piece in gr.pieces:
if len(gr.sourcesOfPiece(piece)) % 2 == 1:
pieces_to_keep.append(piece)
return Part.Compound(pieces_to_keep)

View File

@ -0,0 +1,441 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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.SplitFeatures module"
__author__ = "DeepSOIC"
__url__ = "http://www.freecadweb.org"
__doc__ = "Shape splitting document objects (features)."
from . import SplitAPI
import FreeCAD
import Part
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
#-------------------------- translation-related code ----------------------------------------
#(see forum thread "A new Part tool is being born... JoinFeatures!"
#http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
try:
_fromUtf8 = QtCore.QString.fromUtf8
except Exception:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except NameError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
#--------------------------/translation-related code ----------------------------------------
def getIconPath(icon_dot_svg):
return ":/icons/" + icon_dot_svg
# -------------------------- /common stuff --------------------------------------------------
# -------------------------- BooleanFragments --------------------------------------------------
def makeBooleanFragments(name):
'''makeBooleanFragments(name): makes an BooleanFragments object.'''
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
FeatureBooleanFragments(obj)
if FreeCAD.GuiUp:
ViewProviderBooleanFragments(obj.ViewObject)
return obj
class FeatureBooleanFragments:
"The BooleanFragments feature object"
def __init__(self,obj):
obj.addProperty("App::PropertyLinkList","Objects","BooleanFragments","Object to compute intersections between.")
obj.addProperty("App::PropertyEnumeration","Mode","BooleanFragments","Standard: wires, shells, compsolids remain in one piece. Split: wires, shells, compsolids are split. CompSolid: make compsolid from solid fragments.")
obj.Mode = ["Standard", "Split", "CompSolid"]
obj.addProperty("App::PropertyLength","Tolerance","BooleanFragments","Tolerance when intersecting (fuzzy value). In addition to tolerances of the shapes.")
obj.Proxy = self
def execute(self,selfobj):
shapes = [obj.Shape for obj in selfobj.Objects]
if len(shapes) == 1 and shapes[0].ShapeType == "Compound":
shapes = shapes[0].childShapes()
if len(shapes) < 2:
raise ValueError("At least two shapes are needed for computing boolean fragments. Got only {num}.".format(num= len(shapes)))
selfobj.Shape = SplitAPI.booleanFragments(shapes, selfobj.Mode, selfobj.Tolerance)
class ViewProviderBooleanFragments:
"A View Provider for the Part BooleanFragments feature"
def __init__(self,vobj):
vobj.Proxy = self
def getIcon(self):
return getIconPath("Part_BooleanFragments.svg")
def attach(self, vobj):
self.ViewObject = vobj
self.Object = vobj.Object
def setEdit(self,vobj,mode):
return False
def unsetEdit(self,vobj,mode):
return
def __getstate__(self):
return None
def __setstate__(self,state):
return None
def claimChildren(self):
return self.Object.Objects
def onDelete(self, feature, subelements):
try:
for obj in self.claimChildren():
obj.ViewObject.show()
except Exception as err:
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
return True
def cmdCreateBooleanFragmentsFeature(name, mode):
"""cmdCreateBooleanFragmentsFeature(name, mode): implementation of GUI command to create
BooleanFragments feature (GFA). Mode can be "Standard", "Split", or "CompSolid"."""
sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create Boolean Fragments")
FreeCADGui.addModule("BOPTools.SplitFeatures")
FreeCADGui.doCommand("j = BOPTools.SplitFeatures.makeBooleanFragments(name= '{name}')".format(name= name))
FreeCADGui.doCommand("j.Objects = {sel}".format(
sel= "[" + ", ".join(["App.ActiveDocument."+so.Object.Name for so in sel]) + "]"
))
FreeCADGui.doCommand("j.Mode = {mode}".format(mode= repr(mode)))
try:
FreeCADGui.doCommand("j.Proxy.execute(j)")
FreeCADGui.doCommand("j.purgeTouched()")
except Exception as err:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_SplitFeatures","Computing the result failed with an error: \n\n{err}\n\nClick 'Continue' to create the feature anyway, or 'Abort' to cancel.", None)
.format(err= err.message))
mb.setWindowTitle(_translate("Part_SplitFeatures","Bad selection", None))
btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
btnOK = mb.addButton(_translate("Part_SplitFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
mb.setDefaultButton(btnOK)
mb.exec_()
if mb.clickedButton() is btnAbort:
FreeCAD.ActiveDocument.abortTransaction()
return
FreeCADGui.doCommand("for obj in j.ViewObject.Proxy.claimChildren():\n"
" obj.ViewObject.hide()")
FreeCAD.ActiveDocument.commitTransaction()
class CommandBooleanFragments:
"Command to create BooleanFragments feature"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_BooleanFragments.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_SplitFeatures","Boolean Fragments"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_SplitFeatures","Part_BooleanFragments: split objects where they intersect")}
def Activated(self):
if len(FreeCADGui.Selection.getSelectionEx()) >= 1 :
cmdCreateBooleanFragmentsFeature(name= "BooleanFragments", mode= "Standard")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_SplitFeatures", "Select at least two objects, or one or more compounds, first! If only one compound is selected, the compounded shapes will be intersected between each other (otherwise, compounds with self-intersections are invalid).", None))
mb.setWindowTitle(_translate("Part_SplitFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
# -------------------------- /BooleanFragments --------------------------------------------------
# -------------------------- Slice --------------------------------------------------
def makeSlice(name):
'''makeSlice(name): makes an Slice object.'''
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
FeatureSlice(obj)
if FreeCAD.GuiUp:
ViewProviderSlice(obj.ViewObject)
return obj
class FeatureSlice:
"The Slice feature object"
def __init__(self,obj):
obj.addProperty("App::PropertyLink","Base","Slice","Object to be sliced.")
obj.addProperty("App::PropertyLinkList","Tools","Slice","Objects that slice.")
obj.addProperty("App::PropertyEnumeration","Mode","Slice","Standard: wires, shells, compsolids remain in one piece. Split: wires, shells, compsolids are split. CompSolid: make compsolid from solid fragments.")
obj.Mode = ["Standard", "Split", "CompSolid"]
obj.addProperty("App::PropertyLength","Tolerance","Slice","Tolerance when intersecting (fuzzy value). In addition to tolerances of the shapes.")
obj.Proxy = self
def execute(self,selfobj):
if len(selfobj.Tools) < 1:
raise ValueError("No slicing objects supplied!")
selfobj.Shape = SplitAPI.slice(selfobj.Base.Shape, [obj.Shape for obj in selfobj.Tools], selfobj.Mode, selfobj.Tolerance)
class ViewProviderSlice:
"A View Provider for the Part Slice feature"
def __init__(self,vobj):
vobj.Proxy = self
def getIcon(self):
return getIconPath("Part_Slice.svg")
def attach(self, vobj):
self.ViewObject = vobj
self.Object = vobj.Object
def setEdit(self,vobj,mode):
return False
def unsetEdit(self,vobj,mode):
return
def __getstate__(self):
return None
def __setstate__(self,state):
return None
def claimChildren(self):
return [self.Object.Base] + self.Object.Tools
def onDelete(self, feature, subelements):
try:
for obj in self.claimChildren():
obj.ViewObject.show()
except Exception as err:
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
return True
def cmdCreateSliceFeature(name, mode):
"""cmdCreateSliceFeature(name, mode): implementation of GUI command to create
Slice feature. Mode can be "Standard", "Split", or "CompSolid"."""
sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create Slice")
FreeCADGui.addModule("BOPTools.SplitFeatures")
FreeCADGui.doCommand("j = BOPTools.SplitFeatures.makeSlice(name= '{name}')".format(name= name))
FreeCADGui.doCommand("j.Base = {sel}[0]\n"
"j.Tools = {sel}[1:]".format(
sel= "[" + ", ".join(["App.ActiveDocument."+so.Object.Name for so in sel]) + "]"
))
FreeCADGui.doCommand("j.Mode = {mode}".format(mode= repr(mode)))
try:
FreeCADGui.doCommand("j.Proxy.execute(j)")
FreeCADGui.doCommand("j.purgeTouched()")
except Exception as err:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_SplitFeatures","Computing the result failed with an error: \n\n{err}\n\nClick 'Continue' to create the feature anyway, or 'Abort' to cancel.", None)
.format(err= err.message))
mb.setWindowTitle(_translate("Part_SplitFeatures","Bad selection", None))
btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
btnOK = mb.addButton(_translate("Part_SplitFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
mb.setDefaultButton(btnOK)
mb.exec_()
if mb.clickedButton() is btnAbort:
FreeCAD.ActiveDocument.abortTransaction()
return
FreeCADGui.doCommand("for obj in j.ViewObject.Proxy.claimChildren():\n"
" obj.ViewObject.hide()")
FreeCAD.ActiveDocument.commitTransaction()
class CommandSlice:
"Command to create Slice feature"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_Slice.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_SplitFeatures","Slice"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_SplitFeatures","Part_Slice: split object by intersections with other objects")}
def Activated(self):
if len(FreeCADGui.Selection.getSelectionEx()) > 1 :
cmdCreateSliceFeature(name= "Slice", mode= "Split")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_SplitFeatures", "Select at least two objects, first! First one is the object to be sliced; the rest are objects to slice with.", None))
mb.setWindowTitle(_translate("Part_SplitFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
# -------------------------- /Slice --------------------------------------------------
# -------------------------- XOR --------------------------------------------------
def makeXOR(name):
'''makeXOR(name): makes an XOR object.'''
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
FeatureXOR(obj)
if FreeCAD.GuiUp:
ViewProviderXOR(obj.ViewObject)
return obj
class FeatureXOR:
"The XOR feature object"
def __init__(self,obj):
obj.addProperty("App::PropertyLinkList","Objects","XOR","Object to compute intersections between.")
obj.addProperty("App::PropertyLength","Tolerance","XOR","Tolerance when intersecting (fuzzy value). In addition to tolerances of the shapes.")
obj.Proxy = self
def execute(self,selfobj):
shapes = [obj.Shape for obj in selfobj.Objects]
if len(shapes) == 1 and shapes[0].ShapeType == "Compound":
shapes = shapes[0].childShapes()
if len(shapes) < 2:
raise ValueError("At least two shapes are needed for computing XOR. Got only {num}.".format(num= len(shapes)))
selfobj.Shape = SplitAPI.xor(shapes, selfobj.Tolerance)
class ViewProviderXOR:
"A View Provider for the Part XOR feature"
def __init__(self,vobj):
vobj.Proxy = self
def getIcon(self):
return getIconPath("Part_XOR.svg")
def attach(self, vobj):
self.ViewObject = vobj
self.Object = vobj.Object
def setEdit(self,vobj,mode):
return False
def unsetEdit(self,vobj,mode):
return
def __getstate__(self):
return None
def __setstate__(self,state):
return None
def claimChildren(self):
return self.Object.Objects
def onDelete(self, feature, subelements):
try:
for obj in self.claimChildren():
obj.ViewObject.show()
except Exception as err:
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
return True
def cmdCreateXORFeature(name):
"""cmdCreateXORFeature(name): implementation of GUI command to create
XOR feature (GFA). Mode can be "Standard", "Split", or "CompSolid"."""
sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create Boolean XOR")
FreeCADGui.addModule("BOPTools.SplitFeatures")
FreeCADGui.doCommand("j = BOPTools.SplitFeatures.makeXOR(name= '{name}')".format(name= name))
FreeCADGui.doCommand("j.Objects = {sel}".format(
sel= "[" + ", ".join(["App.ActiveDocument."+so.Object.Name for so in sel]) + "]"
))
try:
FreeCADGui.doCommand("j.Proxy.execute(j)")
FreeCADGui.doCommand("j.purgeTouched()")
except Exception as err:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_SplitFeatures","Computing the result failed with an error: \n\n{err}\n\nClick 'Continue' to create the feature anyway, or 'Abort' to cancel.", None)
.format(err= err.message))
mb.setWindowTitle(_translate("Part_SplitFeatures","Bad selection", None))
btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
btnOK = mb.addButton(_translate("Part_SplitFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
mb.setDefaultButton(btnOK)
mb.exec_()
if mb.clickedButton() is btnAbort:
FreeCAD.ActiveDocument.abortTransaction()
return
FreeCADGui.doCommand("for obj in j.ViewObject.Proxy.claimChildren():\n"
" obj.ViewObject.hide()")
FreeCAD.ActiveDocument.commitTransaction()
class CommandXOR:
"Command to create XOR feature"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_XOR.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_SplitFeatures","Boolean XOR"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_SplitFeatures","Part_XOR: remove intersection fragments")}
def Activated(self):
if len(FreeCADGui.Selection.getSelectionEx()) >= 1 :
cmdCreateXORFeature(name= "XOR")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_SplitFeatures", "Select at least two objects, or one or more compounds, first! If only one compound is selected, the compounded shapes will be intersected between each other (otherwise, compounds with self-intersections are invalid).", None))
mb.setWindowTitle(_translate("Part_SplitFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
# -------------------------- /XOR --------------------------------------------------
def addCommands():
FreeCADGui.addCommand('Part_BooleanFragments',CommandBooleanFragments())
FreeCADGui.addCommand('Part_Slice',CommandSlice())
FreeCADGui.addCommand('Part_XOR',CommandXOR())

View File

@ -0,0 +1,130 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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.Utils module"
__author__ = "DeepSOIC"
__url__ = "http://www.freecadweb.org"
__doc__ = "Utility code, used by various modules of BOPTools."
class HashableShape(object):
"Decorator for Part.Shape, that can be used as key in dicts. Based on isSame method."
def __init__(self, shape):
self.Shape = shape
self.hash = shape.hashCode()
def __eq__(self, other):
return self.Shape.isSame(other.Shape)
def __hash__(self):
return self.hash
class HashableShape_Deep(object):
"""Similar to HashableShape, except that the things the shape is composed of are compared.
Example:
>>> wire2 = Part.Wire(wire1.childShapes())
>>> wire2.isSame(wire1)
False # <--- the wire2 is a new wire, although made of edges of wire1
>>> HashableShape_Deep(wire2) == HashableShape_Deep(wire1)
True # <--- made of same set of elements
"""
def __init__(self, shape):
self.Shape = shape
self.hash = 0
for el in shape.childShapes():
self.hash = self.hash ^ el.hashCode()
def __eq__(self, other):
# avoiding extensive comparison for now. Just doing a few extra tests should reduce the already-low chances of false-positives
if self.hash == other.hash:
if len(self.Shape.childShapes()) == len(other.Shape.childShapes()):
if self.Shape.ShapeType == other.Shape.ShapeType:
return True
return False
def __hash__(self):
return self.hash
def compoundLeaves(shape_or_compound):
"""compoundLeaves(shape_or_compound): extracts all non-compound shapes from a nested compound.
Note: shape_or_compound may be a non-compound; then, it is the only thing in the
returned list."""
if shape_or_compound.ShapeType == "Compound":
leaves = []
for child in shape_or_compound.childShapes():
leaves.extend( compoundLeaves(child) )
return leaves
else:
return [shape_or_compound]
def upgradeToAggregateIfNeeded(list_of_shapes, types = None):
"""upgradeToAggregateIfNeeded(list_of_shapes, types = None): upgrades non-aggregate type
shapes to aggregate-type shapes if the list has a mix of aggregate and non-aggregate
type shapes. Returns the new list. Recursively traverses into compounds.
aggregate shape types are Wire, Shell, CompSolid
non-aggregate shape types are Vertex, Edge, Face, Solid
Compounds are something special: they are recursively traversed to upgrade the
contained shapes.
Examples:
list_of_shapes contains only faces -> nothing happens
list_of_shapes contains faces and shells -> faces are converted to shells
'types' argument is needed for recursive traversal. Do not supply."""
import Part
if types is None:
types = set()
for shape in list_of_shapes:
types.add(shape.ShapeType)
subshapes = compoundLeaves(shape)
for subshape in subshapes:
types.add(subshape.ShapeType)
if "Wire" in types:
list_of_shapes = [(Part.Wire([shape]) if shape.ShapeType == "Edge" else shape) for shape in list_of_shapes]
if "Shell" in types:
list_of_shapes = [(Part.Shell([shape]) if shape.ShapeType == "Face" else shape) for shape in list_of_shapes]
if "CompSolid" in types:
list_of_shapes = [(Part.CompSolid([shape]) if shape.ShapeType == "Solid" else shape) for shape in list_of_shapes]
if "Compound" in types:
list_of_shapes = [(Part.Compound(upgradeToAggregateIfNeeded(shape.childShapes(), types)) if shape.ShapeType == "Compound" else shape) for shape in list_of_shapes]
return list_of_shapes
# adapted from http://stackoverflow.com/a/3603824/6285007
class FrozenClass(object):
'''FrozenClass: prevents adding new attributes to class outside of __init__'''
__isfrozen = False
def __setattr__(self, key, value):
if self.__isfrozen and not hasattr(self, key):
raise TypeError( "{cls} has no attribute {attr}".format(cls= self.__class__.__name__, attr= key) )
object.__setattr__(self, key, value)
def _freeze(self):
self.__isfrozen = True
def _unfreeze(self):
self.__isfrozen = False

View File

@ -0,0 +1,75 @@
#/***************************************************************************
# * Copyright (c) Victor Titov (DeepSOIC) *
# * (vv.titov@gmail.com) 2016 *
# * *
# * 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 package"
__url__ = "http://www.freecadweb.org"
__doc__ = """BOPTools Package (part of FreeCAD). Routines that power Connect, Embed, Cutout,
BooleanFragments, Slice and XOR features of Part Workbench. Useful for other custom
BOP-like operations"""
__all__ = [
"GeneralFuseResult",
"JoinAPI",
"JoinFeatures",
"ShapeMerge",
"Utils",
"SplitAPI",
"SplitFeatures",
]
def importAll():
"importAll(): imports all modules of BOPTools package"
from . import GeneralFuseResult
from . import JoinAPI
from . import JoinFeatures
from . import ShapeMerge
from . import Utils
from . import SplitAPI
from . import SplitFeatures
def reloadAll():
"reloadAll(): reloads all modules of BOPTools package. Useful for debugging."
for modstr in __all__:
reload(globals()[modstr])
import FreeCAD
if FreeCAD.GuiUp:
addCommands()
def addCommands():
"addCommands(): add all GUI commands of BOPTools package to FreeCAD command manager."
JoinFeatures.addCommands()
SplitFeatures.addCommands()
def generalFuseIsAvailable():
"""generalFuseIsAvailable(): returns True if FreeCAD's Part.Shape.generalFuse is functional.
True if Part.OCC_VERSION >= 6.9.0."""
import Part
if not hasattr(Part, "OCC_VERSION"):
return False
else:
ver_string = Part.OCC_VERSION
import re
match = re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)",ver_string)
major,minor,rev = match.groups()
major = int(major); minor = int(minor); rev = int(rev)
return (major,minor,rev)>=(6,9,0)

View File

@ -26,3 +26,17 @@ INSTALL(
DESTINATION
Mod/Part/AttachmentEditor
)
INSTALL(
FILES
BOPTools/__init__.py
BOPTools/GeneralFuseResult.py
BOPTools/JoinAPI.py
BOPTools/JoinFeatures.py
BOPTools/ShapeMerge.py
BOPTools/SplitAPI.py
BOPTools/SplitFeatures.py
BOPTools/Utils.py
DESTINATION
Mod/Part/BOPTools
)

View File

@ -33,6 +33,7 @@
# include <TopoDS_Shape.hxx>
# include <TopExp_Explorer.hxx>
# include <Inventor/events/SoMouseButtonEvent.h>
# include <Standard_Version.hxx>
#endif
#include <Base/Console.h>
@ -569,25 +570,25 @@ void CmdPartCompJoinFeatures::languageChange()
Gui::Command* joinConnect = rcCmdMgr.getCommandByName("Part_JoinConnect");
if (joinConnect) {
QAction* cmd0 = a[0];
cmd0->setText(QApplication::translate("PartCompJoinFeatures", joinConnect->getMenuText()));
cmd0->setToolTip(QApplication::translate("Part_JoinConnect", joinConnect->getToolTipText()));
cmd0->setStatusTip(QApplication::translate("Part_JoinConnect", joinConnect->getStatusTip()));
cmd0->setText(QApplication::translate("Part_JoinFeatures", joinConnect->getMenuText()));
cmd0->setToolTip(QApplication::translate("Part_JoinFeatures", joinConnect->getToolTipText()));
cmd0->setStatusTip(QApplication::translate("Part_JoinFeatures", joinConnect->getStatusTip()));
}
Gui::Command* joinEmbed = rcCmdMgr.getCommandByName("Part_JoinEmbed");
if (joinEmbed) {
QAction* cmd1 = a[1];
cmd1->setText(QApplication::translate("PartCompJoinFeatures", joinEmbed->getMenuText()));
cmd1->setToolTip(QApplication::translate("Part_JoinEmbed", joinEmbed->getToolTipText()));
cmd1->setStatusTip(QApplication::translate("Part_JoinEmbed", joinEmbed->getStatusTip()));
cmd1->setText(QApplication::translate("Part_JoinFeatures", joinEmbed->getMenuText()));
cmd1->setToolTip(QApplication::translate("Part_JoinFeatures", joinEmbed->getToolTipText()));
cmd1->setStatusTip(QApplication::translate("Part_JoinFeatures", joinEmbed->getStatusTip()));
}
Gui::Command* joinCutout = rcCmdMgr.getCommandByName("Part_JoinCutout");
if (joinCutout) {
QAction* cmd2 = a[2];
cmd2->setText(QApplication::translate("PartCompJoinFeatures", joinCutout->getMenuText()));
cmd2->setToolTip(QApplication::translate("Part_JoinCutout", joinCutout->getToolTipText()));
cmd2->setStatusTip(QApplication::translate("Part_JoinCutout", joinCutout->getStatusTip()));
cmd2->setText(QApplication::translate("Part_JoinFeatures", joinCutout->getMenuText()));
cmd2->setToolTip(QApplication::translate("Part_JoinFeatures", joinCutout->getToolTipText()));
cmd2->setStatusTip(QApplication::translate("Part_JoinFeatures", joinCutout->getStatusTip()));
}
}
@ -599,6 +600,116 @@ bool CmdPartCompJoinFeatures::isActive(void)
return false;
}
//===========================================================================
// Part_CompSplitFeatures (dropdown toolbar button for BooleanFragments, Slice)
//===========================================================================
DEF_STD_CMD_ACL(CmdPartCompSplitFeatures);
CmdPartCompSplitFeatures::CmdPartCompSplitFeatures()
: Command("Part_CompSplitFeatures")
{
sAppModule = "Part";
sGroup = QT_TR_NOOP("Part");
sMenuText = QT_TR_NOOP("Split objects...");
sToolTipText = QT_TR_NOOP("Shape splitting tools. Compsolid creation tools. OCC 6.9.0 or later is required.");
sWhatsThis = "Part_CompSplitFeatures";
sStatusTip = sToolTipText;
}
void CmdPartCompSplitFeatures::activated(int iMsg)
{
Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager();
if (iMsg==0)
rcCmdMgr.runCommandByName("Part_BooleanFragments");
else if (iMsg==1)
rcCmdMgr.runCommandByName("Part_Slice");
else if (iMsg==2)
rcCmdMgr.runCommandByName("Part_XOR");
else
return;
// Since the default icon is reset when enabing/disabling the command we have
// to explicitly set the icon of the used command.
Gui::ActionGroup* pcAction = qobject_cast<Gui::ActionGroup*>(_pcAction);
QList<QAction*> a = pcAction->actions();
assert(iMsg < a.size());
pcAction->setIcon(a[iMsg]->icon());
}
Gui::Action * CmdPartCompSplitFeatures::createAction(void)
{
Gui::ActionGroup* pcAction = new Gui::ActionGroup(this, Gui::getMainWindow());
pcAction->setDropDownMenu(true);
applyCommandData(this->className(), pcAction);
QAction* cmd0 = pcAction->addAction(QString());
cmd0->setIcon(Gui::BitmapFactory().pixmap("Part_BooleanFragments"));
QAction* cmd1 = pcAction->addAction(QString());
cmd1->setIcon(Gui::BitmapFactory().pixmap("Part_Slice"));
QAction* cmd2 = pcAction->addAction(QString());
cmd2->setIcon(Gui::BitmapFactory().pixmap("Part_XOR"));
_pcAction = pcAction;
languageChange();
pcAction->setIcon(cmd0->icon());
int defaultId = 0;
pcAction->setProperty("defaultAction", QVariant(defaultId));
return pcAction;
}
void CmdPartCompSplitFeatures::languageChange()
{
Command::languageChange();
if (!_pcAction)
return;
Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager();
Gui::ActionGroup* pcAction = qobject_cast<Gui::ActionGroup*>(_pcAction);
QList<QAction*> a = pcAction->actions();
Gui::Command* splitBoolFragments = rcCmdMgr.getCommandByName("Part_BooleanFragments");
if (splitBoolFragments) {
QAction* cmd0 = a[0];
cmd0->setText(QApplication::translate("Part_SplitFeatures", splitBoolFragments->getMenuText()));
cmd0->setToolTip(QApplication::translate("Part_SplitFeatures", splitBoolFragments->getToolTipText()));
cmd0->setStatusTip(QApplication::translate("Part_SplitFeatures", splitBoolFragments->getStatusTip()));
}
Gui::Command* splitSlice = rcCmdMgr.getCommandByName("Part_Slice");
if (splitSlice) {
QAction* cmd1 = a[1];
cmd1->setText(QApplication::translate("Part_SplitFeatures", splitSlice->getMenuText()));
cmd1->setToolTip(QApplication::translate("Part_SplitFeatures", splitSlice->getToolTipText()));
cmd1->setStatusTip(QApplication::translate("Part_SplitFeatures", splitSlice->getStatusTip()));
}
Gui::Command* splitXOR = rcCmdMgr.getCommandByName("Part_XOR");
if (splitXOR) {
QAction* cmd2 = a[2];
cmd2->setText(QApplication::translate("Part_SplitFeatures", splitXOR->getMenuText()));
cmd2->setToolTip(QApplication::translate("Part_SplitFeatures", splitXOR->getToolTipText()));
cmd2->setStatusTip(QApplication::translate("Part_SplitFeatures", splitXOR->getStatusTip()));
}
}
bool CmdPartCompSplitFeatures::isActive(void)
{
if (getActiveGuiDocument())
#if OCC_VERSION_HEX < 0x060900
return false;
#else
return true;
#endif
else
return false;
}
//===========================================================================
// Part_Compound
//===========================================================================
@ -1909,6 +2020,7 @@ void CreatePartCommands(void)
rcCmdMgr.addCommand(new CmdPartCut());
rcCmdMgr.addCommand(new CmdPartFuse());
rcCmdMgr.addCommand(new CmdPartCompJoinFeatures());
rcCmdMgr.addCommand(new CmdPartCompSplitFeatures());
rcCmdMgr.addCommand(new CmdPartCompound());
rcCmdMgr.addCommand(new CmdPartSection());
//rcCmdMgr.addCommand(new CmdPartBox2());

View File

@ -5,6 +5,7 @@
<file>icons/PartFeatureImport.xpm</file>
<file>icons/Part_Attachment.svg</file>
<file>icons/Part_Booleans.svg</file>
<file>icons/Part_BooleanFragments.svg</file>
<file>icons/Part_Box.svg</file>
<file>icons/Part_Chamfer.svg</file>
<file>icons/Part_Common.svg</file>
@ -28,10 +29,12 @@
<file>icons/Part_CrossSections.svg</file>
<file>icons/Part_Shapebuilder.png</file>
<file>icons/Part_ShapeInfo.svg</file>
<file>icons/Part_Slice.svg</file>
<file>icons/Part_Sphere.svg</file>
<file>icons/Part_Sweep.svg</file>
<file>icons/Part_Thickness.svg</file>
<file>icons/Part_Torus.svg</file>
<file>icons/Part_XOR.svg</file>
<file>icons/preferences-part_design.svg</file>
<file>icons/Tree_Part.svg</file>
<file>icons/Part_CheckGeometry.svg</file>

View File

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64px"
height="64px"
id="svg2568"
sodipodi:version="0.32"
inkscape:version="0.48.4 r9939"
sodipodi:docname="Part_Booleans.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.1">
<defs
id="defs2570">
<linearGradient
id="linearGradient3864">
<stop
id="stop3866"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient3593">
<stop
style="stop-color:#c8e0f9;stop-opacity:1;"
offset="0"
id="stop3595" />
<stop
style="stop-color:#637dca;stop-opacity:1;"
offset="1"
id="stop3597" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 32 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="64 : 32 : 1"
inkscape:persp3d-origin="32 : 21.333333 : 1"
id="perspective2576" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864-8"
id="radialGradient3094"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.95535607,0,0,0.95539836,-8.548892,-12.077169)"
cx="51.637894"
cy="24.962704"
fx="51.637894"
fy="24.962704"
r="19.571428" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864-6"
id="radialGradient3634"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.542613,0.570754,-0.2959998,0.2814055,118.56568,-25.069586)"
cx="81.866722"
cy="12.084522"
fx="81.866722"
fy="12.084522"
r="19.571428" />
<linearGradient
id="linearGradient3864-6">
<stop
id="stop3866-0"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868-6"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864-8"
id="radialGradient3634-1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.542613,0.570754,-0.2959998,0.2814055,118.56568,-25.069586)"
cx="81.866722"
cy="12.084522"
fx="81.866722"
fy="12.084522"
r="19.571428" />
<linearGradient
id="linearGradient3864-8">
<stop
id="stop3866-4"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868-9"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
r="19.571428"
fy="12.084522"
fx="81.866722"
cy="12.084522"
cx="81.866722"
gradientTransform="matrix(0.38078949,0.40007655,-0.20772376,0.19725441,1.0290225,-8.1325274)"
gradientUnits="userSpaceOnUse"
id="radialGradient3058"
xlink:href="#linearGradient3864-8"
inkscape:collect="always" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864"
id="radialGradient3136"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0690889,0,0,1.0691362,-35.029998,5.7483177)"
cx="48.645836"
cy="25.149042"
fx="48.645836"
fy="25.149042"
r="19.571428" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.5"
inkscape:cx="32"
inkscape:cy="32"
inkscape:current-layer="g3912"
showgrid="true"
inkscape:document-units="px"
inkscape:grid-bbox="true"
inkscape:window-width="1113"
inkscape:window-height="684"
inkscape:window-x="0"
inkscape:window-y="93"
inkscape:window-maximized="0" />
<metadata
id="metadata2573">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<g
id="g3560"
transform="translate(8.7385455e-2,-0.7238878)">
<g
id="g3912"
transform="matrix(0.99373286,0,0,0.38750794,2.7398647,38.320441)">
<path
style="fill:#000000;fill-opacity:0.45801529;fill-rule:evenodd;stroke:none;stroke-width:2.20000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 20.49763,22.92386 C 10.171118,23.627396 2.0092055,32.209267 2.0092055,42.714746 c 0,10.965826 8.8838305,19.850051 19.8491725,19.850051 10.824365,0 19.624655,-8.66486 19.849172,-19.435892 -6.456994,-3.000476 -17.606437,-6.308587 -21.476154,-17.365097 0,-0.969344 0.116669,-1.912809 0.266234,-2.839948 z"
id="path3138"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssccc" />
<path
style="fill:#000000;fill-opacity:0.45801529;fill-rule:evenodd;stroke:none;stroke-width:2.20000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 42.283044,3.1948292 c -8.829516,0 -16.120247,6.4642616 -17.482654,14.9097248 0.452506,-0.03083 0.904747,-0.220654 1.360748,-0.05917 10.599411,3.753589 15.64098,11.230218 19.849172,19.850052 0.04296,0.08799 0.0029,0.273878 0,0.414159 8.011483,-1.710645 14.021622,-8.8428 14.021622,-17.365097 0,-9.79925 -7.950072,-17.7496728 -17.748888,-17.7496688 z"
id="path3140"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsscss" />
<path
inkscape:connector-curvature="0"
id="path3142"
d="m 26.1304,22.66174 c -0.399011,0 -0.779102,0.02126 -1.171309,0.04515 -0.130355,0.711425 -0.203705,1.423499 -0.203705,2.167414 0,7.474822 6.822161,13.54635 15.252468,13.54635 1.09856,0 2.175367,-0.09778 3.208365,-0.293504 0.0024,-0.104055 0,-0.211523 0,-0.31608 0,-8.364682 -7.651912,-15.149336 -17.085819,-15.149335 z"
style="fill:#000000;fill-opacity:0.45801529;fill-rule:evenodd;stroke:none;stroke-width:2.20000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
</g>
<path
style="fill:url(#radialGradient3136);fill-opacity:1;fill-rule:evenodd;stroke:#000137;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 20.49763,22.92386 C 10.171118,23.627396 2.0092055,32.209267 2.0092055,42.714746 c 0,10.965826 8.8838305,19.850051 19.8491725,19.850051 10.824365,0 19.624655,-8.66486 19.849172,-19.435892 -1.200414,0.256317 -2.450368,0.384576 -3.727266,0.384576 -9.798816,0 -17.748888,-7.950423 -17.748888,-17.749673 0,-0.969344 0.116669,-1.912809 0.266234,-2.839948 z"
id="path3550"
inkscape:connector-curvature="0" />
<path
style="fill:url(#radialGradient3094);fill-opacity:1.0;fill-rule:evenodd;stroke:#000000;stroke-width:2.20000004999999990;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 42.283044,3.1948292 c -8.829516,0 -16.120247,6.4642616 -17.482654,14.9097248 0.452506,-0.03083 0.90042,-0.05917 1.360748,-0.05917 10.965341,0 19.849172,8.884225 19.849172,19.850052 0,0.140982 0.0029,0.273878 0,0.414159 8.011483,-1.710645 14.021622,-8.8428 14.021622,-17.365097 0,-9.79925 -7.950072,-17.7496728 -17.748888,-17.7496728 z"
id="path3087"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path3605"
d="m 25.939623,23.130939 c -0.34365,0 -0.671005,0.02063 -1.008795,0.04381 -0.112269,0.690244 -0.175442,1.381117 -0.175442,2.102883 0,7.25227 5.875617,13.143027 13.136257,13.143027 0.946139,0 1.873544,-0.09487 2.763218,-0.284765 0.0021,-0.100957 0,-0.205226 0,-0.30667 0,-8.115635 -6.590243,-14.698286 -14.715238,-14.698285 z"
style="fill:url(#radialGradient3058);fill-opacity:1;fill-rule:evenodd;stroke:#000137;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64px"
height="64px"
id="svg2605"
sodipodi:version="0.32"
inkscape:version="0.48.4 r9939"
sodipodi:docname="Part_Common.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.1">
<defs
id="defs2607">
<linearGradient
id="linearGradient3864">
<stop
id="stop3866"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864"
id="radialGradient3634"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.30708915,0.72517946,-0.37608666,0.15926004,144.43738,-41.038703)"
cx="81.866722"
cy="12.084522"
fx="81.866722"
fy="12.084522"
r="19.571428" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 32 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="64 : 32 : 1"
inkscape:persp3d-origin="32 : 21.333333 : 1"
id="perspective2613" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864-7"
id="radialGradient3784"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7037846,0.9728679,-1.3005398,0.9408264,121.13694,-67.560812)"
cx="109.47948"
cy="-6.229341"
fx="109.47948"
fy="-6.229341"
r="19.571428" />
<linearGradient
id="linearGradient3864-7">
<stop
id="stop3866-1"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868-1"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
r="19.571428"
fy="-6.229341"
fx="109.47948"
cy="-6.229341"
cx="109.47948"
gradientTransform="matrix(0.7037846,0.9728679,-1.3005398,0.9408264,121.13694,-67.560812)"
gradientUnits="userSpaceOnUse"
id="radialGradient3009"
xlink:href="#linearGradient3864-7"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.5"
inkscape:cx="32"
inkscape:cy="32"
inkscape:current-layer="g3640"
showgrid="true"
inkscape:document-units="px"
inkscape:grid-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="986"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1" />
<metadata
id="metadata2610">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<g
id="g3640"
transform="translate(-129.29181,-0.6692279)">
<path
id="path3636"
d="m 161.61943,45.663377 c -0.59617,-0.09885 -1.16582,-0.182465 -1.75378,-0.26778 -0.25328,0.320561 -0.42143,0.655568 -0.48262,1.02454 -0.61471,3.707397 9.07909,8.408892 21.67496,10.497399 1.64137,0.272157 3.25829,0.490423 4.81781,0.649261 0.0125,-0.05096 0.0174,-0.10493 0.026,-0.156783 0.6879,-4.148757 -10.187,-9.409516 -24.28236,-11.746652 z"
style="opacity:0.66523605;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.98973471;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
inkscape:connector-curvature="0" />
<g
id="g3706"
transform="translate(-67.634718,0.17619367)"
style="fill:#908c8c;fill-opacity:1">
<path
transform="matrix(1.0092422,0,0,1.0092422,183.40104,-12.283801)"
d="m 71.785715,34.571426 c 0,10.256717 -8.314712,18.571429 -18.571428,18.571429 -10.256717,0 -18.571428,-8.314712 -18.571428,-18.571429 0,-10.256716 8.314711,-18.571428 18.571428,-18.571428 10.256716,0 18.571428,8.314712 18.571428,18.571428 z"
sodipodi:ry="18.571428"
sodipodi:rx="18.571428"
sodipodi:cy="34.571426"
sodipodi:cx="53.214287"
id="path3664"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000137;stroke-width:2.17985344;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3686"
d="m 230.09375,52.522322 c -10.89909,0.205981 -19.53124,2.700494 -19.53125,5.750333 0,3.184529 9.39089,5.767524 20.96875,5.767524 11.43314,0 20.73789,-2.517585 20.96875,-5.647188 -1.26776,0.07451 -2.58928,0.11174 -3.9375,0.11174 -10.34618,0 -18.71875,-2.3115 -18.71875,-5.157249 0,-0.283217 0.09,-0.554312 0.25,-0.82516 z"
style="opacity:0.66523605;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.98973471;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:connector-curvature="0"
id="path3654"
d="m 218.625,19.59375 c -10.89909,0.748877 -19.53124,9.818077 -19.53125,20.90625 0,11.577861 9.39089,20.96875 20.96875,20.96875 11.43314,0 20.73789,-9.153079 20.96875,-20.53125 -1.26776,0.270909 -2.58928,0.40625 -3.9375,0.40625 -10.34618,-2e-6 -18.71875,-8.403826 -18.71875,-18.75 0,-1.02968 0.09,-2.01529 0.25,-3 z"
style="fill:url(#radialGradient3009);fill-opacity:1;fill-rule:evenodd;stroke:#000137;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
</g>
<path
id="path3605"
d="m 161.97104,13.19993 c -0.45837,-0.172302 -0.90537,-0.308886 -1.36757,-0.447295 -0.49622,0.865451 -0.92728,1.756359 -1.28959,2.720194 -3.6404,9.684565 1.2398,20.496957 10.92437,24.137361 1.26201,0.474384 2.54664,0.812688 3.82865,1.005176 0.0535,-0.133761 0.10302,-0.274055 0.15394,-0.409523 4.07378,-10.837489 -1.41231,-22.932133 -12.2498,-27.005913 z"
style="fill:url(#radialGradient3634);fill-opacity:1;fill-rule:evenodd;stroke:#000137;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64px"
height="64px"
id="svg2568"
sodipodi:version="0.32"
inkscape:version="0.48.4 r9939"
sodipodi:docname="Part_BooleanFragments.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.1">
<defs
id="defs2570">
<linearGradient
id="linearGradient3864">
<stop
id="stop3866"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient3593">
<stop
style="stop-color:#c8e0f9;stop-opacity:1;"
offset="0"
id="stop3595" />
<stop
style="stop-color:#637dca;stop-opacity:1;"
offset="1"
id="stop3597" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 32 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="64 : 32 : 1"
inkscape:persp3d-origin="32 : 21.333333 : 1"
id="perspective2576" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864-8"
id="radialGradient3094"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.009587,0,0,1.0096217,-13.183145,-11.125744)"
cx="51.637894"
cy="24.962704"
fx="51.637894"
fy="24.962704"
r="19.571428" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864-6"
id="radialGradient3634"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.542613,0.570754,-0.2959998,0.2814055,118.56568,-25.069586)"
cx="81.866722"
cy="12.084522"
fx="81.866722"
fy="12.084522"
r="19.571428" />
<linearGradient
id="linearGradient3864-6">
<stop
id="stop3866-0"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868-6"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864-8"
id="radialGradient3634-1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.542613,0.570754,-0.2959998,0.2814055,118.56568,-25.069586)"
cx="81.866722"
cy="12.084522"
fx="81.866722"
fy="12.084522"
r="19.571428" />
<linearGradient
id="linearGradient3864-8">
<stop
id="stop3866-4"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop3868-9"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
r="19.571428"
fy="12.084522"
fx="81.866722"
cy="12.084522"
cx="81.866722"
gradientTransform="matrix(0.38078949,0.40007655,-0.20772376,0.19725441,1.0290225,-8.1325274)"
gradientUnits="userSpaceOnUse"
id="radialGradient3058"
xlink:href="#linearGradient3864-8"
inkscape:collect="always" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3864"
id="radialGradient3136"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1297758,0,0,1.1298147,-37.132532,2.5237176)"
cx="48.645836"
cy="25.149042"
fx="48.645836"
fy="25.149042"
r="19.571428" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.5"
inkscape:cx="32"
inkscape:cy="32"
inkscape:current-layer="g3912"
showgrid="true"
inkscape:document-units="px"
inkscape:grid-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="986"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1" />
<metadata
id="metadata2573">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<g
id="g3560"
transform="translate(8.7385455e-2,-0.7238878)">
<g
id="g3912"
transform="matrix(0.99373286,0,0,0.38750794,2.7398647,38.320441)">
<path
style="fill:#000000;fill-opacity:0.45801529;fill-rule:evenodd;stroke:none;stroke-width:2.20000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 20.654932,22.58659 C 10.24056,23.296112 2.0092055,31.950999 2.0092055,42.545859 c 0,11.059125 8.9594155,20.018938 20.0180515,20.018938 10.91646,0 19.791624,-8.738582 20.018051,-19.601255 -6.511931,-3.026004 -17.756234,-6.362261 -21.658875,-17.512841 0,-0.977591 0.117661,-1.929083 0.268499,-2.864111 z"
id="path3138"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssccc" />
<path
style="fill:#000000;fill-opacity:0.45801529;fill-rule:evenodd;stroke:none;stroke-width:2.20000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 38.935268,6.948418 c -8.904639,0 -16.2574,6.51926 -17.631399,15.036578 0.456356,-0.03109 0.912445,-0.222531 1.372326,-0.05967 10.689592,3.785525 15.774055,11.325766 20.018051,20.018938 0.04333,0.08874 0.0029,0.276208 0,0.417683 8.079645,-1.725199 14.140919,-8.918036 14.140919,-17.512841 0,-9.882624 -8.017712,-17.900689 -17.899897,-17.900685 z"
id="path3140"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsscss" />
</g>
<path
style="fill:url(#radialGradient3136);fill-opacity:1;fill-rule:evenodd;stroke:#000137;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 21.547127,20.674053 C 10.63443,21.417518 2.0092055,30.48645 2.0092055,41.588164 c 0,11.588189 9.3881225,20.976635 20.9759125,20.976635 11.438811,0 20.738651,-9.156631 20.975913,-20.53897 -1.268556,0.270864 -2.589464,0.406402 -3.938845,0.406402 -10.355047,0 -18.756406,-8.401646 -18.756406,-18.75705 0,-1.024359 0.123292,-2.02137 0.281347,-3.001128 z"
id="path3550"
inkscape:connector-curvature="0" />
<path
style="fill:url(#radialGradient3094);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 40.534271,5.0130116 c -9.330724,0 -17.035314,6.8311394 -18.475059,15.7559214 0.478193,-0.03258 0.951533,-0.06253 1.437991,-0.06253 11.58779,0 20.975913,9.388447 20.975913,20.976636 0,0.148983 0.0031,0.289422 0,0.437664 8.466255,-1.807732 14.81756,-9.34467 14.81756,-18.350647 0,-10.355404 -8.401358,-18.7570507 -18.756405,-18.7570507 z"
id="path3087"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -71,6 +71,10 @@ Gui::MenuItem* Workbench::setupMenuBar() const
join->setCommand("Join");
*join << "Part_JoinConnect" << "Part_JoinEmbed" << "Part_JoinCutout";
Gui::MenuItem* split = new Gui::MenuItem;
split->setCommand("Split");
*split << "Part_BooleanFragments" << "Part_Slice" << "Part_XOR";
Gui::MenuItem* part = new Gui::MenuItem;
root->insertItem(item, part);
part->setCommand("&Part");
@ -78,7 +82,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const
*part << prim << "Part_Primitives" << "Part_Builder" << "Separator"
<< "Part_ShapeFromMesh" << "Part_MakeSolid" << "Part_ReverseShape"
<< "Part_SimpleCopy" << "Part_RefineShape" << "Part_CheckGeometry"
<< "Separator" << bop << join << "Separator"
<< "Separator" << bop << join << split << "Separator"
<< "Part_CrossSections" << "Part_Compound" << "Part_MakeFace" << "Part_Extrude"
<< "Part_Revolve" << "Part_Mirror" << "Part_Fillet" << "Part_Chamfer"
<< "Part_RuledSurface" << "Part_Loft" << "Part_Sweep"
@ -123,7 +127,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const
Gui::ToolBarItem* boolop = new Gui::ToolBarItem(root);
boolop->setCommand("Boolean");
*boolop << "Part_Boolean" << "Part_Cut" << "Part_Fuse" << "Part_Common"
<< "Part_CompJoinFeatures" << "Part_CheckGeometry" << "Part_Section"
<< "Part_CompJoinFeatures" << "Part_CompSplitFeatures" << "Part_CheckGeometry" << "Part_Section"
<< "Part_CrossSections";
Gui::ToolBarItem* measure = new Gui::ToolBarItem(root);

View File

@ -43,9 +43,9 @@ class PartWorkbench ( Workbench ):
import PartGui
import Part
try:
import JoinFeatures
except ImportError:
print "JoinFeatures module cannot be loaded"
Part.BOPTools.addCommands()
except Exception as err:
FreeCAD.Console.PrintError("Features from BOPTools package cannot be loaded. {err}\n".format(err= err.message))
def GetClassName(self):
return "PartGui::Workbench"

View File

@ -23,30 +23,32 @@
import FreeCAD, Part
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
#if FreeCAD.GuiUp:
# import FreeCADGui
# from PySide import QtCore, QtGui
__title__="JoinFeatures module"
__title__="JoinFeatures module (legacy)"
__author__ = "DeepSOIC"
__url__ = "http://www.freecadweb.org"
__doc__ = "Legacy JoinFeatures module provided for ability to load projects made with \
FreeCAD v0.16. Do not use. Use BOPTools.JoinFeatures instead."
#-------------------------- translation-related code ----------------------------------------
#Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!"
#http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
#--------------------------/translation-related code ----------------------------------------
##-------------------------- translation-related code ----------------------------------------
##Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!"
##http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
#try:
# _fromUtf8 = QtCore.QString.fromUtf8
#except AttributeError:
# def _fromUtf8(s):
# return s
#try:
# _encoding = QtGui.QApplication.UnicodeUTF8
# def _translate(context, text, disambig):
# return QtGui.QApplication.translate(context, text, disambig, _encoding)
#except AttributeError:
# def _translate(context, text, disambig):
# return QtGui.QApplication.translate(context, text, disambig)
##--------------------------/translation-related code ----------------------------------------
# -------------------------- common stuff --------------------------------------------------
def getParamRefine():
@ -71,14 +73,14 @@ def shapeOfMaxVol(compound):
else:
return compound
def makePartJoinFeature(name, mode = 'bypass'):
'''makePartJoinFeature(name, mode = 'bypass'): makes an PartJoinFeature object.'''
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
_PartJoinFeature(obj)
obj.Mode = mode
obj.Refine = getParamRefine()
_ViewProviderPartJoinFeature(obj.ViewObject)
return obj
#def makePartJoinFeature(name, mode = 'bypass'):
# '''makePartJoinFeature(name, mode = 'bypass'): makes an PartJoinFeature object.'''
# obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
# _PartJoinFeature(obj)
# obj.Mode = mode
# obj.Refine = getParamRefine()
# _ViewProviderPartJoinFeature(obj.ViewObject)
# return obj
class _PartJoinFeature:
"The PartJoinFeature object"
@ -159,133 +161,134 @@ class _ViewProviderPartJoinFeature:
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
return True
def CreateJoinFeature(name, mode):
sel = FreeCADGui.Selection.getSelectionEx()
FreeCAD.ActiveDocument.openTransaction("Create "+mode+"ObjectsFeature")
FreeCADGui.addModule("JoinFeatures")
FreeCADGui.doCommand("j = JoinFeatures.makePartJoinFeature(name = '"+name+"', mode = '"+mode+"' )")
FreeCADGui.doCommand("j.Base = App.ActiveDocument."+sel[0].Object.Name)
FreeCADGui.doCommand("j.Tool = App.ActiveDocument."+sel[1].Object.Name)
try:
FreeCADGui.doCommand("j.Proxy.execute(j)")
FreeCADGui.doCommand("j.purgeTouched()")
except Exception as err:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures","Computing the result failed with an error: {err}. Click 'Continue' to create the feature anyway, or 'Abort' to cancel.", None)
.format(err= err.message))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
btnOK = mb.addButton(_translate("Part_JoinFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
mb.setDefaultButton(btnOK)
mb.exec_()
if mb.clickedButton() is btnAbort:
FreeCAD.ActiveDocument.abortTransaction()
return
FreeCADGui.doCommand("j.Base.ViewObject.hide()")
FreeCADGui.doCommand("j.Tool.ViewObject.hide()")
FreeCAD.ActiveDocument.commitTransaction()
#
#def CreateJoinFeature(name, mode):
# sel = FreeCADGui.Selection.getSelectionEx()
# FreeCAD.ActiveDocument.openTransaction("Create "+mode+"ObjectsFeature")
# FreeCADGui.addModule("JoinFeatures")
# FreeCADGui.doCommand("j = JoinFeatures.makePartJoinFeature(name = '"+name+"', mode = '"+mode+"' )")
# FreeCADGui.doCommand("j.Base = App.ActiveDocument."+sel[0].Object.Name)
# FreeCADGui.doCommand("j.Tool = App.ActiveDocument."+sel[1].Object.Name)
# try:
# FreeCADGui.doCommand("j.Proxy.execute(j)")
# FreeCADGui.doCommand("j.purgeTouched()")
# except Exception as err:
# mb = QtGui.QMessageBox()
# mb.setIcon(mb.Icon.Warning)
# mb.setText(_translate("Part_JoinFeatures","Computing the result failed with an error: {err}. Click 'Continue' to create the feature anyway, or 'Abort' to cancel.", None)
# .format(err= err.message))
# mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
# btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
# btnOK = mb.addButton(_translate("Part_JoinFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
# mb.setDefaultButton(btnOK)
#
# mb.exec_()
#
# if mb.clickedButton() is btnAbort:
# FreeCAD.ActiveDocument.abortTransaction()
# return
#
# FreeCADGui.doCommand("j.Base.ViewObject.hide()")
# FreeCADGui.doCommand("j.Tool.ViewObject.hide()")
#
# FreeCAD.ActiveDocument.commitTransaction()
def getIconPath(icon_dot_svg):
return ":/icons/" + icon_dot_svg
# -------------------------- /common stuff --------------------------------------------------
# -------------------------- ConnectObjectsFeature --------------------------------------------------
class _CommandConnectFeature:
"Command to create PartJoinFeature in Connect mode"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_JoinConnect.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Connect objects"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Fuses objects, taking care to preserve voids.")}
def Activated(self):
if len(FreeCADGui.Selection.getSelectionEx()) == 2 :
CreateJoinFeature(name = "Connect", mode = "Connect")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures", "Two solids need to be selected, first!", None))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
FreeCADGui.addCommand('Part_JoinConnect',_CommandConnectFeature())
# -------------------------- /ConnectObjectsFeature --------------------------------------------------
# -------------------------- EmbedFeature --------------------------------------------------
class _CommandEmbedFeature:
"Command to create PartJoinFeature in Embed mode"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_JoinEmbed.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Embed object"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Fuses one object into another, taking care to preserve voids.")}
def Activated(self):
if len(FreeCADGui.Selection.getSelection()) == 2 :
CreateJoinFeature(name = "Embed", mode = "Embed")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures","Select base object, then the object to embed, and invoke this tool.", None))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
FreeCADGui.addCommand('Part_JoinEmbed',_CommandEmbedFeature())
# -------------------------- /EmbedFeature --------------------------------------------------
# -------------------------- CutoutFeature --------------------------------------------------
class _CommandCutoutFeature:
"Command to create PartJoinFeature in Cutout mode"
def GetResources(self):
return {'Pixmap' : getIconPath("Part_JoinCutout.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Cutout for object"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Makes a cutout in one object to fit another object.")}
def Activated(self):
if len(FreeCADGui.Selection.getSelection()) == 2 :
CreateJoinFeature(name = "Cutout", mode = "Cutout")
else:
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(_translate("Part_JoinFeatures","Select the object to make a cutout in, then the object that should fit into the cutout, and invoke this tool.", None))
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
mb.exec_()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
FreeCADGui.addCommand('Part_JoinCutout',_CommandCutoutFeature())
# -------------------------- /CutoutFeature --------------------------------------------------
## -------------------------- ConnectObjectsFeature --------------------------------------------------
#
#class _CommandConnectFeature:
# "Command to create PartJoinFeature in Connect mode"
# def GetResources(self):
# return {'Pixmap' : getIconPath("Part_JoinConnect.svg"),
# 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Connect objects"),
# 'Accel': "",
# 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Fuses objects, taking care to preserve voids.")}
#
# def Activated(self):
# if len(FreeCADGui.Selection.getSelectionEx()) == 2 :
# CreateJoinFeature(name = "Connect", mode = "Connect")
# else:
# mb = QtGui.QMessageBox()
# mb.setIcon(mb.Icon.Warning)
# mb.setText(_translate("Part_JoinFeatures", "Two solids need to be selected, first!", None))
# mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
# mb.exec_()
#
# def IsActive(self):
# if FreeCAD.ActiveDocument:
# return True
# else:
# return False
#
#FreeCADGui.addCommand('Part_JoinConnect',_CommandConnectFeature())
#
## -------------------------- /ConnectObjectsFeature --------------------------------------------------
#
#
## -------------------------- EmbedFeature --------------------------------------------------
#
#class _CommandEmbedFeature:
# "Command to create PartJoinFeature in Embed mode"
# def GetResources(self):
# return {'Pixmap' : getIconPath("Part_JoinEmbed.svg"),
# 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Embed object"),
# 'Accel': "",
# 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Fuses one object into another, taking care to preserve voids.")}
#
# def Activated(self):
# if len(FreeCADGui.Selection.getSelection()) == 2 :
# CreateJoinFeature(name = "Embed", mode = "Embed")
# else:
# mb = QtGui.QMessageBox()
# mb.setIcon(mb.Icon.Warning)
# mb.setText(_translate("Part_JoinFeatures","Select base object, then the object to embed, and invoke this tool.", None))
# mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
# mb.exec_()
#
#
# def IsActive(self):
# if FreeCAD.ActiveDocument:
# return True
# else:
# return False
#
#FreeCADGui.addCommand('Part_JoinEmbed',_CommandEmbedFeature())
#
## -------------------------- /EmbedFeature --------------------------------------------------
#
#
#
## -------------------------- CutoutFeature --------------------------------------------------
#
#class _CommandCutoutFeature:
# "Command to create PartJoinFeature in Cutout mode"
# def GetResources(self):
# return {'Pixmap' : getIconPath("Part_JoinCutout.svg"),
# 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Cutout for object"),
# 'Accel': "",
# 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Makes a cutout in one object to fit another object.")}
#
# def Activated(self):
# if len(FreeCADGui.Selection.getSelection()) == 2 :
# CreateJoinFeature(name = "Cutout", mode = "Cutout")
# else:
# mb = QtGui.QMessageBox()
# mb.setIcon(mb.Icon.Warning)
# mb.setText(_translate("Part_JoinFeatures","Select the object to make a cutout in, then the object that should fit into the cutout, and invoke this tool.", None))
# mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
# mb.exec_()
#
# def IsActive(self):
# if FreeCAD.ActiveDocument:
# return True
# else:
# return False
#
#FreeCADGui.addCommand('Part_JoinCutout',_CommandCutoutFeature())
#
## -------------------------- /CutoutFeature --------------------------------------------------
#