557 lines
22 KiB
Python
557 lines
22 KiB
Python
# ***************************************************************************
|
|
# * Copyright (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net> *
|
|
# * Copyright (c) 2009, 2010 Ken Cline <cline@frii.com> *
|
|
# * Copyright (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
|
|
# * *
|
|
# * This program is free software; you can redistribute it and/or modify *
|
|
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
# * as published by the Free Software Foundation; either version 2 of *
|
|
# * the License, or (at your option) any later version. *
|
|
# * for detail see the LICENCE text file. *
|
|
# * *
|
|
# * This program 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 program; if not, write to the Free Software *
|
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
# * USA *
|
|
# * *
|
|
# ***************************************************************************
|
|
"""Provides functions to upgrade objects by different methods.
|
|
|
|
See also the `downgrade` function.
|
|
"""
|
|
## @package downgrade
|
|
# \ingroup draftfunctions
|
|
# \brief Provides functions to upgrade objects by different methods.
|
|
|
|
import re
|
|
import lazy_loader.lazy_loader as lz
|
|
|
|
import FreeCAD as App
|
|
from draftfunctions import draftify
|
|
from draftfunctions import fuse
|
|
from draftgeoutils.geometry import is_straight_line
|
|
from draftmake import make_block
|
|
from draftmake import make_line
|
|
from draftmake import make_wire
|
|
from draftutils import gui_utils
|
|
from draftutils import params
|
|
from draftutils import utils
|
|
from draftutils.messages import _msg, _err
|
|
from draftutils.translate import translate
|
|
|
|
# Delay import of module until first use because it is heavy
|
|
Part = lz.LazyLoader("Part", globals(), "Part")
|
|
DraftGeomUtils = lz.LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
|
|
Arch = lz.LazyLoader("Arch", globals(), "Arch")
|
|
|
|
_DEBUG = False
|
|
|
|
## \addtogroup draftfunctions
|
|
# @{
|
|
|
|
|
|
def upgrade(objects, delete=False, force=None):
|
|
"""Upgrade the given objects.
|
|
|
|
This is a counterpart to `downgrade`.
|
|
|
|
Parameters
|
|
----------
|
|
objects: Part::Feature or list
|
|
A single object to upgrade or a list
|
|
containing various such objects.
|
|
|
|
delete: bool, optional
|
|
It defaults to `False`.
|
|
If it is `True`, the old objects are deleted, and only the resulting
|
|
object is kept.
|
|
|
|
force: str, optional
|
|
It defaults to `None`.
|
|
Its value can be used to force a certain method of upgrading.
|
|
It can be any of: `'makeCompound'`, `'closeGroupWires'`,
|
|
`'makeSolid'`, `'closeWire'`, `'turnToParts'`, `'makeFusion'`,
|
|
`'makeShell'`, `'makeFaces'`, `'draftify'`, `'joinFaces'`,
|
|
`'makeSketchFace'`, `'makeWires'`.
|
|
|
|
Returns
|
|
-------
|
|
tuple
|
|
A tuple containing two lists, a list of new objects
|
|
and a list of objects to be deleted.
|
|
|
|
None
|
|
If there is a problem it will return `None`.
|
|
|
|
See Also
|
|
--------
|
|
downgrade
|
|
"""
|
|
_name = "upgrade"
|
|
|
|
if not isinstance(objects, list):
|
|
objects = [objects]
|
|
|
|
delete_list = []
|
|
add_list = []
|
|
doc = App.ActiveDocument
|
|
|
|
# definitions of actions to perform
|
|
def turnToLine(obj):
|
|
"""Turn an edge into a Draft Line."""
|
|
p1 = obj.Shape.Vertexes[0].Point
|
|
p2 = obj.Shape.Vertexes[-1].Point
|
|
newobj = make_line.make_line(p1, p2)
|
|
add_list.append(newobj)
|
|
delete_list.append(obj)
|
|
return newobj
|
|
|
|
def makeCompound(objectslist):
|
|
"""Return a compound object made from the given objects."""
|
|
newobj = make_block.make_block(objectslist)
|
|
add_list.append(newobj)
|
|
return newobj
|
|
|
|
def closeGroupWires(groupslist):
|
|
"""Close every open wire in the given groups."""
|
|
result = False
|
|
for grp in groupslist:
|
|
for obj in grp.Group:
|
|
newobj = closeWire(obj)
|
|
# add new objects to their respective groups
|
|
if newobj:
|
|
result = True
|
|
grp.addObject(newobj)
|
|
return result
|
|
|
|
def makeSolid(obj):
|
|
"""Turn an object into a solid, if possible."""
|
|
if obj.Shape.Solids:
|
|
return None
|
|
sol = None
|
|
try:
|
|
sol = Part.makeSolid(obj.Shape)
|
|
except Part.OCCError:
|
|
return None
|
|
else:
|
|
if sol:
|
|
if sol.isClosed():
|
|
newobj = doc.addObject("Part::Feature", "Solid")
|
|
newobj.Shape = sol
|
|
add_list.append(newobj)
|
|
delete_list.append(obj)
|
|
return newobj
|
|
else:
|
|
_err(translate("draft","Object must be a closed shape"))
|
|
else:
|
|
_err(translate("draft","No solid object created"))
|
|
return None
|
|
|
|
def closeWire(obj):
|
|
"""Close a wire object, if possible."""
|
|
if obj.Shape.Faces:
|
|
return None
|
|
if len(obj.Shape.Wires) != 1:
|
|
return None
|
|
if len(obj.Shape.Edges) == 1:
|
|
return None
|
|
if is_straight_line(obj.Shape):
|
|
return None
|
|
if utils.get_type(obj) == "Wire":
|
|
obj.Closed = True
|
|
return True
|
|
else:
|
|
w = obj.Shape.Wires[0]
|
|
if not w.isClosed():
|
|
edges = w.Edges
|
|
p0 = w.Vertexes[0].Point
|
|
p1 = w.Vertexes[-1].Point
|
|
if p0 == p1:
|
|
# sometimes an open wire can have the same start
|
|
# and end points (OCCT bug); in this case,
|
|
# although it is not closed, the face works.
|
|
f = Part.Face(w)
|
|
newobj = doc.addObject("Part::Feature", "Face")
|
|
newobj.Shape = f
|
|
else:
|
|
edges.append(Part.LineSegment(p1, p0).toShape())
|
|
w = Part.Wire(Part.__sortEdges__(edges))
|
|
newobj = doc.addObject("Part::Feature", "Wire")
|
|
newobj.Shape = w
|
|
add_list.append(newobj)
|
|
delete_list.append(obj)
|
|
return newobj
|
|
else:
|
|
return None
|
|
|
|
def turnToParts(meshes):
|
|
"""Turn given meshes to parts."""
|
|
result = False
|
|
for mesh in meshes:
|
|
sh = Arch.getShapeFromMesh(mesh.Mesh)
|
|
if sh:
|
|
newobj = doc.addObject("Part::Feature", "Shell")
|
|
newobj.Shape = sh
|
|
add_list.append(newobj)
|
|
delete_list.append(mesh)
|
|
result = True
|
|
return result
|
|
|
|
def makeFusion(obj1, obj2=None):
|
|
"""Make a Draft or Part fusion between 2 given objects."""
|
|
if not obj2 and isinstance(obj1, (list, tuple)):
|
|
obj1, obj2 = obj1[0], obj1[1]
|
|
|
|
newobj = fuse.fuse(obj1, obj2)
|
|
if newobj:
|
|
add_list.append(newobj)
|
|
return newobj
|
|
return None
|
|
|
|
def makeShell(objectslist):
|
|
"""Make a shell or compound with the given objects."""
|
|
preserveFaceColor = params.get_param("preserveFaceColor")
|
|
preserveFaceNames = params.get_param("preserveFaceNames")
|
|
faces = []
|
|
facecolors = [[], []] if preserveFaceColor else None
|
|
for obj in objectslist:
|
|
faces.extend(obj.Shape.Faces)
|
|
if App.GuiUp and preserveFaceColor:
|
|
# at this point, obj.Shape.Faces are not in same order as the
|
|
# original faces we might have gotten as a result
|
|
# of downgrade, nor do they have the same hashCode().
|
|
# Nevertheless, they still keep reference to their original
|
|
# colors, capture that in facecolors.
|
|
# Also, cannot use ShapeColor here, we need a whole array
|
|
# matching the colors of the array of faces per object,
|
|
# only DiffuseColor has that
|
|
facecolors[0].extend(obj.ViewObject.DiffuseColor)
|
|
facecolors[1] = faces
|
|
sh = Part.makeShell(faces)
|
|
if sh:
|
|
if sh.Faces:
|
|
newobj = doc.addObject("Part::Feature", str(sh.ShapeType))
|
|
newobj.Shape = sh
|
|
if preserveFaceNames:
|
|
firstName = objectslist[0].Label
|
|
nameNoTrailNumbers = re.sub(r"\d+$", "", firstName)
|
|
newobj.Label = "{} {}".format(newobj.Label,
|
|
nameNoTrailNumbers)
|
|
if App.GuiUp and preserveFaceColor:
|
|
# At this point, sh.Faces are completely new,
|
|
# with different hashCodes and different ordering
|
|
# from obj.Shape.Faces. Since we cannot compare
|
|
# via hashCode(), we have to iterate and use a different
|
|
# criteria to find the original matching color
|
|
colarray = []
|
|
for ind, face in enumerate(newobj.Shape.Faces):
|
|
for fcind, fcface in enumerate(facecolors[1]):
|
|
if (face.Area == fcface.Area
|
|
and face.CenterOfMass == fcface.CenterOfMass):
|
|
colarray.append(facecolors[0][fcind])
|
|
break
|
|
newobj.ViewObject.DiffuseColor = colarray
|
|
add_list.append(newobj)
|
|
delete_list.extend(objectslist)
|
|
return newobj
|
|
return None
|
|
|
|
def joinFaces(objectslist, coplanarity=False, checked=False):
|
|
"""Make one big face from selected objects, if possible."""
|
|
faces = []
|
|
for obj in objectslist:
|
|
faces.extend(obj.Shape.Faces)
|
|
|
|
# check coplanarity if needed
|
|
if not checked:
|
|
coplanarity = DraftGeomUtils.is_coplanar(faces, 1e-3)
|
|
if not coplanarity:
|
|
_err(translate("draft","Faces must be coplanar to be refined"))
|
|
return None
|
|
|
|
# fuse faces
|
|
fuse_face = faces.pop(0)
|
|
for face in faces:
|
|
fuse_face = fuse_face.fuse(face)
|
|
|
|
face = DraftGeomUtils.concatenate(fuse_face)
|
|
# to prevent create new object if concatenate fails
|
|
if face.isEqual(fuse_face):
|
|
face = None
|
|
|
|
if face:
|
|
# several coplanar and non-curved faces,
|
|
# they can become a Draft Wire
|
|
if (not DraftGeomUtils.hasCurves(face)
|
|
and len(face.Wires) == 1):
|
|
newobj = make_wire.make_wire(face.Wires[0],
|
|
closed=True, face=True)
|
|
# if not possible, we do a non-parametric union
|
|
else:
|
|
newobj = doc.addObject("Part::Feature", "Union")
|
|
newobj.Shape = face
|
|
add_list.append(newobj)
|
|
delete_list.extend(objectslist)
|
|
return newobj
|
|
return None
|
|
|
|
def makeSketchFace(obj):
|
|
"""Make a face from a sketch."""
|
|
face = Part.makeFace(obj.Shape.Wires, "Part::FaceMakerBullseye")
|
|
if face:
|
|
newobj = doc.addObject("Part::Feature", "Face")
|
|
newobj.Shape = face
|
|
|
|
add_list.append(newobj)
|
|
if App.GuiUp:
|
|
obj.ViewObject.Visibility = False
|
|
return newobj
|
|
return None
|
|
|
|
def makeFaces(objectslist):
|
|
"""Make a face from every closed wire in the list."""
|
|
result = False
|
|
for o in objectslist:
|
|
for w in o.Shape.Wires:
|
|
try:
|
|
f = Part.Face(w)
|
|
except Part.OCCError:
|
|
pass
|
|
else:
|
|
newobj = doc.addObject("Part::Feature", "Face")
|
|
newobj.Shape = f
|
|
add_list.append(newobj)
|
|
result = True
|
|
if o not in delete_list:
|
|
delete_list.append(o)
|
|
return result
|
|
|
|
def makeWires(objectslist):
|
|
"""Join edges in the given objects list into wires."""
|
|
edges = []
|
|
for object in objectslist:
|
|
for edge in object.Shape.Edges:
|
|
edges.append(edge)
|
|
|
|
try:
|
|
sorted_edges = Part.sortEdges(edges)
|
|
if _DEBUG:
|
|
for item_sorted_edges in sorted_edges:
|
|
for e in item_sorted_edges:
|
|
print("Curve: {}".format(e.Curve))
|
|
print("first: {}, last: {}".format(e.Vertexes[0].Point,
|
|
e.Vertexes[-1].Point))
|
|
wires = [Part.Wire(e) for e in sorted_edges]
|
|
except Part.OCCError:
|
|
return None
|
|
else:
|
|
if (len(objectslist) > 1) and (len(wires) == len(objectslist)):
|
|
# we still have the same number of objects, we actually didn't join anything!
|
|
return makeCompound(objectslist)
|
|
for wire in wires:
|
|
newobj = doc.addObject("Part::Feature", "Wire")
|
|
newobj.Shape = wire
|
|
add_list.append(newobj)
|
|
# delete object only if there are no links to it
|
|
# TODO: A more refined criteria to delete object
|
|
for object in objectslist:
|
|
if object.InList:
|
|
if App.GuiUp:
|
|
object.ViewObject.Visibility = False
|
|
else:
|
|
delete_list.append(object)
|
|
return True
|
|
return None
|
|
|
|
# analyzing what we have in our selection
|
|
edges = []
|
|
wires = []
|
|
openwires = []
|
|
faces = []
|
|
groups = []
|
|
parts = []
|
|
curves = []
|
|
facewires = []
|
|
loneedges = []
|
|
meshes = []
|
|
|
|
for ob in objects:
|
|
if ob.TypeId == "App::DocumentObjectGroup":
|
|
groups.append(ob)
|
|
elif hasattr(ob, 'Shape'):
|
|
parts.append(ob)
|
|
faces.extend(ob.Shape.Faces)
|
|
wires.extend(ob.Shape.Wires)
|
|
edges.extend(ob.Shape.Edges)
|
|
for f in ob.Shape.Faces:
|
|
facewires.extend(f.Wires)
|
|
wirededges = []
|
|
for w in ob.Shape.Wires:
|
|
if len(w.Edges) > 1:
|
|
for e in w.Edges:
|
|
wirededges.append(e.hashCode())
|
|
if not w.isClosed():
|
|
openwires.append(w)
|
|
for e in ob.Shape.Edges:
|
|
if DraftGeomUtils.geomType(e) != "Line":
|
|
curves.append(e)
|
|
if not e.hashCode() in wirededges and not e.isClosed():
|
|
loneedges.append(e)
|
|
elif ob.isDerivedFrom("Mesh::Feature"):
|
|
meshes.append(ob)
|
|
objects = parts
|
|
|
|
if _DEBUG:
|
|
print("objects: {}, edges: {}".format(objects, edges))
|
|
print("wires: {}, openwires: {}".format(wires, openwires))
|
|
print("faces: {}".format(faces))
|
|
print("groups: {}, curves: {}".format(groups, curves))
|
|
print("facewires: {}, loneedges: {}".format(facewires, loneedges))
|
|
|
|
if force:
|
|
all_func = {"makeCompound" : makeCompound,
|
|
"closeGroupWires" : closeGroupWires,
|
|
"makeSolid" : makeSolid,
|
|
"closeWire" : closeWire,
|
|
"turnToParts" : turnToParts,
|
|
"makeFusion" : makeFusion,
|
|
"makeShell" : makeShell,
|
|
"makeFaces" : makeFaces,
|
|
"draftify" : draftify.draftify,
|
|
"joinFaces" : joinFaces,
|
|
"makeSketchFace" : makeSketchFace,
|
|
"makeWires" : makeWires,
|
|
"turnToLine" : turnToLine}
|
|
if force in all_func:
|
|
result = all_func[force](objects)
|
|
else:
|
|
_msg(translate("draft","Upgrade: Unknown force method:") + " " + force)
|
|
result = None
|
|
|
|
else:
|
|
# checking faces coplanarity
|
|
# The precision needed in Part.makeFace is 1e-7. Here we use a
|
|
# higher value to let that function throw the exception when
|
|
# joinFaces is called if the precision is insufficient
|
|
if faces:
|
|
faces_coplanarity = DraftGeomUtils.is_coplanar(faces, 1e-3)
|
|
|
|
# applying transformations automatically
|
|
result = None
|
|
|
|
# if we have a group: turn each closed wire inside into a face
|
|
if groups:
|
|
result = closeGroupWires(groups)
|
|
if result:
|
|
_msg(translate("draft","Found groups: closing each open object inside"))
|
|
|
|
# if we have meshes, we try to turn them into shapes
|
|
elif meshes:
|
|
result = turnToParts(meshes)
|
|
if result:
|
|
_msg(translate("draft","Found meshes: turning into Part shapes"))
|
|
|
|
# we have only faces here, no lone edges
|
|
elif faces and (len(wires) + len(openwires) == len(facewires)):
|
|
# we have one shell: we try to make a solid
|
|
if len(objects) == 1 and len(faces) > 3 and not faces_coplanarity:
|
|
result = makeSolid(objects[0])
|
|
if result:
|
|
_msg(translate("draft","Found 1 solidifiable object: solidifying it"))
|
|
# we have exactly 2 objects: we fuse them
|
|
elif len(objects) == 2 and not curves and not faces_coplanarity:
|
|
result = makeFusion(objects[0], objects[1])
|
|
if result:
|
|
_msg(translate("draft","Found 2 objects: fusing them"))
|
|
# we have many separate faces: we try to make a shell or compound
|
|
elif len(objects) >= 2 and len(faces) > 1 and not loneedges:
|
|
result = makeShell(objects)
|
|
if result:
|
|
_msg(translate("draft","Found several objects: creating a "
|
|
+ str(result.Shape.ShapeType)))
|
|
# we have faces: we try to join them if they are coplanar
|
|
elif len(objects) == 1 and len(faces) > 1:
|
|
result = joinFaces(objects, faces_coplanarity, True)
|
|
if result:
|
|
_msg(translate("draft","Found object with several coplanar faces: refine them"))
|
|
# only one object: if not parametric, we "draftify" it
|
|
elif (len(objects) == 1
|
|
and not objects[0].isDerivedFrom("Part::Part2DObjectPython")):
|
|
result = draftify.draftify(objects[0], delete=False)
|
|
if result:
|
|
add_list.append(result)
|
|
delete_list.append(objects[0])
|
|
_msg(translate("draft","Found 1 non-parametric objects: draftifying it"))
|
|
|
|
# in the following cases there are no faces
|
|
elif not faces:
|
|
# we have only closed wires
|
|
if wires and not openwires and not loneedges:
|
|
# we have a sketch: extract a face
|
|
if (len(objects) == 1
|
|
and objects[0].isDerivedFrom("Sketcher::SketchObject")):
|
|
result = makeSketchFace(objects[0])
|
|
if result:
|
|
_msg(translate("draft","Found 1 closed sketch object: creating a face from it"))
|
|
# only closed wires
|
|
else:
|
|
result = makeFaces(objects)
|
|
if result:
|
|
_msg(translate("draft","Found closed wires: creating faces"))
|
|
# wires or edges: we try to join them
|
|
elif len(objects) > 1 and len(edges) > 1:
|
|
result = makeWires(objects)
|
|
if result:
|
|
_msg(translate("draft","Found several wires or edges: wiring them"))
|
|
else:
|
|
_msg(translate("draft","Found several non-treatable objects: creating compound"))
|
|
# special case, we have only one open wire. We close it,
|
|
# unless it has only 1 edge!
|
|
elif len(objects) == 1 and len(openwires) == 1:
|
|
result = closeWire(objects[0])
|
|
_msg(translate("draft","trying: closing it"))
|
|
if result:
|
|
_msg(translate("draft","Found 1 open wire: closing it"))
|
|
elif (len(objects) == 1 and len(edges) == 1
|
|
and not objects[0].isDerivedFrom("Part::Part2DObjectPython")):
|
|
e = objects[0].Shape.Edges[0]
|
|
edge_type = DraftGeomUtils.geomType(e)
|
|
# currently only support Line and Circle
|
|
if edge_type in ("Line", "Circle"):
|
|
result = draftify.draftify(objects[0], delete=False)
|
|
if result:
|
|
add_list.append(result)
|
|
delete_list.append(objects[0])
|
|
_msg(translate("draft","Found 1 object: draftifying it"))
|
|
# only points, no edges
|
|
elif not edges and len(objects) > 1:
|
|
result = makeCompound(objects)
|
|
if result:
|
|
_msg(translate("draft","Found points: creating compound"))
|
|
# all other cases, if more than 1 object, make a compound
|
|
elif len(objects) > 1:
|
|
result = makeCompound(objects)
|
|
if result:
|
|
_msg(translate("draft","Found several non-treatable objects: creating compound"))
|
|
# no result has been obtained
|
|
if not result:
|
|
_msg(translate("draft","Unable to upgrade these objects."))
|
|
|
|
if delete:
|
|
names = []
|
|
for o in delete_list:
|
|
names.append(o.Name)
|
|
delete_list = []
|
|
for n in names:
|
|
doc.removeObject(n)
|
|
|
|
gui_utils.select(add_list)
|
|
return add_list, delete_list
|
|
|
|
## @}
|