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:
commit
a14794b4d5
|
@ -221,6 +221,19 @@ PyMODINIT_FUNC initPart()
|
||||||
PyModule_AddObject(partModule, "BRepOffsetAPI", brepModule);
|
PyModule_AddObject(partModule, "BRepOffsetAPI", brepModule);
|
||||||
Base::Interpreter().addType(&Part::BRepOffsetAPI_MakePipeShellPy::Type,brepModule,"MakePipeShell");
|
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::TopoShape ::init();
|
||||||
Part::PropertyPartShape ::init();
|
Part::PropertyPartShape ::init();
|
||||||
Part::PropertyGeometryList ::init();
|
Part::PropertyGeometryList ::init();
|
||||||
|
|
|
@ -292,6 +292,14 @@ SET(Part_Scripts
|
||||||
AttachmentEditor/FrozenClass.py
|
AttachmentEditor/FrozenClass.py
|
||||||
AttachmentEditor/TaskAttachmentEditor.py
|
AttachmentEditor/TaskAttachmentEditor.py
|
||||||
AttachmentEditor/TaskAttachmentEditor.ui
|
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})
|
add_library(Part SHARED ${Part_SRCS})
|
||||||
|
|
420
src/Mod/Part/BOPTools/GeneralFuseResult.py
Normal file
420
src/Mod/Part/BOPTools/GeneralFuseResult.py
Normal 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])
|
175
src/Mod/Part/BOPTools/JoinAPI.py
Normal file
175
src/Mod/Part/BOPTools/JoinAPI.py
Normal 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)
|
||||||
|
|
386
src/Mod/Part/BOPTools/JoinFeatures.py
Normal file
386
src/Mod/Part/BOPTools/JoinFeatures.py
Normal 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())
|
227
src/Mod/Part/BOPTools/ShapeMerge.py
Normal file
227
src/Mod/Part/BOPTools/ShapeMerge.py
Normal 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
|
103
src/Mod/Part/BOPTools/SplitAPI.py
Normal file
103
src/Mod/Part/BOPTools/SplitAPI.py
Normal 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)
|
441
src/Mod/Part/BOPTools/SplitFeatures.py
Normal file
441
src/Mod/Part/BOPTools/SplitFeatures.py
Normal 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())
|
130
src/Mod/Part/BOPTools/Utils.py
Normal file
130
src/Mod/Part/BOPTools/Utils.py
Normal 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
|
||||||
|
|
75
src/Mod/Part/BOPTools/__init__.py
Normal file
75
src/Mod/Part/BOPTools/__init__.py
Normal 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)
|
|
@ -26,3 +26,17 @@ INSTALL(
|
||||||
DESTINATION
|
DESTINATION
|
||||||
Mod/Part/AttachmentEditor
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
# include <TopoDS_Shape.hxx>
|
# include <TopoDS_Shape.hxx>
|
||||||
# include <TopExp_Explorer.hxx>
|
# include <TopExp_Explorer.hxx>
|
||||||
# include <Inventor/events/SoMouseButtonEvent.h>
|
# include <Inventor/events/SoMouseButtonEvent.h>
|
||||||
|
# include <Standard_Version.hxx>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <Base/Console.h>
|
#include <Base/Console.h>
|
||||||
|
@ -569,25 +570,25 @@ void CmdPartCompJoinFeatures::languageChange()
|
||||||
Gui::Command* joinConnect = rcCmdMgr.getCommandByName("Part_JoinConnect");
|
Gui::Command* joinConnect = rcCmdMgr.getCommandByName("Part_JoinConnect");
|
||||||
if (joinConnect) {
|
if (joinConnect) {
|
||||||
QAction* cmd0 = a[0];
|
QAction* cmd0 = a[0];
|
||||||
cmd0->setText(QApplication::translate("PartCompJoinFeatures", joinConnect->getMenuText()));
|
cmd0->setText(QApplication::translate("Part_JoinFeatures", joinConnect->getMenuText()));
|
||||||
cmd0->setToolTip(QApplication::translate("Part_JoinConnect", joinConnect->getToolTipText()));
|
cmd0->setToolTip(QApplication::translate("Part_JoinFeatures", joinConnect->getToolTipText()));
|
||||||
cmd0->setStatusTip(QApplication::translate("Part_JoinConnect", joinConnect->getStatusTip()));
|
cmd0->setStatusTip(QApplication::translate("Part_JoinFeatures", joinConnect->getStatusTip()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Gui::Command* joinEmbed = rcCmdMgr.getCommandByName("Part_JoinEmbed");
|
Gui::Command* joinEmbed = rcCmdMgr.getCommandByName("Part_JoinEmbed");
|
||||||
if (joinEmbed) {
|
if (joinEmbed) {
|
||||||
QAction* cmd1 = a[1];
|
QAction* cmd1 = a[1];
|
||||||
cmd1->setText(QApplication::translate("PartCompJoinFeatures", joinEmbed->getMenuText()));
|
cmd1->setText(QApplication::translate("Part_JoinFeatures", joinEmbed->getMenuText()));
|
||||||
cmd1->setToolTip(QApplication::translate("Part_JoinEmbed", joinEmbed->getToolTipText()));
|
cmd1->setToolTip(QApplication::translate("Part_JoinFeatures", joinEmbed->getToolTipText()));
|
||||||
cmd1->setStatusTip(QApplication::translate("Part_JoinEmbed", joinEmbed->getStatusTip()));
|
cmd1->setStatusTip(QApplication::translate("Part_JoinFeatures", joinEmbed->getStatusTip()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Gui::Command* joinCutout = rcCmdMgr.getCommandByName("Part_JoinCutout");
|
Gui::Command* joinCutout = rcCmdMgr.getCommandByName("Part_JoinCutout");
|
||||||
if (joinCutout) {
|
if (joinCutout) {
|
||||||
QAction* cmd2 = a[2];
|
QAction* cmd2 = a[2];
|
||||||
cmd2->setText(QApplication::translate("PartCompJoinFeatures", joinCutout->getMenuText()));
|
cmd2->setText(QApplication::translate("Part_JoinFeatures", joinCutout->getMenuText()));
|
||||||
cmd2->setToolTip(QApplication::translate("Part_JoinCutout", joinCutout->getToolTipText()));
|
cmd2->setToolTip(QApplication::translate("Part_JoinFeatures", joinCutout->getToolTipText()));
|
||||||
cmd2->setStatusTip(QApplication::translate("Part_JoinCutout", joinCutout->getStatusTip()));
|
cmd2->setStatusTip(QApplication::translate("Part_JoinFeatures", joinCutout->getStatusTip()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -599,6 +600,116 @@ bool CmdPartCompJoinFeatures::isActive(void)
|
||||||
return false;
|
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
|
// Part_Compound
|
||||||
//===========================================================================
|
//===========================================================================
|
||||||
|
@ -1909,6 +2020,7 @@ void CreatePartCommands(void)
|
||||||
rcCmdMgr.addCommand(new CmdPartCut());
|
rcCmdMgr.addCommand(new CmdPartCut());
|
||||||
rcCmdMgr.addCommand(new CmdPartFuse());
|
rcCmdMgr.addCommand(new CmdPartFuse());
|
||||||
rcCmdMgr.addCommand(new CmdPartCompJoinFeatures());
|
rcCmdMgr.addCommand(new CmdPartCompJoinFeatures());
|
||||||
|
rcCmdMgr.addCommand(new CmdPartCompSplitFeatures());
|
||||||
rcCmdMgr.addCommand(new CmdPartCompound());
|
rcCmdMgr.addCommand(new CmdPartCompound());
|
||||||
rcCmdMgr.addCommand(new CmdPartSection());
|
rcCmdMgr.addCommand(new CmdPartSection());
|
||||||
//rcCmdMgr.addCommand(new CmdPartBox2());
|
//rcCmdMgr.addCommand(new CmdPartBox2());
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<file>icons/PartFeatureImport.xpm</file>
|
<file>icons/PartFeatureImport.xpm</file>
|
||||||
<file>icons/Part_Attachment.svg</file>
|
<file>icons/Part_Attachment.svg</file>
|
||||||
<file>icons/Part_Booleans.svg</file>
|
<file>icons/Part_Booleans.svg</file>
|
||||||
|
<file>icons/Part_BooleanFragments.svg</file>
|
||||||
<file>icons/Part_Box.svg</file>
|
<file>icons/Part_Box.svg</file>
|
||||||
<file>icons/Part_Chamfer.svg</file>
|
<file>icons/Part_Chamfer.svg</file>
|
||||||
<file>icons/Part_Common.svg</file>
|
<file>icons/Part_Common.svg</file>
|
||||||
|
@ -28,10 +29,12 @@
|
||||||
<file>icons/Part_CrossSections.svg</file>
|
<file>icons/Part_CrossSections.svg</file>
|
||||||
<file>icons/Part_Shapebuilder.png</file>
|
<file>icons/Part_Shapebuilder.png</file>
|
||||||
<file>icons/Part_ShapeInfo.svg</file>
|
<file>icons/Part_ShapeInfo.svg</file>
|
||||||
|
<file>icons/Part_Slice.svg</file>
|
||||||
<file>icons/Part_Sphere.svg</file>
|
<file>icons/Part_Sphere.svg</file>
|
||||||
<file>icons/Part_Sweep.svg</file>
|
<file>icons/Part_Sweep.svg</file>
|
||||||
<file>icons/Part_Thickness.svg</file>
|
<file>icons/Part_Thickness.svg</file>
|
||||||
<file>icons/Part_Torus.svg</file>
|
<file>icons/Part_Torus.svg</file>
|
||||||
|
<file>icons/Part_XOR.svg</file>
|
||||||
<file>icons/preferences-part_design.svg</file>
|
<file>icons/preferences-part_design.svg</file>
|
||||||
<file>icons/Tree_Part.svg</file>
|
<file>icons/Tree_Part.svg</file>
|
||||||
<file>icons/Part_CheckGeometry.svg</file>
|
<file>icons/Part_CheckGeometry.svg</file>
|
||||||
|
|
206
src/Mod/Part/Gui/Resources/icons/Part_BooleanFragments.svg
Normal file
206
src/Mod/Part/Gui/Resources/icons/Part_BooleanFragments.svg
Normal 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 |
161
src/Mod/Part/Gui/Resources/icons/Part_Slice.svg
Normal file
161
src/Mod/Part/Gui/Resources/icons/Part_Slice.svg
Normal 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 |
196
src/Mod/Part/Gui/Resources/icons/Part_XOR.svg
Normal file
196
src/Mod/Part/Gui/Resources/icons/Part_XOR.svg
Normal 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 |
|
@ -71,6 +71,10 @@ Gui::MenuItem* Workbench::setupMenuBar() const
|
||||||
join->setCommand("Join");
|
join->setCommand("Join");
|
||||||
*join << "Part_JoinConnect" << "Part_JoinEmbed" << "Part_JoinCutout";
|
*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;
|
Gui::MenuItem* part = new Gui::MenuItem;
|
||||||
root->insertItem(item, part);
|
root->insertItem(item, part);
|
||||||
part->setCommand("&Part");
|
part->setCommand("&Part");
|
||||||
|
@ -78,7 +82,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const
|
||||||
*part << prim << "Part_Primitives" << "Part_Builder" << "Separator"
|
*part << prim << "Part_Primitives" << "Part_Builder" << "Separator"
|
||||||
<< "Part_ShapeFromMesh" << "Part_MakeSolid" << "Part_ReverseShape"
|
<< "Part_ShapeFromMesh" << "Part_MakeSolid" << "Part_ReverseShape"
|
||||||
<< "Part_SimpleCopy" << "Part_RefineShape" << "Part_CheckGeometry"
|
<< "Part_SimpleCopy" << "Part_RefineShape" << "Part_CheckGeometry"
|
||||||
<< "Separator" << bop << join << "Separator"
|
<< "Separator" << bop << join << split << "Separator"
|
||||||
<< "Part_CrossSections" << "Part_Compound" << "Part_MakeFace" << "Part_Extrude"
|
<< "Part_CrossSections" << "Part_Compound" << "Part_MakeFace" << "Part_Extrude"
|
||||||
<< "Part_Revolve" << "Part_Mirror" << "Part_Fillet" << "Part_Chamfer"
|
<< "Part_Revolve" << "Part_Mirror" << "Part_Fillet" << "Part_Chamfer"
|
||||||
<< "Part_RuledSurface" << "Part_Loft" << "Part_Sweep"
|
<< "Part_RuledSurface" << "Part_Loft" << "Part_Sweep"
|
||||||
|
@ -123,7 +127,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const
|
||||||
Gui::ToolBarItem* boolop = new Gui::ToolBarItem(root);
|
Gui::ToolBarItem* boolop = new Gui::ToolBarItem(root);
|
||||||
boolop->setCommand("Boolean");
|
boolop->setCommand("Boolean");
|
||||||
*boolop << "Part_Boolean" << "Part_Cut" << "Part_Fuse" << "Part_Common"
|
*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";
|
<< "Part_CrossSections";
|
||||||
|
|
||||||
Gui::ToolBarItem* measure = new Gui::ToolBarItem(root);
|
Gui::ToolBarItem* measure = new Gui::ToolBarItem(root);
|
||||||
|
|
|
@ -43,9 +43,9 @@ class PartWorkbench ( Workbench ):
|
||||||
import PartGui
|
import PartGui
|
||||||
import Part
|
import Part
|
||||||
try:
|
try:
|
||||||
import JoinFeatures
|
Part.BOPTools.addCommands()
|
||||||
except ImportError:
|
except Exception as err:
|
||||||
print "JoinFeatures module cannot be loaded"
|
FreeCAD.Console.PrintError("Features from BOPTools package cannot be loaded. {err}\n".format(err= err.message))
|
||||||
|
|
||||||
def GetClassName(self):
|
def GetClassName(self):
|
||||||
return "PartGui::Workbench"
|
return "PartGui::Workbench"
|
||||||
|
|
|
@ -23,30 +23,32 @@
|
||||||
|
|
||||||
import FreeCAD, Part
|
import FreeCAD, Part
|
||||||
|
|
||||||
if FreeCAD.GuiUp:
|
#if FreeCAD.GuiUp:
|
||||||
import FreeCADGui
|
# import FreeCADGui
|
||||||
from PySide import QtCore, QtGui
|
# from PySide import QtCore, QtGui
|
||||||
|
|
||||||
__title__="JoinFeatures module"
|
__title__="JoinFeatures module (legacy)"
|
||||||
__author__ = "DeepSOIC"
|
__author__ = "DeepSOIC"
|
||||||
__url__ = "http://www.freecadweb.org"
|
__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 ----------------------------------------
|
##-------------------------- translation-related code ----------------------------------------
|
||||||
#Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!"
|
##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 )
|
##http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
|
||||||
try:
|
#try:
|
||||||
_fromUtf8 = QtCore.QString.fromUtf8
|
# _fromUtf8 = QtCore.QString.fromUtf8
|
||||||
except AttributeError:
|
#except AttributeError:
|
||||||
def _fromUtf8(s):
|
# def _fromUtf8(s):
|
||||||
return s
|
# return s
|
||||||
try:
|
#try:
|
||||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
# _encoding = QtGui.QApplication.UnicodeUTF8
|
||||||
def _translate(context, text, disambig):
|
# def _translate(context, text, disambig):
|
||||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
# return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||||
except AttributeError:
|
#except AttributeError:
|
||||||
def _translate(context, text, disambig):
|
# def _translate(context, text, disambig):
|
||||||
return QtGui.QApplication.translate(context, text, disambig)
|
# return QtGui.QApplication.translate(context, text, disambig)
|
||||||
#--------------------------/translation-related code ----------------------------------------
|
##--------------------------/translation-related code ----------------------------------------
|
||||||
|
|
||||||
# -------------------------- common stuff --------------------------------------------------
|
# -------------------------- common stuff --------------------------------------------------
|
||||||
def getParamRefine():
|
def getParamRefine():
|
||||||
|
@ -71,14 +73,14 @@ def shapeOfMaxVol(compound):
|
||||||
else:
|
else:
|
||||||
return compound
|
return compound
|
||||||
|
|
||||||
def makePartJoinFeature(name, mode = 'bypass'):
|
#def makePartJoinFeature(name, mode = 'bypass'):
|
||||||
'''makePartJoinFeature(name, mode = 'bypass'): makes an PartJoinFeature object.'''
|
# '''makePartJoinFeature(name, mode = 'bypass'): makes an PartJoinFeature object.'''
|
||||||
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
|
# obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
|
||||||
_PartJoinFeature(obj)
|
# _PartJoinFeature(obj)
|
||||||
obj.Mode = mode
|
# obj.Mode = mode
|
||||||
obj.Refine = getParamRefine()
|
# obj.Refine = getParamRefine()
|
||||||
_ViewProviderPartJoinFeature(obj.ViewObject)
|
# _ViewProviderPartJoinFeature(obj.ViewObject)
|
||||||
return obj
|
# return obj
|
||||||
|
|
||||||
class _PartJoinFeature:
|
class _PartJoinFeature:
|
||||||
"The PartJoinFeature object"
|
"The PartJoinFeature object"
|
||||||
|
@ -159,133 +161,134 @@ class _ViewProviderPartJoinFeature:
|
||||||
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
|
FreeCAD.Console.PrintError("Error in onDelete: " + err.message)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
#
|
||||||
def CreateJoinFeature(name, mode):
|
#def CreateJoinFeature(name, mode):
|
||||||
sel = FreeCADGui.Selection.getSelectionEx()
|
# sel = FreeCADGui.Selection.getSelectionEx()
|
||||||
FreeCAD.ActiveDocument.openTransaction("Create "+mode+"ObjectsFeature")
|
# FreeCAD.ActiveDocument.openTransaction("Create "+mode+"ObjectsFeature")
|
||||||
FreeCADGui.addModule("JoinFeatures")
|
# FreeCADGui.addModule("JoinFeatures")
|
||||||
FreeCADGui.doCommand("j = JoinFeatures.makePartJoinFeature(name = '"+name+"', mode = '"+mode+"' )")
|
# FreeCADGui.doCommand("j = JoinFeatures.makePartJoinFeature(name = '"+name+"', mode = '"+mode+"' )")
|
||||||
FreeCADGui.doCommand("j.Base = App.ActiveDocument."+sel[0].Object.Name)
|
# FreeCADGui.doCommand("j.Base = App.ActiveDocument."+sel[0].Object.Name)
|
||||||
FreeCADGui.doCommand("j.Tool = App.ActiveDocument."+sel[1].Object.Name)
|
# FreeCADGui.doCommand("j.Tool = App.ActiveDocument."+sel[1].Object.Name)
|
||||||
try:
|
# try:
|
||||||
FreeCADGui.doCommand("j.Proxy.execute(j)")
|
# FreeCADGui.doCommand("j.Proxy.execute(j)")
|
||||||
FreeCADGui.doCommand("j.purgeTouched()")
|
# FreeCADGui.doCommand("j.purgeTouched()")
|
||||||
except Exception as err:
|
# except Exception as err:
|
||||||
mb = QtGui.QMessageBox()
|
# mb = QtGui.QMessageBox()
|
||||||
mb.setIcon(mb.Icon.Warning)
|
# 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)
|
# 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))
|
# .format(err= err.message))
|
||||||
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
|
# mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
|
||||||
btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
|
# btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
|
||||||
btnOK = mb.addButton(_translate("Part_JoinFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
|
# btnOK = mb.addButton(_translate("Part_JoinFeatures","Continue",None), QtGui.QMessageBox.ButtonRole.ActionRole)
|
||||||
mb.setDefaultButton(btnOK)
|
# mb.setDefaultButton(btnOK)
|
||||||
|
#
|
||||||
mb.exec_()
|
# mb.exec_()
|
||||||
|
#
|
||||||
if mb.clickedButton() is btnAbort:
|
# if mb.clickedButton() is btnAbort:
|
||||||
FreeCAD.ActiveDocument.abortTransaction()
|
# FreeCAD.ActiveDocument.abortTransaction()
|
||||||
return
|
# return
|
||||||
|
#
|
||||||
FreeCADGui.doCommand("j.Base.ViewObject.hide()")
|
# FreeCADGui.doCommand("j.Base.ViewObject.hide()")
|
||||||
FreeCADGui.doCommand("j.Tool.ViewObject.hide()")
|
# FreeCADGui.doCommand("j.Tool.ViewObject.hide()")
|
||||||
|
#
|
||||||
FreeCAD.ActiveDocument.commitTransaction()
|
# FreeCAD.ActiveDocument.commitTransaction()
|
||||||
|
|
||||||
def getIconPath(icon_dot_svg):
|
def getIconPath(icon_dot_svg):
|
||||||
return ":/icons/" + icon_dot_svg
|
return ":/icons/" + icon_dot_svg
|
||||||
|
|
||||||
# -------------------------- /common stuff --------------------------------------------------
|
# -------------------------- /common stuff --------------------------------------------------
|
||||||
|
|
||||||
# -------------------------- ConnectObjectsFeature --------------------------------------------------
|
## -------------------------- ConnectObjectsFeature --------------------------------------------------
|
||||||
|
#
|
||||||
class _CommandConnectFeature:
|
#class _CommandConnectFeature:
|
||||||
"Command to create PartJoinFeature in Connect mode"
|
# "Command to create PartJoinFeature in Connect mode"
|
||||||
def GetResources(self):
|
# def GetResources(self):
|
||||||
return {'Pixmap' : getIconPath("Part_JoinConnect.svg"),
|
# return {'Pixmap' : getIconPath("Part_JoinConnect.svg"),
|
||||||
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Connect objects"),
|
# 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Connect objects"),
|
||||||
'Accel': "",
|
# 'Accel': "",
|
||||||
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Fuses objects, taking care to preserve voids.")}
|
# 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Fuses objects, taking care to preserve voids.")}
|
||||||
|
#
|
||||||
def Activated(self):
|
# def Activated(self):
|
||||||
if len(FreeCADGui.Selection.getSelectionEx()) == 2 :
|
# if len(FreeCADGui.Selection.getSelectionEx()) == 2 :
|
||||||
CreateJoinFeature(name = "Connect", mode = "Connect")
|
# CreateJoinFeature(name = "Connect", mode = "Connect")
|
||||||
else:
|
# else:
|
||||||
mb = QtGui.QMessageBox()
|
# mb = QtGui.QMessageBox()
|
||||||
mb.setIcon(mb.Icon.Warning)
|
# mb.setIcon(mb.Icon.Warning)
|
||||||
mb.setText(_translate("Part_JoinFeatures", "Two solids need to be selected, first!", None))
|
# mb.setText(_translate("Part_JoinFeatures", "Two solids need to be selected, first!", None))
|
||||||
mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
|
# mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
|
||||||
mb.exec_()
|
# mb.exec_()
|
||||||
|
#
|
||||||
def IsActive(self):
|
# def IsActive(self):
|
||||||
if FreeCAD.ActiveDocument:
|
# if FreeCAD.ActiveDocument:
|
||||||
return True
|
# return True
|
||||||
else:
|
# else:
|
||||||
return False
|
# return False
|
||||||
|
#
|
||||||
FreeCADGui.addCommand('Part_JoinConnect',_CommandConnectFeature())
|
#FreeCADGui.addCommand('Part_JoinConnect',_CommandConnectFeature())
|
||||||
|
#
|
||||||
# -------------------------- /ConnectObjectsFeature --------------------------------------------------
|
## -------------------------- /ConnectObjectsFeature --------------------------------------------------
|
||||||
|
#
|
||||||
|
#
|
||||||
# -------------------------- EmbedFeature --------------------------------------------------
|
## -------------------------- EmbedFeature --------------------------------------------------
|
||||||
|
#
|
||||||
class _CommandEmbedFeature:
|
#class _CommandEmbedFeature:
|
||||||
"Command to create PartJoinFeature in Embed mode"
|
# "Command to create PartJoinFeature in Embed mode"
|
||||||
def GetResources(self):
|
# def GetResources(self):
|
||||||
return {'Pixmap' : getIconPath("Part_JoinEmbed.svg"),
|
# return {'Pixmap' : getIconPath("Part_JoinEmbed.svg"),
|
||||||
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Embed object"),
|
# 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Embed object"),
|
||||||
'Accel': "",
|
# 'Accel': "",
|
||||||
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Fuses one object into another, taking care to preserve voids.")}
|
# 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Fuses one object into another, taking care to preserve voids.")}
|
||||||
|
#
|
||||||
def Activated(self):
|
# def Activated(self):
|
||||||
if len(FreeCADGui.Selection.getSelection()) == 2 :
|
# if len(FreeCADGui.Selection.getSelection()) == 2 :
|
||||||
CreateJoinFeature(name = "Embed", mode = "Embed")
|
# CreateJoinFeature(name = "Embed", mode = "Embed")
|
||||||
else:
|
# else:
|
||||||
mb = QtGui.QMessageBox()
|
# mb = QtGui.QMessageBox()
|
||||||
mb.setIcon(mb.Icon.Warning)
|
# mb.setIcon(mb.Icon.Warning)
|
||||||
mb.setText(_translate("Part_JoinFeatures","Select base object, then the object to embed, and invoke this tool.", None))
|
# 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.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
|
||||||
mb.exec_()
|
# mb.exec_()
|
||||||
|
#
|
||||||
|
#
|
||||||
def IsActive(self):
|
# def IsActive(self):
|
||||||
if FreeCAD.ActiveDocument:
|
# if FreeCAD.ActiveDocument:
|
||||||
return True
|
# return True
|
||||||
else:
|
# else:
|
||||||
return False
|
# return False
|
||||||
|
#
|
||||||
FreeCADGui.addCommand('Part_JoinEmbed',_CommandEmbedFeature())
|
#FreeCADGui.addCommand('Part_JoinEmbed',_CommandEmbedFeature())
|
||||||
|
#
|
||||||
# -------------------------- /EmbedFeature --------------------------------------------------
|
## -------------------------- /EmbedFeature --------------------------------------------------
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
# -------------------------- CutoutFeature --------------------------------------------------
|
## -------------------------- CutoutFeature --------------------------------------------------
|
||||||
|
#
|
||||||
class _CommandCutoutFeature:
|
#class _CommandCutoutFeature:
|
||||||
"Command to create PartJoinFeature in Cutout mode"
|
# "Command to create PartJoinFeature in Cutout mode"
|
||||||
def GetResources(self):
|
# def GetResources(self):
|
||||||
return {'Pixmap' : getIconPath("Part_JoinCutout.svg"),
|
# return {'Pixmap' : getIconPath("Part_JoinCutout.svg"),
|
||||||
'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Cutout for object"),
|
# 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Cutout for object"),
|
||||||
'Accel': "",
|
# 'Accel': "",
|
||||||
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Makes a cutout in one object to fit another object.")}
|
# 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Makes a cutout in one object to fit another object.")}
|
||||||
|
#
|
||||||
def Activated(self):
|
# def Activated(self):
|
||||||
if len(FreeCADGui.Selection.getSelection()) == 2 :
|
# if len(FreeCADGui.Selection.getSelection()) == 2 :
|
||||||
CreateJoinFeature(name = "Cutout", mode = "Cutout")
|
# CreateJoinFeature(name = "Cutout", mode = "Cutout")
|
||||||
else:
|
# else:
|
||||||
mb = QtGui.QMessageBox()
|
# mb = QtGui.QMessageBox()
|
||||||
mb.setIcon(mb.Icon.Warning)
|
# 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.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.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None))
|
||||||
mb.exec_()
|
# mb.exec_()
|
||||||
|
#
|
||||||
def IsActive(self):
|
# def IsActive(self):
|
||||||
if FreeCAD.ActiveDocument:
|
# if FreeCAD.ActiveDocument:
|
||||||
return True
|
# return True
|
||||||
else:
|
# else:
|
||||||
return False
|
# return False
|
||||||
|
#
|
||||||
FreeCADGui.addCommand('Part_JoinCutout',_CommandCutoutFeature())
|
#FreeCADGui.addCommand('Part_JoinCutout',_CommandCutoutFeature())
|
||||||
|
#
|
||||||
# -------------------------- /CutoutFeature --------------------------------------------------
|
## -------------------------- /CutoutFeature --------------------------------------------------
|
||||||
|
#
|
Loading…
Reference in New Issue
Block a user