| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import FreeCAD |
| | import FreeCADGui |
| | import Path |
| | import Path.Op.Base as PathOp |
| | import PathScripts.PathUtils as PathUtils |
| | import Path.Base.Util as PathUtil |
| | from Path.Dressup.Utils import toolController |
| | from PySide import QtCore |
| |
|
| | import random |
| |
|
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| |
|
| | __doc__ = """CAM Array object and FreeCAD command""" |
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| |
|
| | class ObjectArray: |
| | def __init__(self, obj): |
| | obj.addProperty( |
| | "App::PropertyLinkList", |
| | "Base", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "The toolpaths to array"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyEnumeration", |
| | "Type", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Pattern method"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVectorDistance", |
| | "Offset", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The spacing between the array copies in linear pattern", |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyInteger", |
| | "CopiesX", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "The number of copies in X-direction in linear pattern" |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyInteger", |
| | "CopiesY", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "The number of copies in Y-direction in linear pattern" |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyAngle", |
| | "Angle", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Total angle in polar pattern"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyInteger", |
| | "Copies", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "The number of copies in linear 1D and polar pattern" |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVector", |
| | "Centre", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "The centre of rotation in polar pattern"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "SwapDirection", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Make copies in X direction before Y in Linear 2D pattern", |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyInteger", |
| | "JitterPercent", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Percent of copies to randomly offset"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVectorDistance", |
| | "JitterMagnitude", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Maximum random offset of copies"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyInteger", |
| | "JitterSeed", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Seed value for jitter randomness"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyLink", |
| | "ToolController", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The tool controller that will be used to calculate the toolpath", |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "Active", |
| | "Path", |
| | QT_TRANSLATE_NOOP("PathOp", "Make False, to prevent operation from generating code"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "CycleTime", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Operations cycle time estimation"), |
| | ) |
| |
|
| | obj.setEditorMode("CycleTime", 1) |
| | obj.Active = True |
| | obj.Type = ["Linear1D", "Linear2D", "Polar"] |
| |
|
| | self.setEditorModes(obj) |
| | obj.Proxy = self |
| |
|
| | def dumps(self): |
| | return None |
| |
|
| | def loads(self, state): |
| | return None |
| |
|
| | def setEditorModes(self, obj): |
| | if obj.Type == "Linear1D": |
| | angleMode = centreMode = copiesXMode = copiesYMode = swapDirectionMode = 2 |
| | copiesMode = offsetMode = 0 |
| | elif obj.Type == "Linear2D": |
| | angleMode = copiesMode = centreMode = 2 |
| | copiesXMode = copiesYMode = offsetMode = swapDirectionMode = 0 |
| | elif obj.Type == "Polar": |
| | angleMode = copiesMode = centreMode = 0 |
| | copiesXMode = copiesYMode = offsetMode = swapDirectionMode = 2 |
| |
|
| | obj.setEditorMode("Angle", angleMode) |
| | obj.setEditorMode("Copies", copiesMode) |
| | obj.setEditorMode("Centre", centreMode) |
| | obj.setEditorMode("CopiesX", copiesXMode) |
| | obj.setEditorMode("CopiesY", copiesYMode) |
| | obj.setEditorMode("Offset", offsetMode) |
| | obj.setEditorMode("SwapDirection", swapDirectionMode) |
| | obj.setEditorMode("JitterPercent", 0) |
| | obj.setEditorMode("JitterMagnitude", 0) |
| | obj.setEditorMode("JitterSeed", 0) |
| | obj.setEditorMode("ToolController", 2) |
| |
|
| | def onChanged(self, obj, prop): |
| | if prop == "Type": |
| | self.setEditorModes(obj) |
| |
|
| | if prop == "Active" and obj.ViewObject: |
| | obj.ViewObject.signalChangeIcon() |
| |
|
| | def onDocumentRestored(self, obj): |
| | """onDocumentRestored(obj) ... Called automatically when document is restored.""" |
| |
|
| | if not hasattr(obj, "Active"): |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "Active", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "PathOp", "Make False, to prevent operation from generating code" |
| | ), |
| | ) |
| | obj.Active = True |
| |
|
| | if not hasattr(obj, "JitterSeed"): |
| | obj.addProperty( |
| | "App::PropertyInteger", |
| | "JitterSeed", |
| | "Path", |
| | QtCore.QT_TRANSLATE_NOOP("App::Property", "Seed value for jitter randomness"), |
| | ) |
| | obj.JitterSeed = 0 |
| |
|
| | if not hasattr(obj, "CycleTime"): |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "CycleTime", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Operations cycle time estimation"), |
| | ) |
| | obj.CycleTime = self.getCycleTimeEstimate(obj) |
| |
|
| | self.setEditorModes(obj) |
| |
|
| | def execute(self, obj): |
| | |
| | if isinstance(obj.Base, list): |
| | base = obj.Base |
| | else: |
| | base = [obj.Base] |
| |
|
| | |
| | |
| | if not obj.Active or len(base) == 0 or not self.isBaseCompatible(obj): |
| | obj.Path = Path.Path() |
| | return |
| |
|
| | obj.ToolController = toolController(base[0]) |
| |
|
| | |
| | seed = obj.JitterSeed or obj.Name |
| |
|
| | pa = PathArray( |
| | obj.Base, |
| | obj.Type, |
| | obj.Copies, |
| | obj.Offset, |
| | obj.CopiesX, |
| | obj.CopiesY, |
| | obj.Angle, |
| | obj.Centre, |
| | obj.SwapDirection, |
| | obj.JitterMagnitude, |
| | obj.JitterPercent, |
| | seed, |
| | ) |
| |
|
| | obj.Path = pa.getPath() |
| | obj.CycleTime = PathOp.getCycleTimeEstimate(obj) |
| |
|
| | def isBaseCompatible(self, obj): |
| | if not obj.Base: |
| | return False |
| | tcs = [] |
| | cms = [] |
| | for sel in obj.Base: |
| | if not sel.isDerivedFrom("Path::Feature"): |
| | return False |
| | tcs.append(toolController(sel)) |
| | cms.append(PathUtil.coolantModeForOp(sel)) |
| |
|
| | if tcs == {None} or len(set(tcs)) > 1: |
| | Path.Log.warning( |
| | translate( |
| | "PathArray", |
| | "Arrays of toolpaths having different tool controllers or tool controller not selected.", |
| | ) |
| | ) |
| | return False |
| |
|
| | if set(cms) != {"None"}: |
| | Path.Log.warning( |
| | translate( |
| | "PathArray", |
| | "Arrays not compatible with coolant modes.", |
| | ) |
| | ) |
| | return False |
| |
|
| | return True |
| |
|
| |
|
| | class PathArray: |
| | """class PathArray ... |
| | This class receives one or more base operations and repeats those operations |
| | at set intervals based upon array type requested and the related settings for that type.""" |
| |
|
| | def __init__( |
| | self, |
| | base, |
| | arrayType, |
| | copies, |
| | offsetVector, |
| | copiesX, |
| | copiesY, |
| | angle, |
| | centre, |
| | swapDirection, |
| | jitterMagnitude, |
| | jitterPercent, |
| | seed, |
| | ): |
| | self.base = base |
| | self.arrayType = arrayType |
| | self.copies = copies |
| | self.offsetVector = offsetVector |
| | self.copiesX = copiesX |
| | self.copiesY = copiesY |
| | self.polarAngle = angle |
| | self.polarCentre = centre |
| | self.swapDirection = swapDirection |
| | self.jitterMagnitude = jitterMagnitude |
| | self.jitterPercent = jitterPercent |
| | self.seed = seed |
| |
|
| | def getPath(self): |
| | """getPath() ... Call this method on an instance of the class to generate and return |
| | path data for the requested path array.""" |
| |
|
| | commands = [] |
| | random.seed(self.seed) |
| |
|
| | if self.arrayType == "Polar": |
| | self.getPolarArray(commands) |
| | elif self.arrayType == "Linear2D": |
| | if self.swapDirection: |
| | self.getLinear2DXYArray(commands) |
| | else: |
| | self.getLinear2DYXArray(commands) |
| | else: |
| | self.getLinear1DArray(commands) |
| |
|
| | return Path.Path(commands) |
| |
|
| | def calculateJitter(self, pos): |
| | """Returns the position argument with a random vector shift applied.""" |
| | if self.jitterPercent == 0: |
| | pass |
| | elif random.randint(0, 100) < self.jitterPercent: |
| | pos.x = pos.x + random.uniform(-self.jitterMagnitude.x, self.jitterMagnitude.x) |
| | pos.y = pos.y + random.uniform(-self.jitterMagnitude.y, self.jitterMagnitude.y) |
| | pos.z = pos.z + random.uniform(-self.jitterMagnitude.z, self.jitterMagnitude.z) |
| | return pos |
| |
|
| | def getLinear1DArray(self, commands): |
| | """Array type Linear1D""" |
| | for i in range(self.copies): |
| | pos = FreeCAD.Vector( |
| | self.offsetVector.x * (i + 1), |
| | self.offsetVector.y * (i + 1), |
| | self.offsetVector.z * (i + 1), |
| | ) |
| | pos = self.calculateJitter(pos) |
| |
|
| | for b in self.base: |
| | pl = FreeCAD.Placement() |
| | pl.move(pos) |
| | path = PathUtils.getPathWithPlacement(b) |
| | path = PathUtils.applyPlacementToPath(pl, path) |
| | commands.extend(path.Commands) |
| |
|
| | def getLinear2DXYArray(self, commands): |
| | """Array type Linear2D with initial X direction""" |
| | for i in range(self.copiesY + 1): |
| | for j in range(self.copiesX + 1): |
| | if (i % 2) == 0: |
| | pos = FreeCAD.Vector( |
| | self.offsetVector.x * j, |
| | self.offsetVector.y * i, |
| | self.offsetVector.z * i, |
| | ) |
| | else: |
| | pos = FreeCAD.Vector( |
| | self.offsetVector.x * (self.copiesX - j), |
| | self.offsetVector.y * i, |
| | self.offsetVector.z * i, |
| | ) |
| | pos = self.calculateJitter(pos) |
| |
|
| | for b in self.base: |
| | pl = FreeCAD.Placement() |
| | |
| | if i != 0 or j != 0: |
| | pl.move(pos) |
| | path = PathUtils.getPathWithPlacement(b) |
| | path = PathUtils.applyPlacementToPath(pl, path) |
| | commands.extend(path.Commands) |
| |
|
| | def getLinear2DYXArray(self, commands): |
| | """Array type Linear2D with initial Y direction""" |
| | for i in range(self.copiesX + 1): |
| | for j in range(self.copiesY + 1): |
| | if (i % 2) == 0: |
| | pos = FreeCAD.Vector( |
| | self.offsetVector.x * i, |
| | self.offsetVector.y * j, |
| | self.offsetVector.z * i, |
| | ) |
| | else: |
| | pos = FreeCAD.Vector( |
| | self.offsetVector.x * i, |
| | self.offsetVector.y * (self.copiesY - j), |
| | self.offsetVector.z * i, |
| | ) |
| | pos = self.calculateJitter(pos) |
| |
|
| | for b in self.base: |
| | pl = FreeCAD.Placement() |
| | |
| | if i != 0 or j != 0: |
| | pl.move(pos) |
| | path = PathUtils.getPathWithPlacement(b) |
| | path = PathUtils.applyPlacementToPath(pl, path) |
| | commands.extend(path.Commands) |
| |
|
| | def getPolarArray(self, commands): |
| | """Array type Polar""" |
| | for i in range(self.copies): |
| | ang = 360 |
| | if self.copies > 0: |
| | ang = self.polarAngle / self.copies * (1 + i) |
| |
|
| | |
| | pl = FreeCAD.Placement() |
| | pl.rotate(self.polarCentre, FreeCAD.Vector(0, 0, 1), ang) |
| |
|
| | for b in self.base: |
| | path = PathUtils.getPathWithPlacement(b) |
| | path = PathUtils.applyPlacementToPath(pl, path) |
| | commands.extend(path.Commands) |
| |
|
| |
|
| | class ViewProviderArray: |
| | def __init__(self, vobj): |
| | self.attach(vobj) |
| | vobj.Proxy = self |
| |
|
| | def attach(self, vobj): |
| | self.vobj = vobj |
| | self.obj = vobj.Object |
| |
|
| | def dumps(self): |
| | return None |
| |
|
| | def loads(self, state): |
| | return None |
| |
|
| | def onChanged(self, vobj, prop): |
| | return None |
| |
|
| | def claimChildren(self): |
| | return [] |
| |
|
| | def onDelete(self, vobj, args): |
| | return None |
| |
|
| | def getIcon(self): |
| | if self.obj.Active: |
| | return ":/icons/CAM_Array.svg" |
| | else: |
| | return ":/icons/CAM_OpActive.svg" |
| |
|
| |
|
| | class CommandPathArray: |
| | def GetResources(self): |
| | return { |
| | "Pixmap": "CAM_Array", |
| | "MenuText": QT_TRANSLATE_NOOP("CAM_Array", "Array"), |
| | "ToolTip": QT_TRANSLATE_NOOP("CAM_Array", "Creates an array from selected toolpaths"), |
| | } |
| |
|
| | def IsActive(self): |
| | selection = FreeCADGui.Selection.getSelection() |
| | if not selection: |
| | return False |
| | tcs = [] |
| | for sel in selection: |
| | if not sel.isDerivedFrom("Path::Feature"): |
| | return False |
| | tc = toolController(sel) |
| | if tc: |
| | |
| | tcs.append(tc) |
| | if len(set(tcs)) != 1: |
| | return False |
| | else: |
| | return False |
| | if PathUtil.coolantModeForOp(sel) != "None": |
| | |
| | return False |
| | return True |
| |
|
| | def Activated(self): |
| |
|
| | |
| | selection = FreeCADGui.Selection.getSelection() |
| |
|
| | for sel in selection: |
| | if not (sel.isDerivedFrom("Path::Feature")): |
| | FreeCAD.Console.PrintError( |
| | translate("CAM_Array", "Arrays can be created only from toolpath operations.") |
| | + "\n" |
| | ) |
| | return |
| |
|
| | |
| | |
| | FreeCAD.ActiveDocument.openTransaction("Create Array") |
| | FreeCADGui.addModule("Path.Op.Gui.Array") |
| | FreeCADGui.addModule("PathScripts.PathUtils") |
| |
|
| | FreeCADGui.doCommand( |
| | 'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Array")' |
| | ) |
| |
|
| | FreeCADGui.doCommand("Path.Op.Gui.Array.ObjectArray(obj)") |
| |
|
| | baseString = "[%s]" % ",".join( |
| | ["FreeCAD.ActiveDocument.%s" % sel.Name for sel in selection] |
| | ) |
| | FreeCADGui.doCommand("obj.Base = %s" % baseString) |
| |
|
| | FreeCADGui.doCommand( |
| | "obj.ViewObject.Proxy = Path.Op.Gui.Array.ViewProviderArray(obj.ViewObject)" |
| | ) |
| | FreeCADGui.doCommand("PathScripts.PathUtils.addToJob(obj)") |
| | FreeCAD.ActiveDocument.commitTransaction() |
| | FreeCAD.ActiveDocument.recompute() |
| |
|
| |
|
| | if FreeCAD.GuiUp: |
| | |
| | FreeCADGui.addCommand("CAM_Array", CommandPathArray()) |
| |
|