#*************************************************************************** #* * #* Copyright (c) 2018 - Victor Titov (DeepSOIC) * #* * #* * #* 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 * #* * #*************************************************************************** __title__="Polar array feature module for lattice workbench for FreeCAD" __author__ = "DeepSOIC" __url__ = "" import math turn = 2 * math.pi import FreeCAD as App import Part from lattice2Common import * import lattice2BaseFeature from lattice2BaseFeature import assureProperty import lattice2Executer import lattice2GeomUtils from lattice2ValueSeriesGenerator import ValueSeriesGenerator import lattice2Utils as Utils from lattice2Utils import linkSubList_convertToOldStyle import lattice2AttachablePlacement as APlm V = App.Vector def make(): '''make(): makes a PolarArray object.''' obj = lattice2BaseFeature.makeLatticeFeature('PolarArray', PolarArray, ViewProviderPolarArray, no_disable_attacher= True) return obj def fetchArc(obj, sub): """returns None, or tuple (arc_span, arc_radius)""" if len(sub) > 0: linkedShape = obj.Shape.getElement(sub) else: linkedShape = obj.Shape if linkedShape.ShapeType == 'Edge': crv = linkedShape.Curve if isinstance(crv, Part.Circle): return linkedShape.LastParameter - linkedShape.FirstParameter, crv.Radius class PolarArray(APlm.AttachableFeature): """The Lattice Polar Array of placements""" def derivedInit(self,selfobj): super(PolarArray, self).derivedInit(selfobj) selfobj.addProperty('App::PropertyLength','Radius',"Polar Array","Radius of the array (set to zero for just rotation).") selfobj.Radius = 3 selfobj.addProperty('App::PropertyEnumeration', 'UseArcRange', "Polar Array", "If attachment mode is concentric, supporting arc's range can be used as array's Span or Step.") selfobj.UseArcRange = ['ignore', 'as Span', 'as Step'] selfobj.addProperty('App::PropertyBool', 'UseArcRadius', "Polar Array", "If True, and attachment mode is concentric, supporting arc's radius is used as array radius.") selfobj.addProperty('App::PropertyEnumeration','OrientMode',"Polar Array","Orientation of placements. Zero - aligns with origin. Static - aligns with self placement.") selfobj.OrientMode = ['Zero', 'Static', 'Radial', 'Vortex', 'Centrifuge', 'Launchpad', 'Dominoes'] selfobj.OrientMode = 'Radial' selfobj.addProperty('App::PropertyBool', 'Reverse', "Polar Array", "Reverses array direction.") selfobj.addProperty('App::PropertyBool', 'FlipX', "Polar Array", "Reverses x axis of every placement.") selfobj.addProperty('App::PropertyBool', 'FlipZ', "Polar Array", "Reverses z axis of every placement.") self.assureGenerator(selfobj) selfobj.ValuesSource = 'Generator' selfobj.SpanStart = 0 selfobj.SpanEnd = 360 selfobj.EndInclusive = False selfobj.Step = 55 selfobj.Count = 7 def assureGenerator(self, selfobj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self,'generator'): return self.generator = ValueSeriesGenerator(selfobj) self.generator.addProperties(groupname= "Polar Array", groupname_gen= "Lattice Series Generator", valuesdoc= "List of angles, in degrees.", valuestype= 'App::PropertyFloat') self.updateReadonlyness(selfobj) def updateReadonlyness(self, selfobj): self.generator.updateReadonlyness() arc = self.fetchArc(selfobj) selfobj.setEditorMode('Radius', 1 if arc and selfobj.UseArcRadius else 0) self.generator.setPropertyWritable('SpanEnd', False if arc and selfobj.UseArcRange == 'as Span' else True) self.generator.setPropertyWritable('SpanStart', False if arc and selfobj.UseArcRange == 'as Span' else True) self.generator.setPropertyWritable('Step', False if arc and selfobj.UseArcRange == 'as Step' else True) def fetchArc(self, selfobj): """returns None, or tuple (arc_span, arc_radius)""" if selfobj.Support: lnkobj, sub = selfobj.Support[0] sub = sub[0] #resolve the link return fetchArc(lnkobj, sub) def derivedExecute(self,selfobj): self.assureGenerator(selfobj) self.updateReadonlyness(selfobj) selfobj.positionBySupport() # Apply links if selfobj.UseArcRange != 'ignore' or selfobj.UseArcRadius: range, radius = self.fetchArc(selfobj) if selfobj.UseArcRange == 'as Span': selfobj.SpanStart = 0.0 selfobj.SpanEnd = range/turn*360 elif selfobj.UseArcRange == 'as Step': selfobj.Step = range/turn*360 if selfobj.UseArcRadius: selfobj.Radius = radius self.generator.execute() # cache properties into variables radius = float(selfobj.Radius) values = [float(strv) for strv in selfobj.Values] irot = selfobj.Placement.inverse().Rotation # compute internam placement, one behind OrientMode property baseplm = App.Placement() is_zero = selfobj.OrientMode == 'Zero' is_static = selfobj.OrientMode == 'Static' if is_zero or is_static: pass elif selfobj.OrientMode == 'Radial': baseplm = App.Placement() elif selfobj.OrientMode == 'Vortex': baseplm = App.Placement(V(), App.Rotation( V( 0, 1, 0), V( ), V( 0, 0, 1) )) elif selfobj.OrientMode == 'Centrifuge': baseplm = App.Placement(V(), App.Rotation( V( 0, 1, 0), V( ), V(-1, 0, 0) )) elif selfobj.OrientMode == 'Launchpad': baseplm = App.Placement(V(), App.Rotation( V( 0, 0, 1), V( ), V( 1, 0, 0) )) elif selfobj.OrientMode == 'Dominoes': baseplm = App.Placement(V(), App.Rotation( V( 0, 0, 1), V( ), V( 0,-1, 0) )) else: raise NotImplementedError() flipX = selfobj.FlipX flipZ = selfobj.FlipZ flipY = flipX ^ flipZ flipplm = App.Placement(V(), App.Rotation( V( -1 if flipX else 1, 0, 0), V( 0, -1 if flipY else 1, 0), V( 0, 0, -1 if flipZ else 1) )) baseplm = baseplm.multiply(flipplm) # Make the array on_arc = self.isOnArc(selfobj) angleplus = -90.0 if on_arc else 0.0 mm = -1.0 if selfobj.Reverse else +1.0 output = [] # list of placements for ang in values: localrot = App.Rotation(App.Vector(0,0,1), ang * mm + angleplus) localtransl = localrot.multVec(App.Vector(radius,0,0)) localplm = App.Placement(localtransl, localrot) resultplm = localplm.multiply(baseplm) if is_zero: resultplm.Rotation = irot resultplm = resultplm.multiply(flipplm) elif is_static: resultplm.Rotation = App.Rotation() resultplm = resultplm.multiply(flipplm) output.append(resultplm) return output def isOnArc(self, selfobj): return selfobj.MapMode == 'Concentric' and len(linkSubList_convertToOldStyle(selfobj.Support)) == 1 def onChanged(self, selfobj, propname): super(PolarArray, self).onChanged(selfobj, propname) if 'Restore' in selfobj.State: return if propname == 'Reverse' and self.isOnArc(selfobj): if selfobj.Reverse == True and abs(selfobj.MapPathParameter - 0.0) < ParaConfusion: selfobj.MapPathParameter = 1.0 elif selfobj.Reverse == False and abs(selfobj.MapPathParameter - 1.0) < ParaConfusion: selfobj.MapPathParameter = 0.0 class ViewProviderPolarArray(APlm.ViewProviderAttachableFeature): def getIcon(self): return getIconPath('Lattice2_PolarArray.svg') # -------------------------- /document object -------------------------------------------------- # -------------------------- Gui command -------------------------------------------------- def CreatePolarArray(genmode = 'SpanN'): sublinks = Utils.getSelectionAsPropertyLinkSubList() FreeCAD.ActiveDocument.openTransaction("Create PolarArray") FreeCADGui.addModule('lattice2PolarArray2') FreeCADGui.addModule('lattice2Executer') FreeCADGui.addModule("lattice2Base.Autosize") FreeCADGui.doCommand('f = lattice2PolarArray2.make()') FreeCADGui.doCommand("f.GeneratorMode = {mode}".format(mode= repr(genmode))) attached = False if len(sublinks) == 1: lnk, sub = sublinks[0] arc = fetchArc(lnk, sub) if arc: arcspan, radius = arc fullcircle = abs(arcspan - turn) < ParaConfusion endinclusive = not fullcircle usearcrange = 'as Step' if genmode == 'StepN' and not fullcircle else 'as Span' FreeCADGui.doCommand( 'f.Support = [(App.ActiveDocument.{lnk}, {sub})]\n' 'f.MapMode = \'Concentric\'\n' 'f.UseArcRange = {usearcrange}\n' 'f.EndInclusive = {endinclusive}\n' 'f.UseArcRadius = True' .format(lnk= lnk.Name, sub= repr(sub), usearcrange= repr(usearcrange), endinclusive= repr(endinclusive)) ) attached = True if not attached: FreeCADGui.doCommand("f.Placement.Base = lattice2Base.Autosize.convenientPosition()") FreeCADGui.doCommand("f.Radius = lattice2Base.Autosize.convenientModelSize()/2") FreeCADGui.doCommand('lattice2Executer.executeFeature(f)') if len(sublinks) > 0 and not attached: FreeCADGui.addModule('lattice2AttachablePlacement') FreeCADGui.doCommand('lattice2AttachablePlacement.editNewAttachment(f)') #commitTransaction will be called by attachment editor else: FreeCADGui.doCommand( 'Gui.Selection.clearSelection()\n' 'Gui.Selection.addSelection(f)' ) FreeCAD.ActiveDocument.commitTransaction() class CommandPolarArray(object): """Command to create PolarArray feature""" def __init__(self, mode): self.mode = mode def GetResources(self): mode_tooltips = { 'SpanN': "fit N placements into angle Span", 'StepN': "make N placements spaced by Step", 'SpanStep': "fill Span with placements spaced by Step", 'Random': "put N placements into Span randomly", } return {'Pixmap' : getIconPath('Lattice2_PolarArray_New.svg'), 'MenuText': "Polar array: {mode}".format(mode= ValueSeriesGenerator.mode_userfriendly_names[self.mode]), 'Accel': "", 'ToolTip': "New polar array. Make a polar array of placements - {mode_tooltip}.".format(mode_tooltip= mode_tooltips[self.mode])} def Activated(self): try: if len(FreeCADGui.Selection.getSelection()) < 2 : CreatePolarArray(self.mode) else: infoMessage("Lattice Polar Array", "Polar array command. Creates a polar array of placements.\n\n" "Preselection: you can select nothing -> an array on XY plane is created." " You can select a circle or arc of circle -> an array on that arc/circle is" " created, with array span controlled by arc range.\n\n" "You can later attach the array to anything. Right-click the array in model tree, click 'Attachment...'" ) except Exception as err: msgError(err) def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False _listOfSubCommands = [] for m in ValueSeriesGenerator.gen_modes: cmd_name = 'Lattice2_PolarArray2_'+m _listOfSubCommands.append(cmd_name) if FreeCAD.GuiUp: FreeCADGui.addCommand(cmd_name, CommandPolarArray(m)) class GroupCommandPolarArray: def GetCommands(self): global _listOfSubCommands return tuple(_listOfSubCommands) # a tuple of command names that you want to group def GetDefaultCommand(self): # return the index of the tuple of the default command. This method is optional and when not implemented '0' is used return 0 def GetResources(self): return { 'MenuText': 'Polar Array', 'ToolTip': 'Polar Array: circular array of placements.'} def IsActive(self): # optional return FreeCAD.ActiveDocument is not None if FreeCAD.GuiUp: FreeCADGui.addCommand('Lattice2_PolarArray2_GroupCommand',GroupCommandPolarArray()) import lattice2Compatibility as Compat if Compat.attach_extension_era: exportedCommands = ['Lattice2_PolarArray2_GroupCommand'] else: import lattice2PolarArray exportedCommands = lattice2PolarArray.exportedCommands # -------------------------- /Gui command --------------------------------------------------