diff --git a/src/Mod/BIM/ArchCommands.py b/src/Mod/BIM/ArchCommands.py index 82714de28a..5878504fd8 100644 --- a/src/Mod/BIM/ArchCommands.py +++ b/src/Mod/BIM/ArchCommands.py @@ -558,7 +558,10 @@ def getShapeFromMesh(mesh,fast=True,tolerance=0.001,flat=False,cut=True): print("getShapeFromMesh: error creating solid") return se else: - return solid + if solid.isClosed(): + return solid + else: + return se def projectToVector(shape,vector): '''projectToVector(shape,vector): projects the given shape on the given diff --git a/src/Mod/Draft/draftfunctions/downgrade.py b/src/Mod/Draft/draftfunctions/downgrade.py index e0f41dd237..3aa01e6dc5 100644 --- a/src/Mod/Draft/draftfunctions/downgrade.py +++ b/src/Mod/Draft/draftfunctions/downgrade.py @@ -2,6 +2,7 @@ # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * # * Copyright (c) 2020 Eliud Cabrera Castillo * +# * Copyright (c) 2025 The FreeCAD Project Association * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -31,11 +32,11 @@ See also the `upgrade` function. ## \addtogroup draftfunctions # @{ import FreeCAD as App -from draftfunctions import cut from draftmake import make_copy from draftutils import utils from draftutils import params from draftutils import gui_utils +from draftutils.groups import is_group from draftutils.messages import _msg from draftutils.translate import translate @@ -68,23 +69,13 @@ def downgrade(objects, delete=False, force=None): 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 -------- upgrade """ - _name = "downgrade" - if not isinstance(objects, list): - objects = [objects] + # definitions of actions to perform - delete_list = [] - add_list = [] - doc = App.ActiveDocument - - # actions definitions def explode(obj): """Explode a Draft block or array.""" obj_pl = obj.Placement @@ -92,6 +83,7 @@ def downgrade(objects, delete=False, force=None): if getattr(obj, "Components", []): delete_list.append(obj) for comp in obj.Components: + # Objects in Components are in the same parent group as the block. comp.Placement = obj_pl.multiply(comp.Placement) comp.Visibility = True return True @@ -100,40 +92,57 @@ def downgrade(objects, delete=False, force=None): return False if not hasattr(obj, "PlacementList"): return False - base = obj.Base + # Array must be added to delete_list before base. See can_be_deleted. delete_list.append(obj) + base = obj.Base + if not base.Visibility: + # Delete base if it is not visible. The can_be_deleted + # function will check if it is not used elsewhere. + delete_list.append(base) + new_list = [] if getattr(obj, "ExpandArray", False): + delete_list.extend(obj.ElementList) for lnk in obj.ElementList: - new = make_copy.make_copy(base) - new.Placement = obj_pl.multiply(lnk.Placement) - new.Visibility = True - delete_list.append(lnk) + newobj = make_copy.make_copy(base) + newobj.Placement = obj_pl.multiply(lnk.Placement) + newobj.Visibility = True + new_list.append(newobj) else: for arr_pl in obj.PlacementList: - new = make_copy.make_copy(base) - new.Placement = obj_pl.multiply(arr_pl) - new.Visibility = True + newobj = make_copy.make_copy(base) + newobj.Placement = obj_pl.multiply(arr_pl) + newobj.Visibility = True + new_list.append(newobj) + add_to_parent(obj, new_list) + add_list.extend(new_list) return True def cut2(objects): - """Cut first object from the last one.""" - newobj = cut.cut(objects[0], objects[1]) - if newobj: - add_list.append(newobj) - return newobj - return None + """Cut 2nd object from 1st.""" + newobj = doc.addObject("Part::Cut", "Cut") + newobj.Base = objects[0] + newobj.Tool = objects[1] + format(objects[0], [newobj]) + add_to_parent(objects[0], [newobj]) + add_list.append(newobj) + return True def splitCompounds(objects): """Split solids contained in compound objects into new objects.""" result = False - for o in objects: - if o.Shape.Solids: - for s in o.Shape.Solids: - newobj = doc.addObject("Part::Feature", "Solid") - newobj.Shape = s - add_list.append(newobj) - result = True - delete_list.append(o) + for obj in objects: + if not obj.Shape.Solids: + continue + new_list = [] + for solid in obj.Shape.Solids: + newobj = doc.addObject("Part::Feature", "Solid") + newobj.Shape = solid + new_list.append(newobj) + format(obj, new_list) + add_to_parent(obj, new_list) + add_list.extend(new_list) + delete_list.append(obj) + result = True return result def splitFaces(objects): @@ -141,129 +150,231 @@ def downgrade(objects, delete=False, force=None): result = False preserveFaceColor = params.get_param("preserveFaceColor") preserveFaceNames = params.get_param("preserveFaceNames") - for o in objects: - if App.GuiUp and preserveFaceColor and o.ViewObject: - voDColors = o.ViewObject.DiffuseColor + for obj in objects: + if not obj.Shape.Faces: + continue + new_list = [] + if App.GuiUp and preserveFaceColor and obj.ViewObject: + colors = obj.ViewObject.DiffuseColor else: - voDColors = None - oLabel = o.Label if hasattr(o, 'Label') else "" - if o.Shape.Faces: - for ind, f in enumerate(o.Shape.Faces): - newobj = doc.addObject("Part::Feature", "Face") - newobj.Shape = f - if preserveFaceNames: - newobj.Label = "{} {}".format(oLabel, newobj.Label) - if App.GuiUp and preserveFaceColor and voDColors: - # At this point, some single-color objects might have - # just a single value in voDColors for all faces, - # so we handle that - if ind < len(voDColors): - tcolor = voDColors[ind] - else: - tcolor = voDColors[0] - # does is not applied visually on its own - # just in case - newobj.ViewObject.DiffuseColor[0] = tcolor - # this gets applied, works by itself too - newobj.ViewObject.ShapeColor = tcolor - add_list.append(newobj) - result = True - delete_list.append(o) + colors = None + label = getattr(obj, "Label", "") + for ind, face in enumerate(obj.Shape.Faces): + newobj = doc.addObject("Part::Feature", "Face") + newobj.Shape = face + format(obj, [newobj]) + if App.GuiUp and preserveFaceColor and colors: + # At this point, some single-color objects might have just + # a single value in colors for all faces, so we handle that: + if ind < len(colors): + color = colors[ind] + else: + color = colors[0] + newobj.ViewObject.ShapeColor = color + if preserveFaceNames: + newobj.Label = "{} {}".format(label, newobj.Label) + new_list.append(newobj) + add_to_parent(obj, new_list) + add_list.extend(new_list) + delete_list.append(obj) + result = True return result def subtr(objects): - """Subtract objects from the first one.""" + """Subtract faces from the first one.""" faces = [] - for o in objects: - if o.Shape.Faces: - faces.extend(o.Shape.Faces) - delete_list.append(o) - u = faces.pop(0) - for f in faces: - u = u.cut(f) - if not u.isNull(): - newobj = doc.addObject("Part::Feature", "Subtraction") - newobj.Shape = u - add_list.append(newobj) - return newobj - return None + done_list = [] + for obj in objects: + if obj.Shape.Faces: + faces.extend(obj.Shape.Faces) + done_list.append(obj) + if not faces: + return False + main_face = faces.pop(0) + for face in faces: + main_face = main_face.cut(face) + if main_face.isNull(): + return False + newobj = doc.addObject("Part::Feature", "Subtraction") + newobj.Shape = main_face + format(done_list[0], [newobj]) + add_to_parent(done_list[0], [newobj]) + add_list.append(newobj) + delete_list.extend(done_list) + return True def getWire(obj): """Get the wire from a face object.""" - result = False - for w in obj.Shape.Faces[0].Wires: + if not obj.Shape.Faces: + return False + new_list = [] + for wire in obj.Shape.Faces[0].Wires: newobj = doc.addObject("Part::Feature", "Wire") - newobj.Shape = w - add_list.append(newobj) - result = True + newobj.Shape = wire + new_list.append(newobj) + format(obj, new_list) + add_to_parent(obj, new_list) + add_list.extend(new_list) delete_list.append(obj) - return result + return True def splitWires(objects): """Split the wires contained in objects into edges.""" result = False - for o in objects: - if o.Shape.Edges: - for e in o.Shape.Edges: - newobj = doc.addObject("Part::Feature", "Edge") - newobj.Shape = e - add_list.append(newobj) - delete_list.append(o) - result = True + for obj in objects: + if not obj.Shape.Edges: + continue + new_list = [] + for edge in obj.Shape.Edges: + newobj = doc.addObject("Part::Feature", "Edge") + newobj.Shape = edge + new_list.append(newobj) + format(obj, new_list) + add_to_parent(obj, new_list) + add_list.extend(new_list) + delete_list.append(obj) + result = True return result + def _shapify(obj): + """Wrapper for utils.shapify.""" + newobj = utils.shapify(obj, delete=False) + if newobj: + format(obj, [newobj]) + add_to_parent(obj, [newobj]) + add_list.append(newobj) + delete_list.append(obj) + return True + return False + + + # helper functions (same as in upgrade.py) + + def get_parent(obj): + # Problem with obj.getParent(): + # https://github.com/FreeCAD/FreeCAD/issues/19600 + parent = obj.getParentGroup() + if parent is not None: + return parent + return obj.getParentGeoFeatureGroup() + + def can_be_deleted(obj): + if not obj.InList: + return True + for other in obj.InList: + if is_group(other): + continue + if other.TypeId == "App::Part": + continue + return False + return True + def delete_object(obj): - if obj.FullName == "?": # Already deleted. + if utils.is_deleted(obj): + return + parent = get_parent(obj) + if parent is not None and parent.TypeId == "PartDesign::Body": + obj = parent + if not can_be_deleted(obj): + # Make obj invisible instead: + obj.Visibility = False return - # special case: obj is a body or belongs to a body: if obj.TypeId == "PartDesign::Body": obj.removeObjectsFromDocument() - if hasattr(obj, "_Body") and obj._Body is not None: - obj = obj._Body - obj.removeObjectsFromDocument() - else: - for parent in obj.InList: - if parent.TypeId == "PartDesign::Body" \ - and obj in parent.Group: - obj = parent - obj.removeObjectsFromDocument() - break doc.removeObject(obj.Name) + def add_to_parent(obj, new_list): + parent = get_parent(obj) + if parent is None: + if doc.getObject("Draft_Construction"): + # This cludge is required because the make_* commands may + # put new objects in the construction group. + constr_group = doc.getObject("Draft_Construction") + for newobj in new_list: + constr_group.removeObject(newobj) + return + if parent.TypeId == "PartDesign::Body": + # We don't add to a PD Body. We process its placement and + # add to its parent instead. + for newobj in new_list: + newobj.Placement = parent.Placement.multiply(newobj.Placement) + add_to_parent(parent, new_list) + return + for newobj in new_list: + # Using addObject is different from just changing the Group property. + # With addObject the object will be added to the parent group, but if + # that is a normal group, also to that group's parent GeoFeatureGroup, + # if available. + parent.addObject(newobj) + + def format(obj, new_list): + for newobj in new_list: + gui_utils.format_object(newobj, obj, ignore_construction=True) + + + doc = App.ActiveDocument + add_list = [] + delete_list = [] + result = False + + if not isinstance(objects, list): + objects = [objects] + if not objects: + return add_list, delete_list + # analyzing objects + solids = [] faces = [] edges = [] onlyedges = True parts = [] - solids = [] - result = None - for o in objects: - if hasattr(o, 'Shape'): - for s in o.Shape.Solids: - solids.append(s) - for f in o.Shape.Faces: - faces.append(f) - for e in o.Shape.Edges: - edges.append(e) - if o.Shape.ShapeType != "Edge": + for obj in objects: + if hasattr(obj, "Shape"): + for solid in obj.Shape.Solids: + solids.append(False) + for face in obj.Shape.Faces: + faces.append(face) + for edge in obj.Shape.Edges: + edges.append(edge) + if obj.Shape.ShapeType != "Edge": onlyedges = False - parts.append(o) + parts.append(obj) objects = parts - if force: - if force in ("explode", "shapify", "subtr", "splitFaces", - "cut2", "getWire", "splitWires"): - # TODO: Using eval to evaluate a string is not ideal - # and potentially a security risk. - # How do we execute the function without calling eval? - # Best case, a series of if-then statements. - shapify = utils.shapify - result = eval(force)(objects) + if not objects: + result = False + + elif force: + # functions that work on a single object: + single_funcs = {"explode": explode, + "getWire": getWire, + "shapify": _shapify} + # functions that work on multiple objects: + multi_funcs = {"cut2": cut2, + "splitCompounds": splitCompounds, + "splitFaces": splitFaces, + "splitWires": splitWires, + "subtr": subtr} + if force in single_funcs: + result = any([single_funcs[force](obj) for obj in objects]) + elif force in multi_funcs: + result = multi_funcs[force](objects) else: - _msg(translate("draft", "Upgrade: Unknown force method:") + " " + force) - result = None + _msg(translate("draft", "Downgrade: Unknown force method:") + " " + force) + result = False + else: + parent = get_parent(objects[0]) + same_parent = True + same_parent_type = getattr(parent, "TypeId", "") # "" for global space. + if len(objects) > 1: + for obj in objects[1:]: + if get_parent(obj) != parent: + same_parent = False + same_parent_type = None + break + # we have a block, we explode it if len(objects) == 1 and utils.get_type(objects[0]) == "Block": result = explode(objects[0]) @@ -271,35 +382,29 @@ def downgrade(objects, delete=False, force=None): _msg(translate("draft", "Found 1 block: exploding it")) # we have an array, we explode it - elif len(objects) == 1 \ - and "Array" in utils.get_type(objects[0]) \ - and hasattr(objects[0], "PlacementList"): + elif len(objects) == 1 and "Array" in utils.get_type(objects[0]): result = explode(objects[0]) if result: _msg(translate("draft", "Found 1 array: exploding it")) + # special case, we have one parametric object: we "de-parametrize" it + elif len(objects) == 1 \ + and hasattr(objects[0], "Shape") \ + and (hasattr(objects[0], "Base") or hasattr(objects[0], "Profile")): + result = _shapify(objects[0]) + if result: + _msg(translate("draft", "Found 1 parametric object: breaking its dependencies")) + # we have one multi-solids compound object: extract its solids elif len(objects) == 1 \ and hasattr(objects[0], "Shape") \ and len(solids) > 1: result = splitCompounds(objects) - # print(result) if result: _msg(translate("draft", "Found 1 multi-solids compound: exploding it")) - # special case, we have one parametric object: we "de-parametrize" it - elif len(objects) == 1 \ - and hasattr(objects[0], "Shape") \ - and hasattr(objects[0], "Base") \ - and not objects[0].isDerivedFrom("PartDesign::Feature"): - result = utils.shapify(objects[0]) - if result: - _msg(translate("draft", "Found 1 parametric object: breaking its dependencies")) - add_list.append(result) - # delete_list.append(objects[0]) - # we have only 2 objects: cut 2nd from 1st - elif len(objects) == 2: + elif len(objects) == 2 and same_parent and same_parent_type != "PartDesign::Body": result = cut2(objects) if result: _msg(translate("draft", "Found 2 objects: subtracting them")) @@ -311,10 +416,10 @@ def downgrade(objects, delete=False, force=None): if result: _msg(translate("draft", "Found several faces: splitting them")) # several objects: remove all the faces from the first one - else: + elif same_parent and same_parent_type != "PartDesign::Body": result = subtr(objects) if result: - _msg(translate("draft", "Found several objects: subtracting them from the first one")) + _msg(translate("draft", "Found several faces: subtracting them from the first one")) # only one face: we extract its wires elif len(faces) > 0: @@ -328,14 +433,13 @@ def downgrade(objects, delete=False, force=None): if result: _msg(translate("draft", "Found only wires: extracting their edges")) - # no result has been obtained - if not result: - _msg(translate("draft", "No more downgrade possible")) + # no result has been obtained + if not result: + _msg(translate("draft", "Unable to downgrade these objects")) if delete: - for o in delete_list: - delete_object(o) - delete_list = [] + for obj in delete_list: + delete_object(obj) gui_utils.select(add_list) return add_list, delete_list diff --git a/src/Mod/Draft/draftfunctions/upgrade.py b/src/Mod/Draft/draftfunctions/upgrade.py index e06ad4ed54..3c9ce544a8 100644 --- a/src/Mod/Draft/draftfunctions/upgrade.py +++ b/src/Mod/Draft/draftfunctions/upgrade.py @@ -2,6 +2,7 @@ # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * # * Copyright (c) 2020 Eliud Cabrera Castillo * +# * Copyright (c) 2025 The FreeCAD Project Association * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -28,20 +29,20 @@ See also the `downgrade` function. # \ingroup draftfunctions # \brief Provides functions to upgrade objects by different methods. +import math 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.groups import is_group +from draftutils.messages import _msg from draftutils.translate import translate # Delay import of module until first use because it is heavy @@ -85,470 +86,550 @@ def upgrade(objects, delete=False, force=None): 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): + def makeCompound(objects): """Return a compound object made from the given objects.""" - newobj = make_block.make_block(objectslist) + newobj = make_block.make_block(objects) + format(objects[0], [newobj]) + add_to_parent(objects[0], [newobj]) add_list.append(newobj) - return newobj + return True - def closeGroupWires(groupslist): + def closeGroupWires(groups): """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) + for grp in groups: + if any([closeWire(obj) for obj in grp.Group]): + result = True return result def makeSolid(obj): """Turn an object into a solid, if possible.""" if obj.Shape.Solids: - return None - sol = None + return False try: - sol = Part.makeSolid(obj.Shape) + solid = 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 + return False + if not solid.isClosed(): + return False + newobj = doc.addObject("Part::Feature", "Solid") + newobj.Shape = solid + format(obj, [newobj]) + add_to_parent(obj, [newobj]) + add_list.append(newobj) + delete_list.append(obj) + return True def closeWire(obj): """Close a wire object, if possible.""" if obj.Shape.Faces: - return None + return False if len(obj.Shape.Wires) != 1: - return None + return False if len(obj.Shape.Edges) == 1: - return None + return False if is_straight_line(obj.Shape): - return None + return False 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 + wire = obj.Shape.Wires[0] + if wire.isClosed(): + return False + verts = wire.OrderedVertexes + p0 = verts[0].Point + p1 = verts[-1].Point + edges = wire.Edges + edges.append(Part.LineSegment(p1, p0).toShape()) + wire = Part.Wire(Part.__sortEdges__(edges)) + newobj = doc.addObject("Part::Feature", "Wire") + newobj.Shape = wire + format(obj, [newobj]) + add_to_parent(obj, [newobj]) + add_list.append(newobj) + delete_list.append(obj) + return True 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 + shp = Arch.getShapeFromMesh(mesh.Mesh) + if shp: + newobj = doc.addObject("Part::Feature", shp.ShapeType) + newobj.Shape = shp + format(mesh, [newobj]) + add_to_parent(mesh, [newobj]) add_list.append(newobj) delete_list.append(mesh) result = True return result - def makeFusion(obj1, obj2=None): + def makeFusion(objects): """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 = doc.addObject("Part::Fuse", "Fusion") + newobj.Base = objects[0] + newobj.Tool = objects[1] + format(objects[0], [newobj]) + add_to_parent(objects[0], [newobj]) + add_list.append(newobj) + return True - newobj = fuse.fuse(obj1, obj2) - if newobj: - add_list.append(newobj) - return newobj - return None - - def makeShell(objectslist): + def makeShell(objects): """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")) + done_list = [] + for obj in objects: + if obj.Shape.Faces: + faces.extend(obj.Shape.Faces) + done_list.append(obj) + if not faces: return None + shp = Part.makeShell(faces) + if shp.isNull(): + return None + newobj = doc.addObject("Part::Feature", shp.ShapeType) + newobj.Shape = shp + # Format before applying diffuse color: + format(done_list[0], [newobj]) + add_to_parent(done_list[0], [newobj]) + add_list.append(newobj) + delete_list.extend(done_list) - # fuse faces + if App.GuiUp and params.get_param("preserveFaceColor"): + # Must happen after add_to_parent for correct CenterOfMass. + colors = gui_utils.get_diffuse_color(done_list) + if len(faces) != len(colors): + newobj.ViewObject.DiffuseColor = [colors[0]] + else: + # The ordering of shp.Faces may be different. Since we cannot + # compare via hashCode(), we have to iterate and use different + # criteria to find the correct color. + old_data = [] + for face, color in zip(faces, colors): + old_data.append([face.Area, face.CenterOfMass, color]) + new_colors = [] + for new_face in shp.Faces: + new_area = new_face.Area + new_cen = new_face.CenterOfMass + for old_area, old_cen, old_color in old_data: + if math.isclose(new_area, old_area, abs_tol=1e-7) \ + and new_cen.isEqual(old_cen, 1e-7): + new_colors.append(old_color) + break + newobj.ViewObject.DiffuseColor = new_colors + + if params.get_param("preserveFaceNames"): + firstName = done_list[0].Label + nameNoTrailNumbers = re.sub(r"\d+$", "", firstName) + newobj.Label = "{} {}".format(newobj.Label, nameNoTrailNumbers) + + return newobj + + def joinFaces(objects): + """Make one big face from the given objects, if possible.""" + faces = [] + done_list = [] + for obj in objects: + if obj.Shape.Faces: + faces.extend(obj.Shape.Faces) + done_list.append(obj) + if not faces: + return False + if not DraftGeomUtils.is_coplanar(faces, 1e-3): + return False 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 + # check if concatenate failed 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 + return False + # several coplanar and non-curved faces, they can become a Draft Wire + if len(face.Wires) == 1 and not DraftGeomUtils.hasCurves(face): + 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 + format(done_list[0], [newobj]) + add_to_parent(done_list[0], [newobj]) + add_list.append(newobj) + delete_list.extend(done_list) + return True 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 + if not face: + return False + newobj = doc.addObject("Part::Feature", "Face") + newobj.Shape = face + format(obj, [newobj]) + add_to_parent(obj, [newobj]) + add_list.append(newobj) + delete_list.append(obj) + return True - 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.""" + def makeFaces(objects): + """Make a face from every closed wire in the given objects.""" result = False - for o in objectslist: - for w in o.Shape.Wires: + for obj in objects: + new_list = [] + for wire in obj.Shape.Wires: try: - f = Part.Face(w) + face = Part.Face(wire) 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) + continue + newobj = doc.addObject("Part::Feature", "Face") + newobj.Shape = face + new_list.append(newobj) + if not new_list: + continue + format(obj, new_list) + add_to_parent(obj, new_list) + add_list.extend(new_list) + delete_list.append(obj) + result = True return result - def makeWires(objectslist): - """Join edges in the given objects list into wires.""" + def makeWires(objects): + """Join edges in the given objects into wires.""" edges = [] - for object in objectslist: - for edge in object.Shape.Edges: - edges.append(edge) - + done_list = [] + for obj in objects: + if obj.Shape.Edges: + edges.extend(obj.Shape.Edges) + done_list.append(obj) + if not edges: + return False 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] + for cluster in sorted_edges: + for edge in cluster: + print("Curve: {}".format(edge.Curve)) + print("first: {}, last: {}".format(edge.Vertexes[0].Point, + edge.Vertexes[-1].Point)) + wires = [Part.Wire(cluster) for cluster 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 + return False + if len(objects) > 1 and len(wires) == len(objects): + # we still have the same number of objects, we actually didn't join anything! + return False + new_list = [] + for wire in wires: + newobj = doc.addObject("Part::Feature", "Wire") + newobj.Shape = wire + new_list.append(newobj) + # We don't know which wire came from which obj, we format them the same: + format(done_list[0], new_list) + add_to_parent(done_list[0], new_list) + add_list.extend(new_list) + delete_list.extend(done_list) + return True - # analyzing what we have in our selection - edges = [] + def _draftify(obj): + """Wrapper for draftify.""" + new_list = draftify.draftify(obj, delete=False) + if not new_list: + return False + if not isinstance(new_list, list): + new_list = [new_list] + format(obj, new_list) + add_to_parent(obj, new_list) + add_list.extend(new_list) + delete_list.append(obj) + return True + + + # helper functions (same as in downgrade.py) + + def get_parent(obj): + # Problem with obj.getParent(): + # https://github.com/FreeCAD/FreeCAD/issues/19600 + parent = obj.getParentGroup() + if parent is not None: + return parent + return obj.getParentGeoFeatureGroup() + + def can_be_deleted(obj): + if not obj.InList: + return True + for other in obj.InList: + if is_group(other): + continue + if other.TypeId == "App::Part": + continue + return False + return True + + def delete_object(obj): + if utils.is_deleted(obj): + return + parent = get_parent(obj) + if parent is not None and parent.TypeId == "PartDesign::Body": + obj = parent + if not can_be_deleted(obj): + # Make obj invisible instead: + obj.Visibility = False + return + if obj.TypeId == "PartDesign::Body": + obj.removeObjectsFromDocument() + doc.removeObject(obj.Name) + + def add_to_parent(obj, new_list): + parent = get_parent(obj) + if parent is None: + if doc.getObject("Draft_Construction"): + # This cludge is required because the make_* commands may + # put new objects in the construction group. + constr_group = doc.getObject("Draft_Construction") + for newobj in new_list: + constr_group.removeObject(newobj) + return + if parent.TypeId == "PartDesign::Body": + # We don't add to a PD Body. We process its placement and + # add to its parent instead. + for newobj in new_list: + newobj.Placement = parent.Placement.multiply(newobj.Placement) + add_to_parent(parent, new_list) + return + for newobj in new_list: + # Using addObject is different from just changing the Group property. + # With addObject the object will be added to the parent group, but if + # that is a normal group, also to that group's parent GeoFeatureGroup, + # if available. + parent.addObject(newobj) + + def format(obj, new_list): + for newobj in new_list: + gui_utils.format_object(newobj, obj, ignore_construction=True) + + + doc = App.ActiveDocument + add_list = [] + delete_list = [] + result = False + + if not isinstance(objects, list): + objects = [objects] + if not objects: + return add_list, delete_list + + # analyzing objects + faces = [] wires = [] openwires = [] - faces = [] - groups = [] - parts = [] - curves = [] facewires = [] + edges = [] loneedges = [] + groups = [] meshes = [] + parts = [] - 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) + for obj in objects: + if obj.TypeId == "App::DocumentObjectGroup": + groups.append(obj) + elif hasattr(obj, "Shape"): + parts.append(obj) + faces.extend(obj.Shape.Faces) + wires.extend(obj.Shape.Wires) + edges.extend(obj.Shape.Edges) + for face in obj.Shape.Faces: + facewires.extend(face.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) + for wire in obj.Shape.Wires: + if len(wire.Edges) > 1: + for edge in wire.Edges: + wirededges.append(edge.hashCode()) + if not wire.isClosed(): + openwires.append(wire) + for edge in obj.Shape.Edges: + if not edge.hashCode() in wirededges and not edge.isClosed(): + loneedges.append(edge) + elif obj.isDerivedFrom("Mesh::Feature"): + meshes.append(obj) 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("groups: {}".format(groups)) 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) + if not (groups or objects or meshes): + result = False + + elif force: + if force == "closeGroupWires": + result = closeGroupWires(groups) + elif force == "turnToParts": + result = turnToParts(meshes) else: - _msg(translate("draft","Upgrade: Unknown force method:") + " " + force) - result = None + # functions that work on a single object: + single_funcs = {"closeWire": closeWire, + "draftify": _draftify, + "makeSketchFace": makeSketchFace, + "makeSolid": makeSolid} + # functions that work on multiple objects: + multi_funcs = {"joinFaces": joinFaces, + "makeCompound": makeCompound, + "makeFaces": makeFaces, + "makeFusion": makeFusion, + "makeShell": makeShell, + "makeWires": makeWires} + if force in single_funcs: + result = any([single_funcs[force](obj) for obj in objects]) + elif force in multi_funcs: + result = multi_funcs[force](objects) + else: + _msg(translate("draft", "Upgrade: Unknown force method:") + " " + force) + result = False + + # if we have a group: close each wire inside + elif groups: + result = closeGroupWires(groups) + if result: + _msg(translate("draft", "Found groups: closing open wires 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 them into Part shapes")) 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 + # joinFaces is called if the precision is insufficient. if faces: faces_coplanarity = DraftGeomUtils.is_coplanar(faces, 1e-3) - # applying transformations automatically - result = None + parent = get_parent(objects[0]) + same_parent = True + same_parent_type = getattr(parent, "TypeId", "") # "" for global space. + if len(objects) > 1: + for obj in objects[1:]: + if get_parent(obj) != parent: + same_parent = False + same_parent_type = None + break - # 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")) + # we have only faces + if faces and len(facewires) == len(wires) and not openwires and not loneedges: - # 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 + # this also handles PD Bodies and PD features with solids (result will be False) 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")) + _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]) + elif len(objects) == 2 \ + and not faces_coplanarity \ + and same_parent \ + and same_parent_type != "PartDesign::Body": + result = makeFusion(objects) if result: - _msg(translate("draft","Found 2 objects: fusing them")) + _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: + elif len(objects) > 1 \ + and len(faces) > 1 \ + and same_parent \ + and same_parent_type != "PartDesign::Body": result = makeShell(objects) if result: - _msg(translate("draft","Found several objects: creating a " - + str(result.Shape.ShapeType))) + _msg(translate( + "draft", + "Found several objects: creating a " + 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) + elif len(objects) == 1 and len(faces) > 1 and faces_coplanarity: + result = joinFaces(objects) if result: - _msg(translate("draft","Found object with several coplanar faces: refine them")) + _msg(translate("draft", "Found object with several coplanar faces: refining 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) + elif len(objects) == 1 and not objects[0].isDerivedFrom("Part::Part2DObjectPython"): + result = _draftify(objects[0]) if result: - add_list.append(result) - delete_list.append(objects[0]) - _msg(translate("draft","Found 1 non-parametric objects: draftifying it")) + _msg(translate("draft", "Found 1 non-parametric object: 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")): + 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")) + _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")) + _msg(translate("draft", "Found closed wires: creating faces")) + # wires or edges: we try to join them - elif len(objects) > 1 and len(edges) > 1: + elif len(objects) > 1 and len(edges) > 1 and same_parent: result = makeWires(objects) if result: - _msg(translate("draft","Found several wires or edges: wiring them")) + _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! + result = makeCompound(objects) + if result: + _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) + _msg(translate("draft", "Found 1 open wire: closing it")) + + # only one object: if not parametric, we "draftify" it + elif len(objects) == 1 \ + and len(edges) == 1 \ + and not objects[0].isDerivedFrom("Part::Part2DObjectPython"): + edge_type = DraftGeomUtils.geomType(objects[0].Shape.Edges[0]) # currently only support Line and Circle if edge_type in ("Line", "Circle"): - result = draftify.draftify(objects[0], delete=False) + result = _draftify(objects[0]) if result: - add_list.append(result) - delete_list.append(objects[0]) - _msg(translate("draft","Found 1 object: draftifying it")) + _msg(translate("draft", "Found 1 non-parametric 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")) + _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.")) + _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) + for obj in delete_list: + delete_object(obj) gui_utils.select(add_list) return add_list, delete_list diff --git a/src/Mod/Draft/draftutils/gui_utils.py b/src/Mod/Draft/draftutils/gui_utils.py index bf91bf30fe..17862bba04 100644 --- a/src/Mod/Draft/draftutils/gui_utils.py +++ b/src/Mod/Draft/draftutils/gui_utils.py @@ -499,7 +499,7 @@ def restore_view_object(obj, vp_module, vp_class, format=True, format_ref=None): format_object(obj, format_ref) -def format_object(target, origin=None): +def format_object(target, origin=None, ignore_construction=False): """Apply visual properties to an object. This function only works if the graphical interface is available. @@ -521,6 +521,10 @@ def format_object(target, origin=None): If construction mode is not active, its visual properties are assigned to `target`, with the exception of `BoundingBox`, `Proxy`, `RootNode` and `Visibility`. + + ignore_construction: bool, optional + Defaults to `False`. + Set to `True` to ignore construction mode. """ if not target: return @@ -528,30 +532,38 @@ def format_object(target, origin=None): return if not hasattr(Gui, "draftToolBar"): return - if not hasattr(target, 'ViewObject'): + if not hasattr(target, "ViewObject"): return + if hasattr(target, "Shape") and target.Shape.Faces: + len_faces = len(target.Shape.Faces) + else: + len_faces = 1 obrep = target.ViewObject obprops = obrep.PropertiesList - if origin and hasattr(origin, 'ViewObject'): + if origin and hasattr(origin, "ViewObject"): matchrep = origin.ViewObject for p in matchrep.PropertiesList: - if p not in ("DisplayMode", "BoundingBox", - "Proxy", "RootNode", "Visibility"): - if p in obprops: - if not obrep.getEditorMode(p): - if hasattr(getattr(matchrep, p), "Value"): - val = getattr(matchrep, p).Value - else: - val = getattr(matchrep, p) - try: - setattr(obrep, p, val) - except Exception: - pass + if p in ("DisplayMode", "BoundingBox", "Proxy", "RootNode", "Visibility"): + continue + if p not in obprops: + continue + if obrep.getEditorMode(p): + continue + val = getattr(matchrep, p) + if isinstance(val, tuple): + if len(val) != len_faces: + val = (val[0], ) + elif hasattr(val, "Value"): + val = val.Value + try: + setattr(obrep, p, val) + except Exception: + pass if matchrep.DisplayMode in obrep.listDisplayModes(): obrep.DisplayMode = matchrep.DisplayMode if hasattr(obrep, "DiffuseColor"): difcol = get_diffuse_color(origin) - if difcol: + if difcol and len(difcol) == len_faces: obrep.DiffuseColor = difcol elif "FontName" not in obprops: # Apply 2 Draft style preferences, other style preferences are applied by Core. @@ -561,6 +573,8 @@ def format_object(target, origin=None): dm = utils.DISPLAY_MODES[params.get_param("DefaultDisplayMode")] if dm in obrep.listDisplayModes(): obrep.DisplayMode = dm + if ignore_construction: + return if Gui.draftToolBar.isConstructionMode(): doc = App.ActiveDocument col = params.get_param("constructioncolor") | 0x000000FF diff --git a/src/Mod/Draft/draftutils/utils.py b/src/Mod/Draft/draftutils/utils.py index d7ae12d157..6a83c38790 100644 --- a/src/Mod/Draft/draftutils/utils.py +++ b/src/Mod/Draft/draftutils/utils.py @@ -606,7 +606,7 @@ def get_clone_base(obj, strict=False, recursive=True): getCloneBase = get_clone_base -def shapify(obj): +def shapify(obj, delete=True): """Transform a parametric object into a static, non-parametric shape. Parameters @@ -618,6 +618,10 @@ def shapify(obj): with the same topological shape (`Part::TopoShape`) will be created. + delete: bool, optional + It defaults to `False`. + If it is `True`, the original object is deleted. + Returns ------- Part::Feature @@ -655,7 +659,8 @@ def shapify(obj): else: name = getRealName(obj.Name) - App.ActiveDocument.removeObject(obj.Name) + if delete: + App.ActiveDocument.removeObject(obj.Name) newobj = App.ActiveDocument.addObject("Part::Feature", name) newobj.Shape = shape