# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * # * * # * 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 * # * * # *************************************************************************** """Provide the Draft Workbench public programming interface. The Draft module offers tools to create and manipulate 2D objects. The functions in this file must be usable without requiring the graphical user interface. These functions can be used as the backend for the graphical commands defined in `DraftTools.py`. """ ## \addtogroup DRAFT # \brief Create and manipulate basic 2D objects # # This module offers tools to create and manipulate basic 2D objects # # The module allows to create 2D geometric objects such as line, rectangle, # circle, etc., modify these objects by moving, scaling or rotating them, # and offers a couple of other utilities to manipulate further these objects, # such as decompose them (downgrade) into smaller elements. # # The functionality of the module is divided into GUI tools, usable from the # visual interface, and corresponding python functions, that can perform # the same operation programmatically. # # @{ import math import sys from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD from FreeCAD import Vector import DraftVecUtils import WorkingPlane from draftutils.translate import translate if FreeCAD.GuiUp: import FreeCADGui import Draft_rc gui = True # To prevent complaints from code checkers (flake8) True if Draft_rc.__name__ else False else: gui = False __title__ = "FreeCAD Draft Workbench" __author__ = ("Yorik van Havre, Werner Mayer, Martin Burbaum, Ken Cline, " "Dmitry Chigrin, Daniel Falck") __url__ = "https://www.freecadweb.org" # --------------------------------------------------------------------------- # Backwards compatibility # --------------------------------------------------------------------------- from DraftLayer import Layer as _VisGroup from DraftLayer import ViewProviderLayer as _ViewProviderVisGroup from DraftLayer import makeLayer # import DraftFillet # Fillet = DraftFillet.Fillet # makeFillet = DraftFillet.makeFillet # --------------------------------------------------------------------------- # General functions # --------------------------------------------------------------------------- from draftutils.utils import ARROW_TYPES as arrowtypes from draftutils.utils import stringencodecoin from draftutils.utils import string_encode_coin from draftutils.utils import typecheck from draftutils.utils import type_check from draftutils.utils import getParamType from draftutils.utils import get_param_type from draftutils.utils import getParam from draftutils.utils import get_param from draftutils.utils import setParam from draftutils.utils import set_param from draftutils.utils import precision from draftutils.utils import tolerance from draftutils.utils import epsilon from draftutils.utils import getRealName from draftutils.utils import get_real_name from draftutils.utils import getType from draftutils.utils import get_type from draftutils.utils import getObjectsOfType from draftutils.utils import get_objects_of_type from draftutils.utils import isClone from draftutils.utils import is_clone from draftutils.utils import getGroupNames from draftutils.utils import get_group_names from draftutils.utils import ungroup from draftutils.utils import getGroupContents from draftutils.utils import get_group_contents from draftutils.utils import printShape from draftutils.utils import print_shape from draftutils.utils import compareObjects from draftutils.utils import compare_objects from draftutils.utils import shapify from draftutils.utils import loadSvgPatterns from draftutils.utils import load_svg_patterns from draftutils.utils import svgpatterns from draftutils.utils import svg_patterns from draftutils.utils import getMovableChildren from draftutils.utils import get_movable_children from draftutils.gui_utils import get3DView from draftutils.gui_utils import get_3d_view from draftutils.gui_utils import autogroup from draftutils.gui_utils import dimSymbol from draftutils.gui_utils import dim_symbol from draftutils.gui_utils import dimDash from draftutils.gui_utils import dim_dash from draftutils.gui_utils import removeHidden from draftutils.gui_utils import remove_hidden from draftutils.gui_utils import formatObject from draftutils.gui_utils import format_object from draftutils.gui_utils import getSelection from draftutils.gui_utils import get_selection from draftutils.gui_utils import getSelectionEx from draftutils.gui_utils import get_selection_ex from draftutils.gui_utils import select from draftutils.gui_utils import loadTexture from draftutils.gui_utils import load_texture #--------------------------------------------------------------------------- # Draft functions #--------------------------------------------------------------------------- #--------------------------------------------------------------------------- # Draft objects #--------------------------------------------------------------------------- # base object from draftobjects.base import DraftObject from draftobjects.base import _DraftObject # base viewprovider from draftviewproviders.view_base import ViewProviderDraft from draftviewproviders.view_base import _ViewProviderDraft from draftviewproviders.view_base import ViewProviderDraftAlt from draftviewproviders.view_base import _ViewProviderDraftAlt from draftviewproviders.view_base import ViewProviderDraftPart from draftviewproviders.view_base import _ViewProviderDraftPart # circle from draftmake.make_circle import make_circle, makeCircle from draftobjects.circle import Circle, _Circle # ellipse from draftmake.make_ellipse import make_ellipse, makeEllipse from draftobjects.ellipse import Ellipse, _Ellipse # rectangle from draftmake.make_rectangle import make_rectangle, makeRectangle from draftobjects.rectangle import Rectangle, _Rectangle if FreeCAD.GuiUp: from draftviewproviders.view_rectangle import ViewProviderRectangle from draftviewproviders.view_rectangle import _ViewProviderRectangle # polygon from draftmake.make_polygon import make_polygon, makePolygon from draftobjects.polygon import Polygon, _Polygon # wire and line from draftmake.make_line import make_line, makeLine from draftmake.make_wire import make_wire, makeWire from draftobjects.wire import Wire, _Wire if FreeCAD.GuiUp: from draftviewproviders.view_wire import ViewProviderWire from draftviewproviders.view_wire import _ViewProviderWire # bspline from draftmake.make_bspline import make_bspline, makeBSpline from draftobjects.bspline import BSpline, _BSpline if FreeCAD.GuiUp: # for compatibility with older versions _ViewProviderBSpline = ViewProviderWire # bezcurve from draftmake.make_bezcurve import make_bezcurve, makeBezCurve from draftobjects.bezcurve import BezCurve, _BezCurve if FreeCAD.GuiUp: # for compatibility with older versions _ViewProviderBezCurve = ViewProviderWire # clone from draftmake.make_clone import make_clone, clone from draftobjects.clone import Clone, _Clone if FreeCAD.GuiUp: from draftviewproviders.view_clone import ViewProviderClone from draftviewproviders.view_clone import _ViewProviderClone # point from draftmake.make_point import make_point, makePoint from draftobjects.point import Point, _Point if FreeCAD.GuiUp: from draftviewproviders.view_point import ViewProviderPoint from draftviewproviders.view_point import _ViewProviderPoint # facebinder from draftmake.make_facebinder import make_facebinder, makeFacebinder from draftobjects.facebinder import Facebinder, _Facebinder if FreeCAD.GuiUp: from draftviewproviders.view_facebinder import ViewProviderFacebinder from draftviewproviders.view_facebinder import _ViewProviderFacebinder # working plane proxy from draftmake.make_wpproxy import make_workingplaneproxy from draftmake.make_wpproxy import makeWorkingPlaneProxy from draftobjects.wpproxy import WorkingPlaneProxy if FreeCAD.GuiUp: from draftviewproviders.view_wpproxy import ViewProviderWorkingPlaneProxy #--------------------------------------------------------------------------- # Draft annotation objects #--------------------------------------------------------------------------- from draftobjects.dimension import make_dimension, make_angular_dimension from draftobjects.dimension import LinearDimension, AngularDimension makeDimension = make_dimension makeAngularDimension = make_angular_dimension _Dimension = LinearDimension _AngularDimension = AngularDimension if gui: from draftviewproviders.view_dimension import ViewProviderLinearDimension from draftviewproviders.view_dimension import ViewProviderAngularDimension _ViewProviderDimension = ViewProviderLinearDimension _ViewProviderAngularDimension = ViewProviderAngularDimension from draftobjects.label import make_label from draftobjects.label import Label makeLabel = make_label DraftLabel = Label if gui: from draftviewproviders.view_label import ViewProviderLabel ViewProviderDraftLabel = ViewProviderLabel from draftobjects.text import make_text from draftobjects.text import Text makeText = make_text DraftText = Text if gui: from draftviewproviders.view_text import ViewProviderText ViewProviderDraftText = ViewProviderText def convertDraftTexts(textslist=[]): """ converts the given Draft texts (or all that is found in the active document) to the new object This function was already present at splitting time during v 0.19 """ if not isinstance(textslist,list): textslist = [textslist] if not textslist: for o in FreeCAD.ActiveDocument.Objects: if o.TypeId == "App::Annotation": textslist.append(o) todelete = [] for o in textslist: l = o.Label o.Label = l+".old" obj = makeText(o.LabelText,point=o.Position) obj.Label = l todelete.append(o.Name) for p in o.InList: if p.isDerivedFrom("App::DocumentObjectGroup"): if o in p.Group: g = p.Group g.append(obj) p.Group = g for n in todelete: FreeCAD.ActiveDocument.removeObject(n) def makeWire(pointslist,closed=False,placement=None,face=None,support=None,bs2wire=False): """makeWire(pointslist,[closed],[placement]): Creates a Wire object from the given list of vectors. If closed is True or first and last points are identical, the wire is closed. If face is true (and wire is closed), the wire will appear filled. Instead of a pointslist, you can also pass a Part Wire.""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return import DraftGeomUtils, Part if not isinstance(pointslist,list): e = pointslist.Wires[0].Edges pointslist = Part.Wire(Part.__sortEdges__(e)) nlist = [] for v in pointslist.Vertexes: nlist.append(v.Point) if DraftGeomUtils.isReallyClosed(pointslist): closed = True pointslist = nlist if len(pointslist) == 0: print("Invalid input points: ",pointslist) #print(pointslist) #print(closed) if placement: typecheck([(placement,FreeCAD.Placement)], "makeWire") ipl = placement.inverse() if not bs2wire: pointslist = [ipl.multVec(p) for p in pointslist] if len(pointslist) == 2: fname = "Line" else: fname = "Wire" obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython",fname) _Wire(obj) obj.Points = pointslist obj.Closed = closed obj.Support = support if face != None: obj.MakeFace = face if placement: obj.Placement = placement if gui: _ViewProviderWire(obj.ViewObject) formatObject(obj) select(obj) return obj def makeCopy(obj,force=None,reparent=False): """makeCopy(object): returns an exact copy of an object""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return if (getType(obj) == "Rectangle") or (force == "Rectangle"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _Rectangle(newobj) if gui: _ViewProviderRectangle(newobj.ViewObject) elif (getType(obj) == "Point") or (force == "Point"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _Point(newobj) if gui: _ViewProviderPoint(newobj.ViewObject) elif (getType(obj) in ["Dimension","LinearDimension"]) or (force == "Dimension"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _Dimension(newobj) if gui: _ViewProviderDimension(newobj.ViewObject) elif (getType(obj) == "Wire") or (force == "Wire"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _Wire(newobj) if gui: _ViewProviderWire(newobj.ViewObject) elif (getType(obj) == "Circle") or (force == "Circle"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _Circle(newobj) if gui: _ViewProviderDraft(newobj.ViewObject) elif (getType(obj) == "Polygon") or (force == "Polygon"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _Polygon(newobj) if gui: _ViewProviderDraft(newobj.ViewObject) elif (getType(obj) == "BSpline") or (force == "BSpline"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _BSpline(newobj) if gui: _ViewProviderWire(newobj.ViewObject) elif (getType(obj) == "Block") or (force == "BSpline"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _Block(newobj) if gui: _ViewProviderDraftPart(newobj.ViewObject) elif (getType(obj) == "DrawingView") or (force == "DrawingView"): newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) _DrawingView(newobj) elif (getType(obj) == "Structure") or (force == "Structure"): import ArchStructure newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) ArchStructure._Structure(newobj) if gui: ArchStructure._ViewProviderStructure(newobj.ViewObject) elif (getType(obj) == "Wall") or (force == "Wall"): import ArchWall newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) ArchWall._Wall(newobj) if gui: ArchWall._ViewProviderWall(newobj.ViewObject) elif (getType(obj) == "Window") or (force == "Window"): import ArchWindow newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) ArchWindow._Window(newobj) if gui: ArchWindow._ViewProviderWindow(newobj.ViewObject) elif (getType(obj) == "Panel") or (force == "Panel"): import ArchPanel newobj = FreeCAD.ActiveDocument.addObject(obj.TypeId,getRealName(obj.Name)) ArchPanel._Panel(newobj) if gui: ArchPanel._ViewProviderPanel(newobj.ViewObject) elif (getType(obj) == "Sketch") or (force == "Sketch"): newobj = FreeCAD.ActiveDocument.addObject("Sketcher::SketchObject",getRealName(obj.Name)) for geo in obj.Geometry: newobj.addGeometry(geo) for con in obj.Constraints: newobj.addConstraint(con) elif hasattr(obj, 'Shape'): newobj = FreeCAD.ActiveDocument.addObject("Part::Feature",getRealName(obj.Name)) newobj.Shape = obj.Shape else: print("Error: Object type cannot be copied") return None for p in obj.PropertiesList: if not p in ["Proxy","ExpressionEngine"]: if p in newobj.PropertiesList: if not "ReadOnly" in newobj.getEditorMode(p): try: setattr(newobj,p,obj.getPropertyByName(p)) except AttributeError: try: setattr(newobj,p,obj.getPropertyByName(p).Value) except AttributeError: pass if reparent: parents = obj.InList if parents: for par in parents: if par.isDerivedFrom("App::DocumentObjectGroup"): par.addObject(newobj) else: for prop in par.PropertiesList: if getattr(par,prop) == obj: setattr(par,prop,newobj) formatObject(newobj,obj) return newobj def makeBlock(objectslist): """makeBlock(objectslist): Creates a Draft Block from the given objects""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython","Block") _Block(obj) obj.Components = objectslist if gui: _ViewProviderDraftPart(obj.ViewObject) for o in objectslist: o.ViewObject.Visibility = False select(obj) return obj def makeArray(baseobject,arg1,arg2,arg3,arg4=None,arg5=None,arg6=None,name="Array",use_link=False): """makeArray(object,xvector,yvector,xnum,ynum,[name]) for rectangular array, or makeArray(object,xvector,yvector,zvector,xnum,ynum,znum,[name]) for rectangular array, or makeArray(object,center,totalangle,totalnum,[name]) for polar array, or makeArray(object,rdistance,tdistance,axis,center,ncircles,symmetry,[name]) for circular array: Creates an array of the given object with, in case of rectangular array, xnum of iterations in the x direction at xvector distance between iterations, same for y direction with yvector and ynum, same for z direction with zvector and znum. In case of polar array, center is a vector, totalangle is the angle to cover (in degrees) and totalnum is the number of objects, including the original. In case of a circular array, rdistance is the distance of the circles, tdistance is the distance within circles, axis the rotation-axes, center the center of rotation, ncircles the number of circles and symmetry the number of symmetry-axis of the distribution. The result is a parametric Draft Array. """ if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return if use_link: obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name,_Array(None),None,True) else: obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name) _Array(obj) obj.Base = baseobject if arg6: if isinstance(arg1, (int, float, FreeCAD.Units.Quantity)): obj.ArrayType = "circular" obj.RadialDistance = arg1 obj.TangentialDistance = arg2 obj.Axis = arg3 obj.Center = arg4 obj.NumberCircles = arg5 obj.Symmetry = arg6 else: obj.ArrayType = "ortho" obj.IntervalX = arg1 obj.IntervalY = arg2 obj.IntervalZ = arg3 obj.NumberX = arg4 obj.NumberY = arg5 obj.NumberZ = arg6 elif arg4: obj.ArrayType = "ortho" obj.IntervalX = arg1 obj.IntervalY = arg2 obj.NumberX = arg3 obj.NumberY = arg4 else: obj.ArrayType = "polar" obj.Center = arg1 obj.Angle = arg2 obj.NumberPolar = arg3 if gui: if use_link: _ViewProviderDraftLink(obj.ViewObject) else: _ViewProviderDraftArray(obj.ViewObject) formatObject(obj,obj.Base) if len(obj.Base.ViewObject.DiffuseColor) > 1: obj.ViewObject.Proxy.resetColors(obj.ViewObject) baseobject.ViewObject.hide() select(obj) return obj def makePathArray(baseobject,pathobject,count,xlate=None,align=False,pathobjsubs=[],use_link=False): """makePathArray(docobj,path,count,xlate,align,pathobjsubs,use_link): distribute count copies of a document baseobject along a pathobject or subobjects of a pathobject. Optionally translates each copy by FreeCAD.Vector xlate direction and distance to adjust for difference in shape centre vs shape reference point. Optionally aligns baseobject to tangent/normal/binormal of path.""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return if use_link: obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","PathArray",_PathArray(None),None,True) else: obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","PathArray") _PathArray(obj) obj.Base = baseobject obj.PathObj = pathobject if pathobjsubs: sl = [] for sub in pathobjsubs: sl.append((obj.PathObj,sub)) obj.PathSubs = list(sl) if count > 1: obj.Count = count if xlate: obj.Xlate = xlate obj.Align = align if gui: if use_link: _ViewProviderDraftLink(obj.ViewObject) else: _ViewProviderDraftArray(obj.ViewObject) formatObject(obj,obj.Base) if len(obj.Base.ViewObject.DiffuseColor) > 1: obj.ViewObject.Proxy.resetColors(obj.ViewObject) baseobject.ViewObject.hide() select(obj) return obj def makePointArray(base, ptlst): """makePointArray(base,pointlist):""" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","PointArray") _PointArray(obj, base, ptlst) obj.Base = base obj.PointList = ptlst if gui: _ViewProviderDraftArray(obj.ViewObject) base.ViewObject.hide() formatObject(obj,obj.Base) if len(obj.Base.ViewObject.DiffuseColor) > 1: obj.ViewObject.Proxy.resetColors(obj.ViewObject) select(obj) return obj def extrude(obj,vector,solid=False): """makeExtrusion(object,vector): extrudes the given object in the direction given by the vector. The original object gets hidden.""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return newobj = FreeCAD.ActiveDocument.addObject("Part::Extrusion","Extrusion") newobj.Base = obj newobj.Dir = vector newobj.Solid = solid if gui: obj.ViewObject.Visibility = False formatObject(newobj,obj) select(newobj) return newobj def joinWires(wires, joinAttempts = 0): """joinWires(objects): merges a set of wires where possible, if any of those wires have a coincident start and end point""" if joinAttempts > len(wires): return joinAttempts += 1 for wire1Index, wire1 in enumerate(wires): for wire2Index, wire2 in enumerate(wires): if wire2Index <= wire1Index: continue if joinTwoWires(wire1, wire2): wires.pop(wire2Index) break joinWires(wires, joinAttempts) def joinTwoWires(wire1, wire2): """joinTwoWires(object, object): joins two wires if they share a common point as a start or an end. BUG: it occasionally fails to join lines even if the lines visually share a point. This is a rounding error in the comparison of the shared point; a small difference will result in the points being considered different and thus the lines not joining. Test properly using `DraftVecUtils.equals` because then it will consider the precision set in the Draft preferences. """ wire1AbsPoints = [wire1.Placement.multVec(point) for point in wire1.Points] wire2AbsPoints = [wire2.Placement.multVec(point) for point in wire2.Points] if (wire1AbsPoints[0] == wire2AbsPoints[-1] and wire1AbsPoints[-1] == wire2AbsPoints[0]) \ or (wire1AbsPoints[0] == wire2AbsPoints[0] and wire1AbsPoints[-1] == wire2AbsPoints[-1]): wire2AbsPoints.pop() wire1.Closed = True elif wire1AbsPoints[0] == wire2AbsPoints[0]: wire1AbsPoints = list(reversed(wire1AbsPoints)) elif wire1AbsPoints[0] == wire2AbsPoints[-1]: wire1AbsPoints = list(reversed(wire1AbsPoints)) wire2AbsPoints = list(reversed(wire2AbsPoints)) elif wire1AbsPoints[-1] == wire2AbsPoints[-1]: wire2AbsPoints = list(reversed(wire2AbsPoints)) elif wire1AbsPoints[-1] == wire2AbsPoints[0]: pass else: return False wire2AbsPoints.pop(0) wire1.Points = [wire1.Placement.inverse().multVec(point) for point in wire1AbsPoints] + [wire1.Placement.inverse().multVec(point) for point in wire2AbsPoints] FreeCAD.ActiveDocument.removeObject(wire2.Name) return True def split(wire, newPoint, edgeIndex): if getType(wire) != "Wire": return elif wire.Closed: splitClosedWire(wire, edgeIndex) else: splitOpenWire(wire, newPoint, edgeIndex) def splitClosedWire(wire, edgeIndex): wire.Closed = False if edgeIndex == len(wire.Points): makeWire([wire.Placement.multVec(wire.Points[0]), wire.Placement.multVec(wire.Points[-1])], placement=wire.Placement) else: makeWire([wire.Placement.multVec(wire.Points[edgeIndex-1]), wire.Placement.multVec(wire.Points[edgeIndex])], placement=wire.Placement) wire.Points = list(reversed(wire.Points[0:edgeIndex])) + list(reversed(wire.Points[edgeIndex:])) def splitOpenWire(wire, newPoint, edgeIndex): wire1Points = [] wire2Points = [] for index, point in enumerate(wire.Points): if index == edgeIndex: wire1Points.append(wire.Placement.inverse().multVec(newPoint)) wire2Points.append(newPoint) wire2Points.append(wire.Placement.multVec(point)) elif index < edgeIndex: wire1Points.append(point) elif index > edgeIndex: wire2Points.append(wire.Placement.multVec(point)) wire.Points = wire1Points makeWire(wire2Points, placement=wire.Placement) def fuse(object1,object2): """fuse(oject1,object2): returns an object made from the union of the 2 given objects. If the objects are coplanar, a special Draft Wire is used, otherwise we use a standard Part fuse.""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return import DraftGeomUtils, Part # testing if we have holes: holes = False fshape = object1.Shape.fuse(object2.Shape) fshape = fshape.removeSplitter() for f in fshape.Faces: if len(f.Wires) > 1: holes = True if DraftGeomUtils.isCoplanar(object1.Shape.fuse(object2.Shape).Faces) and not holes: obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython","Fusion") _Wire(obj) if gui: _ViewProviderWire(obj.ViewObject) obj.Base = object1 obj.Tool = object2 elif holes: # temporary hack, since Part::Fuse objects don't remove splitters obj = FreeCAD.ActiveDocument.addObject("Part::Feature","Fusion") obj.Shape = fshape else: obj = FreeCAD.ActiveDocument.addObject("Part::Fuse","Fusion") obj.Base = object1 obj.Tool = object2 if gui: object1.ViewObject.Visibility = False object2.ViewObject.Visibility = False formatObject(obj,object1) select(obj) return obj def cut(object1,object2): """cut(oject1,object2): returns a cut object made from the difference of the 2 given objects.""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return obj = FreeCAD.ActiveDocument.addObject("Part::Cut","Cut") obj.Base = object1 obj.Tool = object2 object1.ViewObject.Visibility = False object2.ViewObject.Visibility = False formatObject(obj,object1) select(obj) return obj def moveVertex(object, vertex_index, vector): points = object.Points points[vertex_index] = points[vertex_index].add(vector) object.Points = points def moveEdge(object, edge_index, vector): moveVertex(object, edge_index, vector) if isClosedEdge(edge_index, object): moveVertex(object, 0, vector) else: moveVertex(object, edge_index+1, vector) def copyMovedEdges(arguments): copied_edges = [] for argument in arguments: copied_edges.append(copyMovedEdge(argument[0], argument[1], argument[2])) joinWires(copied_edges) def copyMovedEdge(object, edge_index, vector): vertex1 = object.Placement.multVec(object.Points[edge_index]).add(vector) if isClosedEdge(edge_index, object): vertex2 = object.Placement.multVec(object.Points[0]).add(vector) else: vertex2 = object.Placement.multVec(object.Points[edge_index+1]).add(vector) return makeLine(vertex1, vertex2) def copyRotatedEdges(arguments): copied_edges = [] for argument in arguments: copied_edges.append(copyRotatedEdge(argument[0], argument[1], argument[2], argument[3], argument[4])) joinWires(copied_edges) def copyRotatedEdge(object, edge_index, angle, center, axis): vertex1 = rotateVectorFromCenter( object.Placement.multVec(object.Points[edge_index]), angle, axis, center) if isClosedEdge(edge_index, object): vertex2 = rotateVectorFromCenter( object.Placement.multVec(object.Points[0]), angle, axis, center) else: vertex2 = rotateVectorFromCenter( object.Placement.multVec(object.Points[edge_index+1]), angle, axis, center) return makeLine(vertex1, vertex2) def isClosedEdge(edge_index, object): return edge_index + 1 >= len(object.Points) def move(objectslist,vector,copy=False): """move(objects,vector,[copy]): Moves the objects contained in objects (that can be an object or a list of objects) in the direction and distance indicated by the given vector. If copy is True, the actual objects are not moved, but copies are created instead. The objects (or their copies) are returned.""" typecheck([(vector,Vector), (copy,bool)], "move") if not isinstance(objectslist,list): objectslist = [objectslist] objectslist.extend(getMovableChildren(objectslist)) newobjlist = [] newgroups = {} objectslist = filterObjectsForModifiers(objectslist, copy) for obj in objectslist: newobj = None # real_vector have been introduced to take into account # the possibility that object is inside an App::Part if hasattr(obj, "getGlobalPlacement"): v_minus_global = obj.getGlobalPlacement().inverse().Rotation.multVec(vector) real_vector = obj.Placement.Rotation.multVec(v_minus_global) else: real_vector = vector if getType(obj) == "Point": v = Vector(obj.X,obj.Y,obj.Z) v = v.add(real_vector) if copy: newobj = makeCopy(obj) else: newobj = obj newobj.X = v.x newobj.Y = v.y newobj.Z = v.z elif obj.isDerivedFrom("App::DocumentObjectGroup"): pass elif hasattr(obj,'Shape'): if copy: newobj = makeCopy(obj) else: newobj = obj pla = newobj.Placement pla.move(real_vector) elif getType(obj) == "Annotation": if copy: newobj = FreeCAD.ActiveDocument.addObject("App::Annotation",getRealName(obj.Name)) newobj.LabelText = obj.LabelText if gui: formatObject(newobj,obj) else: newobj = obj newobj.Position = obj.Position.add(real_vector) elif getType(obj) == "DraftText": if copy: newobj = FreeCAD.ActiveDocument.addObject("App::FeaturePython",getRealName(obj.Name)) DraftText(newobj) if gui: ViewProviderDraftText(newobj.ViewObject) formatObject(newobj,obj) newobj.Text = obj.Text newobj.Placement = obj.Placement if gui: formatObject(newobj,obj) else: newobj = obj newobj.Placement.Base = obj.Placement.Base.add(real_vector) elif getType(obj) in ["Dimension","LinearDimension"]: if copy: newobj = FreeCAD.ActiveDocument.addObject("App::FeaturePython",getRealName(obj.Name)) _Dimension(newobj) if gui: _ViewProviderDimension(newobj.ViewObject) formatObject(newobj,obj) else: newobj = obj newobj.Start = obj.Start.add(real_vector) newobj.End = obj.End.add(real_vector) newobj.Dimline = obj.Dimline.add(real_vector) else: if copy and obj.isDerivedFrom("Mesh::Feature"): print("Mesh copy not supported at the moment") # TODO newobj = obj if "Placement" in obj.PropertiesList: pla = obj.Placement pla.move(real_vector) if newobj is not None: newobjlist.append(newobj) if copy: for p in obj.InList: if p.isDerivedFrom("App::DocumentObjectGroup") and (p in objectslist): g = newgroups.setdefault(p.Name,FreeCAD.ActiveDocument.addObject(p.TypeId,p.Name)) g.addObject(newobj) break if getType(p) == "Layer": p.Proxy.addObject(p,newobj) if copy and getParam("selectBaseObjects",False): select(objectslist) else: select(newobjlist) if len(newobjlist) == 1: return newobjlist[0] return newobjlist def array(objectslist,arg1,arg2,arg3,arg4=None,arg5=None,arg6=None): """array(objectslist,xvector,yvector,xnum,ynum) for rectangular array, array(objectslist,xvector,yvector,zvector,xnum,ynum,znum) for rectangular array, or array(objectslist,center,totalangle,totalnum) for polar array: Creates an array of the objects contained in list (that can be an object or a list of objects) with, in case of rectangular array, xnum of iterations in the x direction at xvector distance between iterations, and same for y and z directions with yvector and ynum and zvector and znum. In case of polar array, center is a vector, totalangle is the angle to cover (in degrees) and totalnum is the number of objects, including the original. This function creates an array of independent objects. Use makeArray() to create a parametric array object.""" def rectArray(objectslist,xvector,yvector,xnum,ynum): typecheck([(xvector,Vector), (yvector,Vector), (xnum,int), (ynum,int)], "rectArray") if not isinstance(objectslist,list): objectslist = [objectslist] for xcount in range(xnum): currentxvector=Vector(xvector).multiply(xcount) if not xcount==0: move(objectslist,currentxvector,True) for ycount in range(ynum): currentxvector=FreeCAD.Base.Vector(currentxvector) currentyvector=currentxvector.add(Vector(yvector).multiply(ycount)) if not ycount==0: move(objectslist,currentyvector,True) def rectArray2(objectslist,xvector,yvector,zvector,xnum,ynum,znum): typecheck([(xvector,Vector), (yvector,Vector), (zvector,Vector),(xnum,int), (ynum,int),(znum,int)], "rectArray2") if not isinstance(objectslist,list): objectslist = [objectslist] for xcount in range(xnum): currentxvector=Vector(xvector).multiply(xcount) if not xcount==0: move(objectslist,currentxvector,True) for ycount in range(ynum): currentxvector=FreeCAD.Base.Vector(currentxvector) currentyvector=currentxvector.add(Vector(yvector).multiply(ycount)) if not ycount==0: move(objectslist,currentyvector,True) for zcount in range(znum): currentzvector=currentyvector.add(Vector(zvector).multiply(zcount)) if not zcount==0: move(objectslist,currentzvector,True) def polarArray(objectslist,center,angle,num): typecheck([(center,Vector), (num,int)], "polarArray") if not isinstance(objectslist,list): objectslist = [objectslist] fraction = float(angle)/num for i in range(num): currangle = fraction + (i*fraction) rotate(objectslist,currangle,center,copy=True) if arg6: rectArray2(objectslist,arg1,arg2,arg3,arg4,arg5,arg6) elif arg4: rectArray(objectslist,arg1,arg2,arg3,arg4) else: polarArray(objectslist,arg1,arg2,arg3) def filterObjectsForModifiers(objects, isCopied=False): filteredObjects = [] for object in objects: if hasattr(object, "MoveBase") and object.MoveBase and object.Base: parents = [] for parent in object.Base.InList: if parent.isDerivedFrom("Part::Feature"): parents.append(parent.Name) if len(parents) > 1: warningMessage = translate("draft","%s shares a base with %d other objects. Please check if you want to modify this.") % (object.Name,len(parents) - 1) FreeCAD.Console.PrintError(warningMessage) if FreeCAD.GuiUp: FreeCADGui.getMainWindow().showMessage(warningMessage, 0) filteredObjects.append(object.Base) elif hasattr(object,"Placement") and object.getEditorMode("Placement") == ["ReadOnly"] and not isCopied: FreeCAD.Console.PrintError(translate("Draft","%s cannot be modified because its placement is readonly.") % obj.Name) continue else: filteredObjects.append(object) return filteredObjects def rotateVertex(object, vertex_index, angle, center, axis): points = object.Points points[vertex_index] = object.Placement.inverse().multVec( rotateVectorFromCenter( object.Placement.multVec(points[vertex_index]), angle, axis, center)) object.Points = points def rotateVectorFromCenter(vector, angle, axis, center): rv = vector.sub(center) rv = DraftVecUtils.rotate(rv, math.radians(angle), axis) return center.add(rv) def rotateEdge(object, edge_index, angle, center, axis): rotateVertex(object, edge_index, angle, center, axis) if isClosedEdge(edge_index, object): rotateVertex(object, 0, angle, center, axis) else: rotateVertex(object, edge_index+1, angle, center, axis) def rotate(objectslist,angle,center=Vector(0,0,0),axis=Vector(0,0,1),copy=False): """rotate(objects,angle,[center,axis,copy]): Rotates the objects contained in objects (that can be a list of objects or an object) of the given angle (in degrees) around the center, using axis as a rotation axis. If axis is omitted, the rotation will be around the vertical Z axis. If copy is True, the actual objects are not moved, but copies are created instead. The objects (or their copies) are returned.""" import Part typecheck([(copy,bool)], "rotate") if not isinstance(objectslist,list): objectslist = [objectslist] objectslist.extend(getMovableChildren(objectslist)) newobjlist = [] newgroups = {} objectslist = filterObjectsForModifiers(objectslist, copy) for obj in objectslist: newobj = None # real_center and real_axis are introduced to take into account # the possibility that object is inside an App::Part if hasattr(obj, "getGlobalPlacement"): ci = obj.getGlobalPlacement().inverse().multVec(center) real_center = obj.Placement.multVec(ci) ai = obj.getGlobalPlacement().inverse().Rotation.multVec(axis) real_axis = obj.Placement.Rotation.multVec(ai) else: real_center = center real_axis = axis if copy: newobj = makeCopy(obj) else: newobj = obj if obj.isDerivedFrom("App::Annotation"): if axis.normalize() == Vector(1,0,0): newobj.ViewObject.RotationAxis = "X" newobj.ViewObject.Rotation = angle elif axis.normalize() == Vector(0,1,0): newobj.ViewObject.RotationAxis = "Y" newobj.ViewObject.Rotation = angle elif axis.normalize() == Vector(0,-1,0): newobj.ViewObject.RotationAxis = "Y" newobj.ViewObject.Rotation = -angle elif axis.normalize() == Vector(0,0,1): newobj.ViewObject.RotationAxis = "Z" newobj.ViewObject.Rotation = angle elif axis.normalize() == Vector(0,0,-1): newobj.ViewObject.RotationAxis = "Z" newobj.ViewObject.Rotation = -angle elif getType(obj) == "Point": v = Vector(obj.X,obj.Y,obj.Z) rv = v.sub(real_center) rv = DraftVecUtils.rotate(rv,math.radians(angle),real_axis) v = real_center.add(rv) newobj.X = v.x newobj.Y = v.y newobj.Z = v.z elif obj.isDerivedFrom("App::DocumentObjectGroup"): pass elif hasattr(obj,"Placement"): #FreeCAD.Console.PrintMessage("placement rotation\n") shape = Part.Shape() shape.Placement = obj.Placement shape.rotate(DraftVecUtils.tup(real_center), DraftVecUtils.tup(real_axis), angle) newobj.Placement = shape.Placement elif hasattr(obj,'Shape') and (getType(obj) not in ["WorkingPlaneProxy","BuildingPart"]): #think it make more sense to try first to rotate placement and later to try with shape. no? shape = obj.Shape.copy() shape.rotate(DraftVecUtils.tup(real_center), DraftVecUtils.tup(real_axis), angle) newobj.Shape = shape if copy: formatObject(newobj,obj) if newobj is not None: newobjlist.append(newobj) if copy: for p in obj.InList: if p.isDerivedFrom("App::DocumentObjectGroup") and (p in objectslist): g = newgroups.setdefault(p.Name,FreeCAD.ActiveDocument.addObject(p.TypeId,p.Name)) g.addObject(newobj) break if copy and getParam("selectBaseObjects",False): select(objectslist) else: select(newobjlist) if len(newobjlist) == 1: return newobjlist[0] return newobjlist def scaleVectorFromCenter(vector, scale, center): return vector.sub(center).scale(scale.x, scale.y, scale.z).add(center) def scaleVertex(object, vertex_index, scale, center): points = object.Points points[vertex_index] = object.Placement.inverse().multVec( scaleVectorFromCenter( object.Placement.multVec(points[vertex_index]), scale, center)) object.Points = points def scaleEdge(object, edge_index, scale, center): scaleVertex(object, edge_index, scale, center) if isClosedEdge(edge_index, object): scaleVertex(object, 0, scale, center) else: scaleVertex(object, edge_index+1, scale, center) def copyScaledEdges(arguments): copied_edges = [] for argument in arguments: copied_edges.append(copyScaledEdge(argument[0], argument[1], argument[2], argument[3])) joinWires(copied_edges) def copyScaledEdge(object, edge_index, scale, center): vertex1 = scaleVectorFromCenter( object.Placement.multVec(object.Points[edge_index]), scale, center) if isClosedEdge(edge_index, object): vertex2 = scaleVectorFromCenter( object.Placement.multVec(object.Points[0]), scale, center) else: vertex2 = scaleVectorFromCenter( object.Placement.multVec(object.Points[edge_index+1]), scale, center) return makeLine(vertex1, vertex2) def scale(objectslist,scale=Vector(1,1,1),center=Vector(0,0,0),copy=False): """scale(objects,vector,[center,copy,legacy]): Scales the objects contained in objects (that can be a list of objects or an object) of the given scale factors defined by the given vector (in X, Y and Z directions) around given center. If copy is True, the actual objects are not moved, but copies are created instead. The objects (or their copies) are returned.""" if not isinstance(objectslist, list): objectslist = [objectslist] newobjlist = [] for obj in objectslist: if copy: newobj = makeCopy(obj) else: newobj = obj if hasattr(obj,'Shape'): scaled_shape = obj.Shape.copy() m = FreeCAD.Matrix() m.move(obj.Placement.Base.negative()) m.move(center.negative()) m.scale(scale.x,scale.y,scale.z) m.move(center) m.move(obj.Placement.Base) scaled_shape = scaled_shape.transformGeometry(m) if getType(obj) == "Rectangle": p = [] for v in scaled_shape.Vertexes: p.append(v.Point) pl = obj.Placement.copy() pl.Base = p[0] diag = p[2].sub(p[0]) bb = p[1].sub(p[0]) bh = p[3].sub(p[0]) nb = DraftVecUtils.project(diag,bb) nh = DraftVecUtils.project(diag,bh) if obj.Length < 0: l = -nb.Length else: l = nb.Length if obj.Height < 0: h = -nh.Length else: h = nh.Length newobj.Length = l newobj.Height = h tr = p[0].sub(obj.Shape.Vertexes[0].Point) newobj.Placement = pl elif getType(obj) == "Wire" or getType(obj) == "BSpline": for index, point in enumerate(newobj.Points): scaleVertex(newobj, index, scale, center) elif hasattr(obj,'Shape'): newobj.Shape = scaled_shape elif (obj.TypeId == "App::Annotation"): factor = scale.y * obj.ViewObject.FontSize newobj.ViewObject.FontSize = factor d = obj.Position.sub(center) newobj.Position = center.add(Vector(d.x*scale.x,d.y*scale.y,d.z*scale.z)) if copy: formatObject(newobj,obj) newobjlist.append(newobj) if copy and getParam("selectBaseObjects",False): select(objectslist) else: select(newobjlist) if len(newobjlist) == 1: return newobjlist[0] return newobjlist def offset(obj,delta,copy=False,bind=False,sym=False,occ=False): """offset(object,delta,[copymode],[bind]): offsets the given wire by applying the given delta Vector to its first vertex. If copymode is True, another object is created, otherwise the same object gets offset. If bind is True, and provided the wire is open, the original and the offset wires will be bound by their endpoints, forming a face if sym is True, bind must be true too, and the offset is made on both sides, the total width being the given delta length. If offsetting a BSpline, the delta must not be a Vector but a list of Vectors, one for each node of the spline.""" import Part, DraftGeomUtils newwire = None delete = None if getType(obj) in ["Sketch","Part"]: copy = True print("the offset tool is currently unable to offset a non-Draft object directly - Creating a copy") def getRect(p,obj): """returns length,height,placement""" pl = obj.Placement.copy() pl.Base = p[0] diag = p[2].sub(p[0]) bb = p[1].sub(p[0]) bh = p[3].sub(p[0]) nb = DraftVecUtils.project(diag,bb) nh = DraftVecUtils.project(diag,bh) if obj.Length.Value < 0: l = -nb.Length else: l = nb.Length if obj.Height.Value < 0: h = -nh.Length else: h = nh.Length return l,h,pl def getRadius(obj,delta): """returns a new radius for a regular polygon""" an = math.pi/obj.FacesNumber nr = DraftVecUtils.rotate(delta,-an) nr.multiply(1/math.cos(an)) nr = obj.Shape.Vertexes[0].Point.add(nr) nr = nr.sub(obj.Placement.Base) nr = nr.Length if obj.DrawMode == "inscribed": return nr else: return nr * math.cos(math.pi/obj.FacesNumber) newwire = None if getType(obj) == "Circle": pass elif getType(obj) == "BSpline": pass else: if sym: d1 = Vector(delta).multiply(0.5) d2 = d1.negative() n1 = DraftGeomUtils.offsetWire(obj.Shape,d1) n2 = DraftGeomUtils.offsetWire(obj.Shape,d2) else: if isinstance(delta,float) and (len(obj.Shape.Edges) == 1): # circle c = obj.Shape.Edges[0].Curve nc = Part.Circle(c.Center,c.Axis,delta) if len(obj.Shape.Vertexes) > 1: nc = Part.ArcOfCircle(nc,obj.Shape.Edges[0].FirstParameter,obj.Shape.Edges[0].LastParameter) newwire = Part.Wire(nc.toShape()) p = [] else: newwire = DraftGeomUtils.offsetWire(obj.Shape,delta) if DraftGeomUtils.hasCurves(newwire) and copy: p = [] else: p = DraftGeomUtils.getVerts(newwire) if occ: newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Offset") newobj.Shape = DraftGeomUtils.offsetWire(obj.Shape,delta,occ=True) formatObject(newobj,obj) if not copy: delete = obj.Name elif bind: if not DraftGeomUtils.isReallyClosed(obj.Shape): if sym: s1 = n1 s2 = n2 else: s1 = obj.Shape s2 = newwire if s1 and s2: w1 = s1.Edges w2 = s2.Edges w3 = Part.LineSegment(s1.Vertexes[0].Point,s2.Vertexes[0].Point).toShape() w4 = Part.LineSegment(s1.Vertexes[-1].Point,s2.Vertexes[-1].Point).toShape() newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Offset") newobj.Shape = Part.Face(Part.Wire(w1+[w3]+w2+[w4])) else: print("Draft.offset: Unable to bind wires") else: newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Offset") newobj.Shape = Part.Face(obj.Shape.Wires[0]) if not copy: delete = obj.Name elif copy: newobj = None if sym: return None if getType(obj) == "Wire": if p: newobj = makeWire(p) newobj.Closed = obj.Closed elif newwire: newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Offset") newobj.Shape = newwire else: print("Draft.offset: Unable to duplicate this object") elif getType(obj) == "Rectangle": if p: length,height,plac = getRect(p,obj) newobj = makeRectangle(length,height,plac) elif newwire: newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Offset") newobj.Shape = newwire else: print("Draft.offset: Unable to duplicate this object") elif getType(obj) == "Circle": pl = obj.Placement newobj = makeCircle(delta) newobj.FirstAngle = obj.FirstAngle newobj.LastAngle = obj.LastAngle newobj.Placement = pl elif getType(obj) == "Polygon": pl = obj.Placement newobj = makePolygon(obj.FacesNumber) newobj.Radius = getRadius(obj,delta) newobj.DrawMode = obj.DrawMode newobj.Placement = pl elif getType(obj) == "BSpline": newobj = makeBSpline(delta) newobj.Closed = obj.Closed else: # try to offset anyway try: if p: newobj = makeWire(p) newobj.Closed = obj.Shape.isClosed() except Part.OCCError: pass if not(newobj) and newwire: newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Offset") newobj.Shape = newwire else: print("Draft.offset: Unable to create an offset") if newobj: formatObject(newobj,obj) else: newobj = None if sym: return None if getType(obj) == "Wire": if obj.Base or obj.Tool: FreeCAD.Console.PrintWarning("Warning: object history removed\n") obj.Base = None obj.Tool = None obj.Points = p elif getType(obj) == "BSpline": #print(delta) obj.Points = delta #print("done") elif getType(obj) == "Rectangle": length,height,plac = getRect(p,obj) obj.Placement = plac obj.Length = length obj.Height = height elif getType(obj) == "Circle": obj.Radius = delta elif getType(obj) == "Polygon": obj.Radius = getRadius(obj,delta) elif getType(obj) == 'Part': print("unsupported object") # TODO newobj = obj if copy and getParam("selectBaseObjects",False): select(newobj) else: select(obj) if delete: FreeCAD.ActiveDocument.removeObject(delete) return newobj def draftify(objectslist,makeblock=False,delete=True): """draftify(objectslist,[makeblock],[delete]): turns each object of the given list (objectslist can also be a single object) into a Draft parametric wire. If makeblock is True, multiple objects will be grouped in a block. If delete = False, old objects are not deleted""" import DraftGeomUtils, Part if not isinstance(objectslist,list): objectslist = [objectslist] newobjlist = [] for obj in objectslist: if hasattr(obj,'Shape'): for cluster in Part.getSortedClusters(obj.Shape.Edges): w = Part.Wire(cluster) if DraftGeomUtils.hasCurves(w): if (len(w.Edges) == 1) and (DraftGeomUtils.geomType(w.Edges[0]) == "Circle"): nobj = makeCircle(w.Edges[0]) else: nobj = FreeCAD.ActiveDocument.addObject("Part::Feature",obj.Name) nobj.Shape = w else: nobj = makeWire(w) newobjlist.append(nobj) formatObject(nobj,obj) # sketches are always in wireframe mode. In Draft we don't like that! if FreeCAD.GuiUp: nobj.ViewObject.DisplayMode = "Flat Lines" if delete: FreeCAD.ActiveDocument.removeObject(obj.Name) if makeblock: return makeBlock(newobjlist) else: if len(newobjlist) == 1: return newobjlist[0] return newobjlist def getDXF(obj,direction=None): """getDXF(object,[direction]): returns a DXF entity from the given object. If direction is given, the object is projected in 2D.""" plane = None result = "" if obj.isDerivedFrom("Drawing::View") or obj.isDerivedFrom("TechDraw::DrawView"): if obj.Source.isDerivedFrom("App::DocumentObjectGroup"): for o in obj.Source.Group: result += getDXF(o,obj.Direction) else: result += getDXF(obj.Source,obj.Direction) return result if direction: if isinstance(direction,FreeCAD.Vector): if direction != Vector(0,0,0): plane = WorkingPlane.plane() plane.alignToPointAndAxis(Vector(0,0,0),direction) def getProj(vec): if not plane: return vec nx = DraftVecUtils.project(vec,plane.u) ny = DraftVecUtils.project(vec,plane.v) return Vector(nx.Length,ny.Length,0) if getType(obj) in ["Dimension","LinearDimension"]: p1 = getProj(obj.Start) p2 = getProj(obj.End) p3 = getProj(obj.Dimline) result += "0\nDIMENSION\n8\n0\n62\n0\n3\nStandard\n70\n1\n" result += "10\n"+str(p3.x)+"\n20\n"+str(p3.y)+"\n30\n"+str(p3.z)+"\n" result += "13\n"+str(p1.x)+"\n23\n"+str(p1.y)+"\n33\n"+str(p1.z)+"\n" result += "14\n"+str(p2.x)+"\n24\n"+str(p2.y)+"\n34\n"+str(p2.z)+"\n" elif getType(obj) == "Annotation": p = getProj(obj.Position) count = 0 for t in obj.LabeLtext: result += "0\nTEXT\n8\n0\n62\n0\n" result += "10\n"+str(p.x)+"\n20\n"+str(p.y+count)+"\n30\n"+str(p.z)+"\n" result += "40\n1\n" result += "1\n"+str(t)+"\n" result += "7\nSTANDARD\n" count += 1 elif hasattr(obj,'Shape'): # TODO do this the Draft way, for ex. using polylines and rectangles import Drawing if not direction: direction = FreeCAD.Vector(0,0,-1) if DraftVecUtils.isNull(direction): direction = FreeCAD.Vector(0,0,-1) try: d = Drawing.projectToDXF(obj.Shape,direction) except: print("Draft.getDXF: Unable to project ",obj.Label," to ",direction) else: result += d else: print("Draft.getDXF: Unsupported object: ",obj.Label) return result def getrgb(color,testbw=True): """getRGB(color,[testbw]): returns a rgb value #000000 from a freecad color if testwb = True (default), pure white will be converted into pure black""" r = str(hex(int(color[0]*255)))[2:].zfill(2) g = str(hex(int(color[1]*255)))[2:].zfill(2) b = str(hex(int(color[2]*255)))[2:].zfill(2) col = "#"+r+g+b if testbw: if col == "#ffffff": #print(getParam('SvgLinesBlack')) if getParam('SvgLinesBlack',True): col = "#000000" return col import getSVG as svg getSVG = svg.getSVG def makeDrawingView(obj,page,lwmod=None,tmod=None,otherProjection=None): """ makeDrawingView(object,page,[lwmod,tmod]) - adds a View of the given object to the given page. lwmod modifies lineweights (in percent), tmod modifies text heights (in percent). The Hint scale, X and Y of the page are used. """ if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return if getType(obj) == "SectionPlane": import ArchSectionPlane viewobj = FreeCAD.ActiveDocument.addObject("Drawing::FeatureViewPython","View") page.addObject(viewobj) ArchSectionPlane._ArchDrawingView(viewobj) viewobj.Source = obj viewobj.Label = "View of "+obj.Name elif getType(obj) == "Panel": import ArchPanel viewobj = ArchPanel.makePanelView(obj,page) else: viewobj = FreeCAD.ActiveDocument.addObject("Drawing::FeatureViewPython","View"+obj.Name) _DrawingView(viewobj) page.addObject(viewobj) if (otherProjection): if hasattr(otherProjection,"Scale"): viewobj.Scale = otherProjection.Scale if hasattr(otherProjection,"X"): viewobj.X = otherProjection.X if hasattr(otherProjection,"Y"): viewobj.Y = otherProjection.Y if hasattr(otherProjection,"Rotation"): viewobj.Rotation = otherProjection.Rotation if hasattr(otherProjection,"Direction"): viewobj.Direction = otherProjection.Direction else: if hasattr(page.ViewObject,"HintScale"): viewobj.Scale = page.ViewObject.HintScale if hasattr(page.ViewObject,"HintOffsetX"): viewobj.X = page.ViewObject.HintOffsetX if hasattr(page.ViewObject,"HintOffsetY"): viewobj.Y = page.ViewObject.HintOffsetY viewobj.Source = obj if lwmod: viewobj.LineweightModifier = lwmod if tmod: viewobj.TextModifier = tmod if hasattr(obj.ViewObject,"Pattern"): if str(obj.ViewObject.Pattern) in list(svgpatterns().keys()): viewobj.FillStyle = str(obj.ViewObject.Pattern) if hasattr(obj.ViewObject,"DrawStyle"): viewobj.LineStyle = obj.ViewObject.DrawStyle if hasattr(obj.ViewObject,"LineColor"): viewobj.LineColor = obj.ViewObject.LineColor elif hasattr(obj.ViewObject,"TextColor"): viewobj.LineColor = obj.ViewObject.TextColor return viewobj def makeShape2DView(baseobj,projectionVector=None,facenumbers=[]): """ makeShape2DView(object,[projectionVector,facenumbers]) - adds a 2D shape to the document, which is a 2D projection of the given object. A specific projection vector can also be given. You can also specify a list of face numbers to be considered in individual faces mode. """ if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython","Shape2DView") _Shape2DView(obj) if gui: _ViewProviderDraftAlt(obj.ViewObject) obj.Base = baseobj if projectionVector: obj.Projection = projectionVector if facenumbers: obj.FaceNumbers = facenumbers select(obj) return obj def makeSketch(objectslist,autoconstraints=False,addTo=None, delete=False,name="Sketch",radiusPrecision=-1): """makeSketch(objectslist,[autoconstraints],[addTo],[delete],[name],[radiusPrecision]): Makes a Sketch objectslist with the given Draft objects. * objectlist: can be single or list of objects of Draft type objects, Part::Feature, Part.Shape, or mix of them. * autoconstraints(False): if True, constraints will be automatically added to wire nodes, rectangles and circles. * addTo(None) : if set to an existing sketch, geometry will be added to it instead of creating a new one. * delete(False): if True, the original object will be deleted. If set to a string 'all' the object and all its linked object will be deleted * name('Sketch'): the name for the new sketch object * radiusPrecision(-1): If <0, disable radius constraint. If =0, add indiviaul radius constraint. If >0, the radius will be rounded according to this precision, and 'Equal' constraint will be added to curve with equal radius within precision.""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return import Part, DraftGeomUtils from Sketcher import Constraint import Sketcher import math StartPoint = 1 EndPoint = 2 MiddlePoint = 3 deletable = None if not isinstance(objectslist,(list,tuple)): objectslist = [objectslist] for obj in objectslist: if isinstance(obj,Part.Shape): shape = obj elif not hasattr(obj,'Shape'): FreeCAD.Console.PrintError(translate("draft","not shape found")) return None else: shape = obj.Shape if not DraftGeomUtils.isPlanar(shape): FreeCAD.Console.PrintError(translate("draft","All Shapes must be co-planar")) return None if addTo: nobj = addTo else: nobj = FreeCAD.ActiveDocument.addObject("Sketcher::SketchObject", name) deletable = nobj if FreeCAD.GuiUp: nobj.ViewObject.Autoconstraints = False # Collect constraints and add in one go to improve performance constraints = [] radiuses = {} def addRadiusConstraint(edge): try: if radiusPrecision<0: return if radiusPrecision==0: constraints.append(Constraint('Radius', nobj.GeometryCount-1, edge.Curve.Radius)) return r = round(edge.Curve.Radius,radiusPrecision) constraints.append(Constraint('Equal', radiuses[r],nobj.GeometryCount-1)) except KeyError: radiuses[r] = nobj.GeometryCount-1 constraints.append(Constraint('Radius',nobj.GeometryCount-1, r)) except AttributeError: pass def convertBezier(edge): if DraftGeomUtils.geomType(edge) == "BezierCurve": return(edge.Curve.toBSpline(edge.FirstParameter,edge.LastParameter).toShape()) else: return(edge) rotation = None for obj in objectslist: ok = False tp = getType(obj) if tp in ["Circle","Ellipse"]: if obj.Shape.Edges: if rotation is None: rotation = obj.Placement.Rotation edge = obj.Shape.Edges[0] if len(edge.Vertexes) == 1: newEdge = DraftGeomUtils.orientEdge(edge) nobj.addGeometry(newEdge) else: # make new ArcOfCircle circle = DraftGeomUtils.orientEdge(edge) angle = edge.Placement.Rotation.Angle axis = edge.Placement.Rotation.Axis circle.Center = DraftVecUtils.rotate(edge.Curve.Center, -angle, axis) first = math.radians(obj.FirstAngle) last = math.radians(obj.LastAngle) arc = Part.ArcOfCircle(circle, first, last) nobj.addGeometry(arc) addRadiusConstraint(edge) ok = True elif tp == "Rectangle": if rotation is None: rotation = obj.Placement.Rotation if obj.FilletRadius.Value == 0: for edge in obj.Shape.Edges: nobj.addGeometry(DraftGeomUtils.orientEdge(edge)) if autoconstraints: last = nobj.GeometryCount - 1 segs = [last-3,last-2,last-1,last] if obj.Placement.Rotation.Q == (0,0,0,1): constraints.append(Constraint("Coincident",last-3,EndPoint,last-2,StartPoint)) constraints.append(Constraint("Coincident",last-2,EndPoint,last-1,StartPoint)) constraints.append(Constraint("Coincident",last-1,EndPoint,last,StartPoint)) constraints.append(Constraint("Coincident",last,EndPoint,last-3,StartPoint)) constraints.append(Constraint("Horizontal",last-3)) constraints.append(Constraint("Vertical",last-2)) constraints.append(Constraint("Horizontal",last-1)) constraints.append(Constraint("Vertical",last)) ok = True elif tp in ["Wire","Polygon"]: if obj.FilletRadius.Value == 0: closed = False if tp == "Polygon": closed = True elif hasattr(obj,"Closed"): closed = obj.Closed if obj.Shape.Edges: if (len(obj.Shape.Vertexes) < 3): e = obj.Shape.Edges[0] nobj.addGeometry(Part.LineSegment(e.Curve,e.FirstParameter,e.LastParameter)) else: # Use the first three points to make a working plane. We've already # checked to make sure everything is coplanar plane = Part.Plane(*[i.Point for i in obj.Shape.Vertexes[:3]]) normal = plane.Axis if rotation is None: axis = FreeCAD.Vector(0,0,1).cross(normal) angle = DraftVecUtils.angle(normal, FreeCAD.Vector(0,0,1)) * FreeCAD.Units.Radian rotation = FreeCAD.Rotation(axis, angle) for edge in obj.Shape.Edges: # edge.rotate(FreeCAD.Vector(0,0,0), rotAxis, rotAngle) edge = DraftGeomUtils.orientEdge(edge, normal) nobj.addGeometry(edge) if autoconstraints: last = nobj.GeometryCount segs = list(range(last-len(obj.Shape.Edges),last-1)) for seg in segs: constraints.append(Constraint("Coincident",seg,EndPoint,seg+1,StartPoint)) if DraftGeomUtils.isAligned(nobj.Geometry[seg],"x"): constraints.append(Constraint("Vertical",seg)) elif DraftGeomUtils.isAligned(nobj.Geometry[seg],"y"): constraints.append(Constraint("Horizontal",seg)) if closed: constraints.append(Constraint("Coincident",last-1,EndPoint,segs[0],StartPoint)) ok = True elif tp == "BSpline": if obj.Shape.Edges: nobj.addGeometry(obj.Shape.Edges[0].Curve) nobj.exposeInternalGeometry(nobj.GeometryCount-1) ok = True elif tp == "BezCurve": if obj.Shape.Edges: bez = obj.Shape.Edges[0].Curve bsp = bez.toBSpline(bez.FirstParameter,bez.LastParameter) nobj.addGeometry(bsp) nobj.exposeInternalGeometry(nobj.GeometryCount-1) ok = True elif tp == 'Shape' or hasattr(obj,'Shape'): shape = obj if tp == 'Shape' else obj.Shape if not DraftGeomUtils.isPlanar(shape): FreeCAD.Console.PrintError(translate("draft","The given object is not planar and cannot be converted into a sketch.")) return None if rotation is None: #rotation = obj.Placement.Rotation norm = DraftGeomUtils.getNormal(shape) if norm: rotation = FreeCAD.Rotation(FreeCAD.Vector(0,0,1),norm) else: FreeCAD.Console.PrintWarning(translate("draft","Unable to guess the normal direction of this object")) rotation = FreeCAD.Rotation() norm = obj.Placement.Rotation.Axis if not shape.Wires: for e in shape.Edges: # unconnected edges newedge = convertBezier(e) nobj.addGeometry(DraftGeomUtils.orientEdge(newedge,norm,make_arc=True)) addRadiusConstraint(newedge) # if not addTo: # nobj.Placement.Rotation = DraftGeomUtils.calculatePlacement(shape).Rotation if autoconstraints: for wire in shape.Wires: last_count = nobj.GeometryCount edges = wire.OrderedEdges for edge in edges: newedge = convertBezier(edge) nobj.addGeometry(DraftGeomUtils.orientEdge( newedge,norm,make_arc=True)) addRadiusConstraint(newedge) for i,g in enumerate(nobj.Geometry[last_count:]): if edges[i].Closed: continue seg = last_count+i if DraftGeomUtils.isAligned(g,"x"): constraints.append(Constraint("Vertical",seg)) elif DraftGeomUtils.isAligned(g,"y"): constraints.append(Constraint("Horizontal",seg)) if seg == nobj.GeometryCount-1: if not wire.isClosed(): break g2 = nobj.Geometry[last_count] seg2 = last_count else: seg2 = seg+1 g2 = nobj.Geometry[seg2] end1 = g.value(g.LastParameter) start2 = g2.value(g2.FirstParameter) if DraftVecUtils.equals(end1,start2) : constraints.append(Constraint( "Coincident",seg,EndPoint,seg2,StartPoint)) continue end2 = g2.value(g2.LastParameter) start1 = g.value(g.FirstParameter) if DraftVecUtils.equals(end2,start1): constraints.append(Constraint( "Coincident",seg,StartPoint,seg2,EndPoint)) elif DraftVecUtils.equals(start1,start2): constraints.append(Constraint( "Coincident",seg,StartPoint,seg2,StartPoint)) elif DraftVecUtils.equals(end1,end2): constraints.append(Constraint( "Coincident",seg,EndPoint,seg2,EndPoint)) else: for wire in shape.Wires: for edge in wire.OrderedEdges: newedge = convertBezier(edge) nobj.addGeometry(DraftGeomUtils.orientEdge( newedge,norm,make_arc=True)) ok = True formatObject(nobj,obj) if ok and delete and hasattr(obj,'Shape'): doc = obj.Document def delObj(obj): if obj.InList: FreeCAD.Console.PrintWarning(translate("draft", "Cannot delete object {} with dependency".format(obj.Label))+"\n") else: doc.removeObject(obj.Name) try: if delete == 'all': objs = [obj] while objs: obj = objs[0] objs = objs[1:] + obj.OutList delObj(obj) else: delObj(obj) except Exception as ex: FreeCAD.Console.PrintWarning(translate("draft", "Failed to delete object {}: {}".format(obj.Label,ex))+"\n") if rotation: nobj.Placement.Rotation = rotation else: print("-----error!!! rotation is still None...") nobj.addConstraint(constraints) return nobj def makeShapeString(String,FontFile,Size = 100,Tracking = 0): """ShapeString(Text,FontFile,Height,Track): Turns a text string into a Compound Shape""" if not FreeCAD.ActiveDocument: FreeCAD.Console.PrintError("No active document. Aborting\n") return obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython","ShapeString") _ShapeString(obj) obj.String = String obj.FontFile = FontFile obj.Size = Size obj.Tracking = Tracking if gui: _ViewProviderDraft(obj.ViewObject) formatObject(obj) obrep = obj.ViewObject if "PointSize" in obrep.PropertiesList: obrep.PointSize = 1 # hide the segment end points select(obj) obj.recompute() return obj def mirror(objlist, p1, p2): """mirror(objlist, p1, p2) creates a Part::Mirror of the given object(s), along a plane defined by the 2 given points and the draft working plane normal. """ if not objlist: _err = "No object given" FreeCAD.Console.PrintError(translate("draft", _err) + "\n") return if p1 == p2: _err = "The two points are coincident" FreeCAD.Console.PrintError(translate("draft", _err) + "\n") return if not isinstance(objlist,list): objlist = [objlist] if hasattr(FreeCAD, "DraftWorkingPlane"): norm = FreeCAD.DraftWorkingPlane.getNormal() elif gui: norm = FreeCADGui.ActiveDocument.ActiveView.getViewDirection().negative() else: norm = FreeCAD.Vector(0,0,1) pnorm = p2.sub(p1).cross(norm).normalize() result = [] for obj in objlist: mir = FreeCAD.ActiveDocument.addObject("Part::Mirroring","mirror") mir.Label = "Mirror of " + obj.Label mir.Source = obj mir.Base = p1 mir.Normal = pnorm formatObject(mir, obj) result.append(mir) if len(result) == 1: result = result[0] select(result) return result def heal(objlist=None,delete=True,reparent=True): """heal([objlist],[delete],[reparent]) - recreates Draft objects that are damaged, for example if created from an earlier version. If delete is True, the damaged objects are deleted (default). If ran without arguments, all the objects in the document will be healed if they are damaged. If reparent is True (default), new objects go at the very same place in the tree than their original.""" auto = False if not objlist: objlist = FreeCAD.ActiveDocument.Objects print("Automatic mode: Healing whole document...") auto = True else: print("Manual mode: Force-healing selected objects...") if not isinstance(objlist,list): objlist = [objlist] dellist = [] got = False for obj in objlist: dtype = getType(obj) ftype = obj.TypeId if ftype in ["Part::FeaturePython","App::FeaturePython","Part::Part2DObjectPython","Drawing::FeatureViewPython"]: proxy = obj.Proxy if hasattr(obj,"ViewObject"): if hasattr(obj.ViewObject,"Proxy"): proxy = obj.ViewObject.Proxy if (proxy == 1) or (dtype in ["Unknown","Part"]) or (not auto): got = True dellist.append(obj.Name) props = obj.PropertiesList if ("Dimline" in props) and ("Start" in props): print("Healing " + obj.Name + " of type Dimension") nobj = makeCopy(obj,force="Dimension",reparent=reparent) elif ("Height" in props) and ("Length" in props): print("Healing " + obj.Name + " of type Rectangle") nobj = makeCopy(obj,force="Rectangle",reparent=reparent) elif ("Points" in props) and ("Closed" in props): if "BSpline" in obj.Name: print("Healing " + obj.Name + " of type BSpline") nobj = makeCopy(obj,force="BSpline",reparent=reparent) else: print("Healing " + obj.Name + " of type Wire") nobj = makeCopy(obj,force="Wire",reparent=reparent) elif ("Radius" in props) and ("FirstAngle" in props): print("Healing " + obj.Name + " of type Circle") nobj = makeCopy(obj,force="Circle",reparent=reparent) elif ("DrawMode" in props) and ("FacesNumber" in props): print("Healing " + obj.Name + " of type Polygon") nobj = makeCopy(obj,force="Polygon",reparent=reparent) elif ("FillStyle" in props) and ("FontSize" in props): nobj = makeCopy(obj,force="DrawingView",reparent=reparent) else: dellist.pop() print("Object " + obj.Name + " is not healable") if not got: print("No object seems to need healing") else: print("Healed ",len(dellist)," objects") if dellist and delete: for n in dellist: FreeCAD.ActiveDocument.removeObject(n) def upgrade(objects,delete=False,force=None): """upgrade(objects,delete=False,force=None): Upgrades the given object(s) (can be an object or a list of objects). If delete is True, old objects are deleted. The force attribute can be used to force a certain way of upgrading. It can be: makeCompound, closeGroupWires, makeSolid, closeWire, turnToParts, makeFusion, makeShell, makeFaces, draftify, joinFaces, makeSketchFace, makeWires Returns a dictionary containing two lists, a list of new objects and a list of objects to be deleted""" import Part, DraftGeomUtils if not isinstance(objects,list): objects = [objects] global deleteList, newList deleteList = [] addList = [] # definitions of actions to perform def turnToLine(obj): """turns an edge into a Draft line""" p1 = obj.Shape.Vertexes[0].Point p2 = obj.Shape.Vertexes[-1].Point newobj = makeLine(p1,p2) addList.append(newobj) deleteList.append(obj) return newobj def makeCompound(objectslist): """returns a compound object made from the given objects""" newobj = makeBlock(objectslist) addList.append(newobj) return newobj def closeGroupWires(groupslist): """closes 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): """turns 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 = FreeCAD.ActiveDocument.addObject("Part::Feature","Solid") newobj.Shape = sol addList.append(newobj) deleteList.append(obj) return newobj def closeWire(obj): """closes 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 getType(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 its start and end points identical (OCC bug) # in that case, although it is not closed, face works... f = Part.Face(w) newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Face") newobj.Shape = f else: edges.append(Part.LineSegment(p1,p0).toShape()) w = Part.Wire(Part.__sortEdges__(edges)) newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Wire") newobj.Shape = w addList.append(newobj) deleteList.append(obj) return newobj else: return None def turnToParts(meshes): """turn given meshes to parts""" result = False import Arch for mesh in meshes: sh = Arch.getShapeFromMesh(mesh.Mesh) if sh: newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Shell") newobj.Shape = sh addList.append(newobj) deleteList.append(mesh) result = True return result def makeFusion(obj1,obj2): """makes a Draft or Part fusion between 2 given objects""" newobj = fuse(obj1,obj2) if newobj: addList.append(newobj) return newobj return None def makeShell(objectslist): """makes a shell with the given objects""" params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") preserveFaceColor = params.GetBool("preserveFaceColor") # True preserveFaceNames = params.GetBool("preserveFaceNames") # True faces = [] facecolors = [[], []] if (preserveFaceColor) else None for obj in objectslist: faces.extend(obj.Shape.Faces) if (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(); but they still keep reference to their original colors - capture that in facecolors. Also, cannot w/ .ShapeColor here, 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 = FreeCAD.ActiveDocument.addObject("Part::Feature","Shell") newobj.Shape = sh if (preserveFaceNames): import re firstName = objectslist[0].Label nameNoTrailNumbers = re.sub("\d+$", "", firstName) newobj.Label = "{} {}".format(newobj.Label, nameNoTrailNumbers) if (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; addList.append(newobj) deleteList.extend(objectslist) return newobj return None def joinFaces(objectslist): """makes one big face from selected objects, if possible""" faces = [] for obj in objectslist: faces.extend(obj.Shape.Faces) u = faces.pop(0) for f in faces: u = u.fuse(f) if DraftGeomUtils.isCoplanar(faces): u = DraftGeomUtils.concatenate(u) if not DraftGeomUtils.hasCurves(u): # several coplanar and non-curved faces: they can become a Draft wire newobj = makeWire(u.Wires[0],closed=True,face=True) else: # if not possible, we do a non-parametric union newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Union") newobj.Shape = u addList.append(newobj) deleteList.extend(objectslist) return newobj return None def makeSketchFace(obj): """Makes a Draft face out of a sketch""" newobj = makeWire(obj.Shape,closed=True) if newobj: newobj.Base = obj obj.ViewObject.Visibility = False addList.append(newobj) 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 = FreeCAD.ActiveDocument.addObject("Part::Feature","Face") newobj.Shape = f addList.append(newobj) result = True if not o in deleteList: deleteList.append(o) return result def makeWires(objectslist): """joins edges in the given objects list into wires""" edges = [] for o in objectslist: for e in o.Shape.Edges: edges.append(e) try: nedges = Part.__sortEdges__(edges[:]) # for e in nedges: print("debug: ",e.Curve,e.Vertexes[0].Point,e.Vertexes[-1].Point) w = Part.Wire(nedges) except Part.OCCError: return None else: if len(w.Edges) == len(edges): newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Wire") newobj.Shape = w addList.append(newobj) deleteList.extend(objectslist) 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: loneedges.append(e) elif ob.isDerivedFrom("Mesh::Feature"): meshes.append(ob) objects = parts #print("objects:",objects," edges:",edges," wires:",wires," openwires:",openwires," faces:",faces) #print("groups:",groups," curves:",curves," facewires:",facewires, "loneedges:", loneedges) if force: if force in ["makeCompound","closeGroupWires","makeSolid","closeWire","turnToParts","makeFusion", "makeShell","makeFaces","draftify","joinFaces","makeSketchFace","makeWires","turnToLine"]: result = eval(force)(objects) else: FreeCAD.Console.PrintMessage(translate("Draft","Upgrade: Unknown force method:")+" "+force) result = None else: # 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: FreeCAD.Console.PrintMessage(translate("draft", "Found groups: closing each open object inside")+"\n") # if we have meshes, we try to turn them into shapes elif meshes: result = turnToParts(meshes) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found mesh(es): turning into Part shapes")+"\n") # 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): result = makeSolid(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 solidifiable object: solidifying it")+"\n") # we have exactly 2 objects: we fuse them elif (len(objects) == 2) and (not curves): result = makeFusion(objects[0],objects[1]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 2 objects: fusing them")+"\n") # we have many separate faces: we try to make a shell elif (len(objects) > 2) and (len(faces) > 1) and (not loneedges): result = makeShell(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found several objects: creating a shell")+"\n") # we have faces: we try to join them if they are coplanar elif len(faces) > 1: result = joinFaces(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found several coplanar objects or faces: creating one face")+"\n") # only one object: if not parametric, we "draftify" it elif len(objects) == 1 and (not objects[0].isDerivedFrom("Part::Part2DObjectPython")): result = draftify(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 non-parametric objects: draftifying it")+"\n") # we have only one object that contains one edge elif (not faces) and (len(objects) == 1) and (len(edges) == 1): # we have a closed sketch: Extract a face if objects[0].isDerivedFrom("Sketcher::SketchObject") and (len(edges[0].Vertexes) == 1): result = makeSketchFace(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 closed sketch object: creating a face from it")+"\n") else: # turn to Draft line e = objects[0].Shape.Edges[0] if isinstance(e.Curve,(Part.LineSegment,Part.Line)): result = turnToLine(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 linear object: converting to line")+"\n") # we have only closed wires, no faces elif wires and (not faces) and (not openwires): # we have a sketch: Extract a face if (len(objects) == 1) and objects[0].isDerivedFrom("Sketcher::SketchObject"): result = makeSketchFace(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 closed sketch object: creating a face from it")+"\n") # only closed wires else: result = makeFaces(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found closed wires: creating faces")+"\n") # special case, we have only one open wire. We close it, unless it has only 1 edge!" elif (len(openwires) == 1) and (not faces) and (not loneedges): result = closeWire(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 open wire: closing it")+"\n") # only open wires and edges: we try to join their edges elif openwires and (not wires) and (not faces): result = makeWires(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found several open wires: joining them")+"\n") # only loneedges: we try to join them elif loneedges and (not facewires): result = makeWires(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found several edges: wiring them")+"\n") # all other cases, if more than 1 object, make a compound elif (len(objects) > 1): result = makeCompound(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found several non-treatable objects: creating compound")+"\n") # no result has been obtained if not result: FreeCAD.Console.PrintMessage(translate("draft", "Unable to upgrade these objects.")+"\n") if delete: names = [] for o in deleteList: names.append(o.Name) deleteList = [] for n in names: FreeCAD.ActiveDocument.removeObject(n) select(addList) return [addList,deleteList] def downgrade(objects,delete=False,force=None): """downgrade(objects,delete=False,force=None): Downgrades the given object(s) (can be an object or a list of objects). If delete is True, old objects are deleted. The force attribute can be used to force a certain way of downgrading. It can be: explode, shapify, subtr, splitFaces, cut2, getWire, splitWires, splitCompounds. Returns a dictionary containing two lists, a list of new objects and a list of objects to be deleted""" import Part, DraftGeomUtils if not isinstance(objects,list): objects = [objects] global deleteList, addList deleteList = [] addList = [] # actions definitions def explode(obj): """explodes a Draft block""" pl = obj.Placement newobj = [] for o in obj.Components: o.ViewObject.Visibility = True o.Placement = o.Placement.multiply(pl) if newobj: deleteList(obj) return newobj return None def cut2(objects): """cuts first object from the last one""" newobj = cut(objects[0],objects[1]) if newobj: addList.append(newobj) return newobj return None 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 = FreeCAD.ActiveDocument.addObject("Part::Feature","Solid") newobj.Shape = s addList.append(newobj) result = True deleteList.append(o) return result def splitFaces(objects): """split faces contained in objects into new objects""" result = False params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") preserveFaceColor = params.GetBool("preserveFaceColor") # True preserveFaceNames = params.GetBool("preserveFaceNames") # True for o in objects: voDColors = o.ViewObject.DiffuseColor if (preserveFaceColor and hasattr(o,'ViewObject')) else None oLabel = o.Label if hasattr(o,'Label') else "" if o.Shape.Faces: for ind, f in enumerate(o.Shape.Faces): newobj = FreeCAD.ActiveDocument.addObject("Part::Feature","Face") newobj.Shape = f if preserveFaceNames: newobj.Label = "{} {}".format(oLabel, newobj.Label) if preserveFaceColor: """ At this point, some single-color objects might have just a single entry in voDColors for all their faces; handle that""" tcolor = voDColors[ind] if ind 1): result = splitCompounds(objects) #print(result) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 multi-solids compound: exploding it")+"\n") # 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'): result = shapify(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 parametric object: breaking its dependencies")+"\n") addList.append(result) #deleteList.append(objects[0]) # we have only 2 objects: cut 2nd from 1st elif len(objects) == 2: result = cut2(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 2 objects: subtracting them")+"\n") elif (len(faces) > 1): # one object with several faces: split it if len(objects) == 1: result = splitFaces(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found several faces: splitting them")+"\n") # several objects: remove all the faces from the first one else: result = subtr(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found several objects: subtracting them from the first one")+"\n") # only one face: we extract its wires elif (len(faces) > 0): result = getWire(objects[0]) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found 1 face: extracting its wires")+"\n") # no faces: split wire into single edges elif not onlyedges: result = splitWires(objects) if result: FreeCAD.Console.PrintMessage(translate("draft", "Found only wires: extracting their edges")+"\n") # no result has been obtained if not result: FreeCAD.Console.PrintMessage(translate("draft", "No more downgrade possible")+"\n") if delete: names = [] for o in deleteList: names.append(o.Name) deleteList = [] for n in names: FreeCAD.ActiveDocument.removeObject(n) select(addList) return [addList,deleteList] def getParameterFromV0(edge, offset): """return parameter at distance offset from edge.Vertexes[0] sb method in Part.TopoShapeEdge???""" lpt = edge.valueAt(edge.getParameterByLength(0)) vpt = edge.Vertexes[0].Point if not DraftVecUtils.equals(vpt, lpt): # this edge is flipped length = edge.Length - offset else: # this edge is right way around length = offset return (edge.getParameterByLength(length)) def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal=None): """Orient shape to tangent at parm offset along edge.""" import functools # http://en.wikipedia.org/wiki/Euler_angles # start with null Placement point so translate goes to right place. placement = FreeCAD.Placement() # preserve global orientation placement.Rotation = globalRotation placement.move(RefPt + xlate) if not align: return placement # unit +Z Probably defined elsewhere? z = FreeCAD.Vector(0, 0, 1) # y = FreeCAD.Vector(0, 1, 0) # unit +Y x = FreeCAD.Vector(1, 0, 0) # unit +X nullv = FreeCAD.Vector(0, 0, 0) # get local coord system - tangent, normal, binormal, if possible t = edge.tangentAt(getParameterFromV0(edge, offset)) t.normalize() try: if normal: n = normal else: n = edge.normalAt(getParameterFromV0(edge, offset)) n.normalize() b = (t.cross(n)) b.normalize() # no normal defined here except FreeCAD.Base.FreeCADError: n = nullv b = nullv FreeCAD.Console.PrintLog( "Draft PathArray.orientShape - Cannot calculate Path normal.\n") lnodes = z.cross(b) try: # Can't normalize null vector. lnodes.normalize() except: # pathological cases: pass # 1) can't determine normal, don't align. if n == nullv: psi = 0.0 theta = 0.0 phi = 0.0 FreeCAD.Console.PrintWarning( "Draft PathArray.orientShape - Path normal is Null. Cannot align.\n") elif abs(b.dot(z)) == 1.0: # 2) binormal is || z # align shape to tangent only psi = math.degrees(DraftVecUtils.angle(x, t, z)) theta = 0.0 phi = 0.0 FreeCAD.Console.PrintWarning( "Draft PathArray.orientShape - Gimbal lock. Infinite lnodes. Change Path or Base.\n") else: # regular case psi = math.degrees(DraftVecUtils.angle(x, lnodes, z)) theta = math.degrees(DraftVecUtils.angle(z, b, lnodes)) phi = math.degrees(DraftVecUtils.angle(lnodes, t, b)) rotations = [placement.Rotation] if psi != 0.0: rotations.insert(0, FreeCAD.Rotation(z, psi)) if theta != 0.0: rotations.insert(0, FreeCAD.Rotation(lnodes, theta)) if phi != 0.0: rotations.insert(0, FreeCAD.Rotation(b, phi)) if len(rotations) == 1: finalRotation = rotations[0] else: finalRotation = functools.reduce( lambda rot1, rot2: rot1.multiply(rot2), rotations) placement.Rotation = finalRotation return placement def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align): """Calculates the placements of a shape along a given path so that each copy will be distributed evenly""" import Part import DraftGeomUtils closedpath = DraftGeomUtils.isReallyClosed(pathwire) normal = DraftGeomUtils.getNormal(pathwire) path = Part.__sortEdges__(pathwire.Edges) ends = [] cdist = 0 for e in path: # find cumulative edge end distance cdist += e.Length ends.append(cdist) placements = [] # place the start shape pt = path[0].Vertexes[0].Point placements.append(calculatePlacement( shapeRotation, path[0], 0, pt, xlate, align, normal)) # closed path doesn't need shape on last vertex if not(closedpath): # place the end shape pt = path[-1].Vertexes[-1].Point placements.append(calculatePlacement( shapeRotation, path[-1], path[-1].Length, pt, xlate, align, normal)) if count < 3: return placements # place the middle shapes if closedpath: stop = count else: stop = count - 1 step = float(cdist) / stop remains = 0 travel = step for i in range(1, stop): # which edge in path should contain this shape? # avoids problems with float math travel > ends[-1] iend = len(ends) - 1 for j in range(0, len(ends)): if travel <= ends[j]: iend = j break # place shape at proper spot on proper edge remains = ends[iend] - travel offset = path[iend].Length - remains pt = path[iend].valueAt(getParameterFromV0(path[iend], offset)) placements.append(calculatePlacement( shapeRotation, path[iend], offset, pt, xlate, align, normal)) travel += step return placements #--------------------------------------------------------------------------- # Python Features definitions #--------------------------------------------------------------------------- import draftobjects.base _DraftObject = draftobjects.base.DraftObject class _ViewProviderDraftLink: "a view provider for link type object" def __init__(self,vobj): self.Object = vobj.Object vobj.Proxy = self def attach(self,vobj): self.Object = vobj.Object def __getstate__(self): return None def __setstate__(self, state): return None def getIcon(self): tp = self.Object.Proxy.Type if tp == 'Array': return ":/icons/Draft_LinkArray.svg" elif tp == 'PathArray': return ":/icons/Draft_PathLinkArray.svg" def claimChildren(self): obj = self.Object if hasattr(obj,'ExpandArray'): expand = obj.ExpandArray else: expand = obj.ShowElement if not expand: return [obj.Base] else: return obj.ElementList class _DrawingView(_DraftObject): """The Draft DrawingView object""" def __init__(self, obj): _DraftObject.__init__(self,obj,"DrawingView") obj.addProperty("App::PropertyVector","Direction","Shape View",QT_TRANSLATE_NOOP("App::Property","Projection direction")) obj.addProperty("App::PropertyFloat","LineWidth","View Style",QT_TRANSLATE_NOOP("App::Property","The width of the lines inside this object")) obj.addProperty("App::PropertyLength","FontSize","View Style",QT_TRANSLATE_NOOP("App::Property","The size of the texts inside this object")) obj.addProperty("App::PropertyLength","LineSpacing","View Style",QT_TRANSLATE_NOOP("App::Property","The spacing between lines of text")) obj.addProperty("App::PropertyColor","LineColor","View Style",QT_TRANSLATE_NOOP("App::Property","The color of the projected objects")) obj.addProperty("App::PropertyLink","Source","Base",QT_TRANSLATE_NOOP("App::Property","The linked object")) obj.addProperty("App::PropertyEnumeration","FillStyle","View Style",QT_TRANSLATE_NOOP("App::Property","Shape Fill Style")) obj.addProperty("App::PropertyEnumeration","LineStyle","View Style",QT_TRANSLATE_NOOP("App::Property","Line Style")) obj.addProperty("App::PropertyBool","AlwaysOn","View Style",QT_TRANSLATE_NOOP("App::Property","If checked, source objects are displayed regardless of being visible in the 3D model")) obj.FillStyle = ['shape color'] + list(svgpatterns().keys()) obj.LineStyle = ['Solid','Dashed','Dotted','Dashdot'] obj.LineWidth = 0.35 obj.FontSize = 12 def execute(self, obj): result = "" if hasattr(obj,"Source"): if obj.Source: if hasattr(obj,"LineStyle"): ls = obj.LineStyle else: ls = None if hasattr(obj,"LineColor"): lc = obj.LineColor else: lc = None if hasattr(obj,"LineSpacing"): lp = obj.LineSpacing else: lp = None if obj.Source.isDerivedFrom("App::DocumentObjectGroup"): svg = "" shapes = [] others = [] objs = getGroupContents([obj.Source]) for o in objs: v = o.ViewObject.isVisible() if hasattr(obj,"AlwaysOn"): if obj.AlwaysOn: v = True if v: svg += getSVG(o,obj.Scale,obj.LineWidth,obj.FontSize.Value,obj.FillStyle,obj.Direction,ls,lc,lp) else: svg = getSVG(obj.Source,obj.Scale,obj.LineWidth,obj.FontSize.Value,obj.FillStyle,obj.Direction,ls,lc,lp) result += '