diff --git a/src/Mod/Path/PathScripts/PathArray.py b/src/Mod/Path/PathScripts/PathArray.py index e33ca1a33d..ed892318cd 100644 --- a/src/Mod/Path/PathScripts/PathArray.py +++ b/src/Mod/Path/PathScripts/PathArray.py @@ -24,8 +24,10 @@ import FreeCAD import FreeCADGui import Path import PathScripts +from PathScripts import PathLog from PySide import QtCore import math +import random __doc__ = """Path Array object and FreeCAD command""" @@ -36,22 +38,28 @@ def translate(context, text, disambig=None): class ObjectArray: def __init__(self, obj): - obj.addProperty("App::PropertyLink", "Base", - "Path", "The path to array") + obj.addProperty("App::PropertyLinkList", "Base", + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The path(s) to array")) obj.addProperty("App::PropertyEnumeration", "Type", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Pattern method")) obj.addProperty("App::PropertyVectorDistance", "Offset", - "Path", "The spacing between the array copies in Linear pattern") + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The spacing between the array copies in Linear pattern")) obj.addProperty("App::PropertyInteger", "CopiesX", - "Path", "The number of copies in X direction in Linear pattern") + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The number of copies in X direction in Linear pattern")) obj.addProperty("App::PropertyInteger", "CopiesY", - "Path", "The number of copies in Y direction in Linear pattern") + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The number of copies in Y direction in Linear pattern")) obj.addProperty("App::PropertyAngle", "Angle", - "Path", "Total angle in Polar pattern") + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Total angle in Polar pattern")) obj.addProperty("App::PropertyInteger", "Copies", - "Path", "The number of copies in Linear 1D and Polar pattern") + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The number of copies in Linear 1D and Polar pattern")) obj.addProperty("App::PropertyVector", "Centre", - "Path", "The centre of rotation in Polar pattern") + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The centre of rotation in Polar pattern")) + obj.addProperty("App::PropertyBool", "SwapDirection", + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Make copies in X direction before Y in Linear 2D pattern")) + obj.addProperty("App::PropertyInteger", "JitterPercent", + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Percent of copies to randomly offset")) + obj.addProperty("App::PropertyVectorDistance", "JitterMagnitude", + "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Maximum random offset of copies")) obj.addProperty("App::PropertyLink", "ToolController", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool controller that will be used to calculate the path")) @@ -67,6 +75,9 @@ class ObjectArray: return None def setEditorProperties(self, obj): + obj.setEditorMode('JitterPercent', 0) + obj.setEditorMode('JitterMagnitude', 0) + obj.setEditorMode('ToolController', 2) if obj.Type == 'Linear2D': obj.setEditorMode('Angle', 2) obj.setEditorMode('Copies', 2) @@ -75,6 +86,7 @@ class ObjectArray: obj.setEditorMode('CopiesX', 0) obj.setEditorMode('CopiesY', 0) obj.setEditorMode('Offset', 0) + obj.setEditorMode('SwapDirection', False) elif obj.Type == 'Polar': obj.setEditorMode('Angle', 0) obj.setEditorMode('Copies', 0) @@ -155,57 +167,99 @@ class ObjectArray: return newPath + def calculateJitter(self, obj, pos): + if random.randint(0,100) < obj.JitterPercent: + pos.x = pos.x + random.uniform(-obj.JitterMagnitude.x, obj.JitterMagnitude.y) + pos.y = pos.y + random.uniform(-obj.JitterMagnitude.y, obj.JitterMagnitude.y) + pos.z = pos.z + random.uniform(-obj.JitterMagnitude.z, obj.JitterMagnitude.z) + return pos + + def execute(self, obj): - if obj.Base: - if not obj.Base.isDerivedFrom("Path::Feature"): - return - if not obj.Base.Path: - return - if not obj.Base.ToolController: - return - obj.ToolController = obj.Base.ToolController + # backwards compatibility for PathArrays created before support for multiple bases + if isinstance(obj.Base, list): + base = obj.Base + else: + base = [obj.Base] - # build copies - basepath = obj.Base.Path - output = "" - if obj.Type == 'Linear1D': - for i in range(obj.Copies): + if len(base)==0: + return + + obj.ToolController = base[0].ToolController + for b in base: + if not b.isDerivedFrom("Path::Feature"): + return + if not b.Path: + return + if not b.ToolController: + return + if b.ToolController != obj.ToolController: + # this may be important if Job output is split by tool controller + PathLog.warning(QtCore.QT_TRANSLATE_NOOP("App::Property",'Arrays of paths having different tool controllers are handled according to the tool controller of the first path.')) + + # build copies + output = "" + random.seed(obj.Name) + if obj.Type == 'Linear1D': + for i in range(obj.Copies): + pos = FreeCAD.Vector(obj.Offset.x * (i + 1), obj.Offset.y * (i + 1), 0) + pos = self.calculateJitter(obj, pos) + + for b in base: pl = FreeCAD.Placement() - pos = FreeCAD.Vector(obj.Offset.x * (i + 1), obj.Offset.y * (i + 1), 0) pl.move(pos) np = Path.Path([cm.transform(pl) - for cm in basepath.Commands]) + for cm in b.Path.Commands]) output += np.toGCode() - elif obj.Type == 'Linear2D': + elif obj.Type == 'Linear2D': + if obj.SwapDirection: + for i in range(obj.CopiesY + 1): + for j in range(obj.CopiesX + 1): + if (i % 2) == 0: + pos = FreeCAD.Vector(obj.Offset.x * j, obj.Offset.y * i, 0) + else: + pos = FreeCAD.Vector(obj.Offset.x * (obj.CopiesX - j), obj.Offset.y * i, 0) + pos = self.calculateJitter(obj, pos) + + for b in base: + pl = FreeCAD.Placement() + # do not process the index 0,0. It will be processed by the base Paths themselves + if not (i == 0 and j == 0): + pl.move(pos) + np = Path.Path([cm.transform(pl) for cm in b.Path.Commands]) + output += np.toGCode() + else: for i in range(obj.CopiesX + 1): for j in range(obj.CopiesY + 1): - pl = FreeCAD.Placement() - # do not process the index 0,0. It will be processed at basepath - if not (i == 0 and j == 0): - if (i % 2) == 0: - pos = FreeCAD.Vector(obj.Offset.x * i, obj.Offset.y * j, 0) - else: - pos = FreeCAD.Vector(obj.Offset.x * i, obj.Offset.y * (obj.CopiesY - j), 0) + if (i % 2) == 0: + pos = FreeCAD.Vector(obj.Offset.x * i, obj.Offset.y * j, 0) + else: + pos = FreeCAD.Vector(obj.Offset.x * i, obj.Offset.y * (obj.CopiesY - j), 0) + pos = self.calculateJitter(obj, pos) - pl.move(pos) - np = Path.Path([cm.transform(pl) - for cm in basepath.Commands]) - output += np.toGCode() + for b in base: + pl = FreeCAD.Placement() + # do not process the index 0,0. It will be processed by the base Paths themselves + if not (i == 0 and j == 0): + pl.move(pos) + np = Path.Path([cm.transform(pl) for cm in b.Path.Commands]) + output += np.toGCode() - else: - for i in range(obj.Copies): + else: + for i in range(obj.Copies): + for b in base: ang = 360 if obj.Copies > 0: ang = obj.Angle / obj.Copies * (1 + i) - - np = self.rotatePath(basepath, ang, obj.Centre) + np = self.rotatePath(b.Path.Commands, ang, obj.Centre) output += np.toGCode() - # print output - path = Path.Path(output) - obj.Path = path + + # print output + path = Path.Path(output) + obj.Path = path class ViewProviderArray: @@ -237,7 +291,7 @@ class CommandPathArray: def GetResources(self): return {'Pixmap': 'Path_Array', 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Array", "Array"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Array", "Creates an array from a selected path")} + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Array", "Creates an array from selected path(s)")} def IsActive(self): if bool(FreeCADGui.Selection.getSelection()) is False: @@ -252,26 +306,26 @@ class CommandPathArray: # check that the selection contains exactly what we want selection = FreeCADGui.Selection.getSelection() - if len(selection) != 1: - FreeCAD.Console.PrintError( - translate("Path_Array", "Please select exactly one path object")+"\n") - return - if not(selection[0].isDerivedFrom("Path::Feature")): - FreeCAD.Console.PrintError( - translate("Path_Array", "Please select exactly one path object")+"\n") - return + + for sel in selection: + if not(sel.isDerivedFrom("Path::Feature")): + FreeCAD.Console.PrintError( + translate("Path_Array", "Arrays can be created only from Path operations.")+"\n") + return # if everything is ok, execute and register the transaction in the # undo/redo stack FreeCAD.ActiveDocument.openTransaction("Create Array") FreeCADGui.addModule("PathScripts.PathArray") FreeCADGui.addModule("PathScripts.PathUtils") - FreeCADGui.doCommand( - 'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Array")') + + FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Array")') + FreeCADGui.doCommand('PathScripts.PathArray.ObjectArray(obj)') - FreeCADGui.doCommand( - 'obj.Base = (FreeCAD.ActiveDocument.' + selection[0].Name + ')') - # FreeCADGui.doCommand('PathScripts.PathArray.ViewProviderArray(obj.ViewObject)') + + baseString = "[%s]" % ','.join(["FreeCAD.ActiveDocument.%s" % sel.Name for sel in selection]) + FreeCADGui.doCommand('obj.Base = %s' % baseString) + FreeCADGui.doCommand('obj.ViewObject.Proxy = 0') FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') FreeCAD.ActiveDocument.commitTransaction()