PolarArray: new implementation

Implemented as new module. Old implementation is still there for back-compatibility.
* attachable
* array on arc of circle
* rich selection of orientation modes
* properties to flip axes and to reverse array direction
* dropdown command with fit presets
This commit is contained in:
DeepSOIC 2018-07-11 15:56:45 +03:00
parent 5f16da6201
commit 65ae662d14
4 changed files with 357 additions and 2 deletions

View File

@ -57,7 +57,7 @@ class Lattice2Workbench (Workbench):
+ Lattice2.ArrayFeatures.Placement.exportedCommands
+ Lattice2.ArrayFeatures.AttachablePlacement.exportedCommands
+ Lattice2.ArrayFeatures.LinearArray.exportedCommands
+ Lattice2.ArrayFeatures.PolarArray.exportedCommands
+ Lattice2.ArrayFeatures.PolarArray2.exportedCommands
+ Lattice2.ArrayFeatures.ArrayFromShape.exportedCommands
+ Lattice2.ArrayFeatures.Invert.exportedCommands
+ Lattice2.ArrayFeatures.JoinArrays.exportedCommands

View File

@ -7,6 +7,7 @@ import lattice2JoinArrays as JoinArrays
import lattice2LinearArray as LinearArray
import lattice2Placement as Placement
import lattice2PolarArray as PolarArray
import lattice2PolarArray2 as PolarArray2
import lattice2PopulateChildren as PopulateChildren
import lattice2PopulateCopies as PopulateCopies
import lattice2ProjectArray as ProjectArray

339
lattice2PolarArray2.py Normal file
View File

@ -0,0 +1,339 @@
#***************************************************************************
#* *
#* Copyright (c) 2018 - Victor Titov (DeepSOIC) *
#* <vv.titov@gmail.com> *
#* *
#* 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.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
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 --------------------------------------------------

View File

@ -77,3 +77,18 @@ def getSelectionAsPropertyLinkSubList():
def getSelectionAsListOfLinkSub():
return [(obj, [sub,]) for (obj,sub) in getSelectionAsPropertyLinkSubList()]
def linkSubList_convertToOldStyle(references):
("input: [(obj1, (sub1, sub2)), (obj2, (sub1, sub2))]\n"
"output: [(obj1, sub1), (obj1, sub2), (obj2, sub1), (obj2, sub2)]")
result = []
for tup in references:
if type(tup[1]) is tuple or type(tup[1]) is list:
for subname in tup[1]:
result.append((tup[0], subname))
if len(tup[1]) == 0:
result.append((tup[0], ''))
elif isinstance(tup[1],basestring):
# old style references, no conversion required
result.append(tup)
return result