# *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * # * Copyright (c) 2020 Eliud Cabrera Castillo * # * * # * 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 ## @}