From 11df51a085542f92fcc24c86aea50b6c81218a93 Mon Sep 17 00:00:00 2001 From: DeepSOIC Date: Fri, 13 Nov 2015 03:03:06 +0300 Subject: [PATCH] New tool: ProjectArray --- InitGui.py | 2 + latticeArrayFilter.py | 2 +- latticeGeomUtils.py | 92 ++++++++++++++++ latticeProjectArray.py | 245 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 latticeProjectArray.py diff --git a/InitGui.py b/InitGui.py index 956fdc8..345b9f8 100644 --- a/InitGui.py +++ b/InitGui.py @@ -62,6 +62,8 @@ class LatticeWorkbench (Workbench): cmdsArrayTools = cmdsArrayTools + mod.exportedCommands import latticeArrayFilter as mod cmdsArrayTools = cmdsArrayTools + mod.exportedCommands + import latticeProjectArray as mod + cmdsArrayTools = cmdsArrayTools + mod.exportedCommands import latticeApply as mod cmdsArrayTools = cmdsArrayTools + mod.exportedCommands diff --git a/latticeArrayFilter.py b/latticeArrayFilter.py index b125735..f6b1bd1 100644 --- a/latticeArrayFilter.py +++ b/latticeArrayFilter.py @@ -74,7 +74,7 @@ class LatticeArrayFilter(latticeBaseFeature.LatticeFeature): def derivedExecute(self,obj): #validity check if not latticeBaseFeature.isObjectLattice(obj.Base): - latticeExecuter.warning(obj,"A generic shape is expected, but a lattice object was supplied. It will be treated as a generic shape.") + latticeExecuter.warning(obj,"A lattice object is expected as Base, but a generic shape was provided. It will be treated as a lattice object; results may be unexpected.") output = [] #variable to receive the final list of placements leaves = LCE.AllLeaves(obj.Base.Shape) diff --git a/latticeGeomUtils.py b/latticeGeomUtils.py index f22620a..a9c7c18 100644 --- a/latticeGeomUtils.py +++ b/latticeGeomUtils.py @@ -39,6 +39,9 @@ def makeOrientationFromLocalAxes(ZAx, XAx = None): corrected and modified ''' + return makeOrientationFromLocalAxesUni("ZX",XAx= XAx, ZAx= ZAx) + + #dead old code that worked if XAx is None: XAx = App.Vector(0,0,1) #Why Z? Because I prefer local X axis to be aligned so that local XZ plane is parallel to global Z axis. #First, compute all three axes. @@ -67,3 +70,92 @@ def makeOrientationFromLocalAxes(ZAx, XAx = None): # and extract rotation from placement. ori = tmpplm.Rotation return ori + +def makeOrientationFromLocalAxesUni(priorityString, XAx = None, YAx = None, ZAx = None): + ''' + makeOrientationFromLocalAxesUni(priorityString, XAx = None, YAx = None, ZAx = None): + constructs App.Rotation to get into alignment with given local axes. + Priority string is a string like "ZXY", which defines how axes are made + perpendicular. For example, "ZXY" means that Z is followed strictly, X is + made to be perpendicular to Z, and Y is completely ignored (a new one will + be computed from X and Z). The strict axis must be specified, all other are + optional. + ''' + + if XAx is None: + XAx = App.Vector() + if YAx is None: + YAx = App.Vector() + if ZAx is None: + ZAx = App.Vector() + + axDic = {"X": XAx, "Y": YAx, "Z": ZAx} + + #expand priority string to list all axes + if len(priorityString) == 0: + priorityString = "ZXY" + if len(priorityString) == 1: + if priorityString == "X": + priorityString = priorityString + "Z" + elif priorityString == "Y": + priorityString = priorityString + "Z" + elif priorityString == "Z": + priorityString = priorityString + "X" + if len(priorityString) == 2: + for ch in "XYZ": + if not (ch in priorityString): + priorityString = priorityString + ch + break + + mainAx = axDic[priorityString[0]] #Driving axis + secAx = axDic[priorityString[1]] #Hint axis + thirdAx = axDic[priorityString[2]] #Ignored axis + #Note: since we need to change the actual XAx,YAx,ZAx while assigning to + # mainAx, secAx, thirdAx, we can't use '=' operator, because '=' reassigns + # the reference, and the variables lose linkage. For that purpose, + # _assignVector routine was introuced. It assigns the coordinates of the + # vector, without replacing referenes + + #force the axes be perpendicular + mainAx.normalize() + tmpAx = mainAx.cross(secAx) + if tmpAx.Length < ParaConfusion*10.0: + #failed, try some other secondary axis + #FIXME: consider thirdAx, maybe?? + _assignVector( secAx, { "X":App.Vector(0,0,1), + "Y":App.Vector(0,0,1), #FIXME: revise + "Z":App.Vector(0,0,1) + }[priorityString[1]]) + tmpAx = mainAx.cross(secAx) + if tmpAx.Length < ParaConfusion*10.0: + #failed again. (mainAx is Z). try some other secondary axis. + # Z axis + _assignVector(secAx, {"X":App.Vector(1,0,0), + "Y":App.Vector(0,1,0), #FIXME: revise + "Z":App.Vector(1,0,0) + }[priorityString[1]]) + tmpAx = mainAx.cross(secAx) + assert(tmpAx.Length > ParaConfusion*10.0) + tmpAx.normalize() + _assignVector(secAx, tmpAx.cross(mainAx)) + + #secAx was made perpendicular and valid, so we can compute the last axis. + # Here we need to take care to produce right handedness. + _assignVector(thirdAx, tmpAx) + if XAx.cross(YAx).dot(ZAx) < 0.0: + _assignVector(thirdAx, tmpAx * (-1.0)) + + #hacky way of constucting rotation to a local coordinate system: + # make matrix, + m = App.Matrix() + m.A = list(XAx)+[0.0]+list(YAx)+[0.0]+list(ZAx)+[0.0]+[0.0]*3+[1.0] + m.transpose() # local axes vectors are columns of matrix, but we put them in as rwos, because it is convenient, and then transpose it. + # make placement out of matrix, + tmpplm = App.Placement(m) + # and extract rotation from placement. + ori = tmpplm.Rotation + return ori + +def _assignVector(lhs, rhs): + '''A helper function for assigning vectors without creating new ones. Used as a hack to make aliases in OrientationFromLocalAxesUni''' + (lhs.x,lhs.y,lhs.z) = tuple(rhs) \ No newline at end of file diff --git a/latticeProjectArray.py b/latticeProjectArray.py new file mode 100644 index 0000000..0d026e5 --- /dev/null +++ b/latticeProjectArray.py @@ -0,0 +1,245 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2015 - 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 * +#* * +#*************************************************************************** + +import FreeCAD as App +import Part + +from latticeCommon import * +import latticeBaseFeature +import latticeCompoundExplorer as LCE +import latticeExecuter +import latticeGeomUtils as Utils + +__title__="Lattice ProjectArray module for FreeCAD" +__author__ = "DeepSOIC" +__url__ = "" + + +# -------------------------- common stuff -------------------------------------------------- + +def makeProjectArray(name): + '''makeProjectArray(name): makes a Lattice ProjectArray object.''' + return latticeBaseFeature.makeLatticeFeature(name, LatticeProjectArray, ViewProviderProjectArray) + +class LatticeProjectArray(latticeBaseFeature.LatticeFeature): + "The Lattice ProjectArray object" + + def derivedInit(self,obj): + self.Type = "LatticeProjectArray" + + obj.addProperty("App::PropertyLink","Base","Lattice ProjectArray","Array to be altered") + obj.addProperty("App::PropertyLink","Tool","Lattice ProjectArray","Shape to project onto") + + obj.addProperty("App::PropertyEnumeration","PositionMode","Lattice ProjectArray","") + obj.PositionMode = ['keep','projected','mixed'] + obj.PositionMode = 'projected' + + obj.addProperty("App::PropertyFloat","PosMixFraction","Lattice ProjectArray","Value controls mixing of positioning between the shape and the placement. 0 is on shape, 1 is at placement.") + + obj.addProperty("App::PropertyDistance","PosElevation","Lattice ProjectArray","Extra displacement along normal to face or along gap, away from the shape.") + + obj.addProperty("App::PropertyEnumeration","OrientMode","Lattice ProjectArray","") + obj.OrientMode = ['keep','along gap','tangent plane','along u', 'along v'] + obj.OrientMode = 'along u' + + obj.addProperty("App::PropertyEnumeration","Multisolution","Lattice ProjectArray","Specify the way of dealing with multiple solutions of projection") + obj.Multisolution = ['use first','use all'] + + + def derivedExecute(self,obj): + #validity check + if not latticeBaseFeature.isObjectLattice(obj.Base): + latticeExecuter.warning(obj,"A lattice object is expected as Base, but a generic shape was provided. It will be treated as a lattice object; results may be unexpected.") + + toolShape = obj.Tool.Shape + if latticeBaseFeature.isObjectLattice(obj.Tool): + latticeExecuter.warning(obj,"A lattice object was provided as Tool. It will be converted into points; orientations will be ignored.") + leaves = LCE.AllLeaves(toolShape) + points = [Part.Vertex(leaf.Placement.Base) for leaf in leaves] + toolShape = Part.makeCompound(points) + + leaves = LCE.AllLeaves(obj.Base.Shape) + input = [leaf.Placement for leaf in leaves] + + output = [] #variable to receive the final list of placements + + #cache settings + elev = float(obj.PosElevation) + posIsKeep = obj.PositionMode == 'keep' + posIsProjected = obj.PositionMode == 'projected' + posIsMixed = obj.PositionMode == 'mixed' + mixF = float(obj.PosMixFraction) + oriIsKeep = obj.OrientMode == 'keep' + oriIsAlongGap = obj.OrientMode == 'along gap' + oriIsTangentPlane = obj.OrientMode == 'tangent plane' + oriIsAlongU = obj.OrientMode == 'along u' + oriIsAlongV = obj.OrientMode == 'along v' + + isMultiSol = obj.Multisolution == 'use all' + + for plm in input: + v = Part.Vertex(plm.Base) + projection = v.distToShape(toolShape) + (dist, gaps, infos) = projection + for iSol in range(0,len(gaps)): + (posKeep, posPrj) = gaps[iSol] + (dummy, dummy, dummy, el_topo, el_index, el_params) = infos[iSol] + + # Fetch all possible parameters (some may not be required, depending on modes) + normal = posKeep - posPrj + if normal.Length < DistConfusion: + normal = None + + tangU = None + tangV = None + if el_topo == 'Face': + face = toolShape.Faces[el_index] + if normal is None: + normal = face.normalAt(*el_params) + (tangU, tangV) = face.tangentAt(*el_params) + elif el_topo == "Edge": + edge = toolShape.Edges[el_index] + tangU = edge.tangentAt(el_params) + + if normal is not None: + normal.normalize() + + #mode logic - compute new placement + if posIsKeep: + pos = plm.Base + elif posIsProjected: + pos = posPrj + elif posIsMixed: + pos = posKeep*mixF + posPrj*(1-mixF) + else: + raise ValueError("Positioning mode not implemented: " + obj.PositionMode ) + + if abs(elev) > DistConfusion: + if normal is None: + raise ValueError("Normal vector not available for a placement resting on " + el_topo +". Normal vector is required for nonzero position elevation.") + pos += normal * elev + + + if oriIsKeep: + ori = plm.Rotation + elif oriIsAlongGap: + if normal is None: + raise ValueError("Normal vector not available for a placement resting on " + el_topo +". Normal vector is required for orientation mode '"+obj.OrientMode+"'") + ori = Utils.makeOrientationFromLocalAxesUni("X",XAx= normal*(-1.0)) + elif oriIsTangentPlane: + if normal is None: + raise ValueError("Normal vector not available for a placement resting on " + el_topo +". Normal vector is required for orientation mode '"+obj.OrientMode+"'") + ori = Utils.makeOrientationFromLocalAxesUni("Z",ZAx= normal) + elif oriIsAlongU: + if normal is None: + raise ValueError("Normal vector not available for a placement resting on " + el_topo +". Normal vector is required for orientation mode '"+obj.OrientMode+"'") + if tangU is None: + raise ValueError("TangentU vector not available for point on " + el_topo +". TangentU vector is required for orientation mode '"+obj.OrientMode+"'") + ori = Utils.makeOrientationFromLocalAxesUni("ZX",ZAx= normal, XAx= tangU) + elif oriIsAlongV: + if normal is None: + raise ValueError("Normal vector not available for a placement resting on " + el_topo +". Normal vector is required for orientation mode '"+obj.OrientMode+"'") + if tangV is None: + raise ValueError("TangentV vector not available for point on " + el_topo +". TangentV vector is required for orientation mode '"+obj.OrientMode+"'") + ori = Utils.makeOrientationFromLocalAxesUni("ZX",ZAx= normal, XAx= tangV) + else: + raise ValueError("Orientation mode not implemented: " + obj.OrientMode ) + + output.append(App.Placement(pos,ori)) + + if not isMultiSol: + break + + return output + + +class ViewProviderProjectArray(latticeBaseFeature.ViewProviderLatticeFeature): + "A View Provider for the Lattice ProjectArray object" + + def getIcon(self): + return getIconPath("Lattice_ProjectArray.svg") + + def claimChildren(self): + return [self.Object.Base] + +def CreateLatticeProjectArray(name): + sel = FreeCADGui.Selection.getSelectionEx() + + # selection order independece logic (lattice object and generic shape stencil can be told apart) + iLtc = 0 #index of lattice object in selection + iStc = 1 #index of stencil object in selection + for i in range(0,len(sel)): + if latticeBaseFeature.isObjectLattice(sel[i]): + iLtc = i + iStc = i-1 #this may give negative index, but python accepts negative indexes + break + FreeCAD.ActiveDocument.openTransaction("Create ProjectArray") + FreeCADGui.addModule("latticeProjectArray") + FreeCADGui.addModule("latticeExecuter") + FreeCADGui.doCommand("sel = Gui.Selection.getSelectionEx()") + FreeCADGui.doCommand("f = latticeProjectArray.makeProjectArray(name = '"+name+"')") + FreeCADGui.doCommand("f.Base = App.ActiveDocument."+sel[iLtc].ObjectName) + FreeCADGui.doCommand("f.Tool = App.ActiveDocument."+sel[iStc].ObjectName) + + FreeCADGui.doCommand("for child in f.ViewObject.Proxy.claimChildren():\n"+ + " child.ViewObject.hide()") + FreeCADGui.doCommand("latticeExecuter.executeFeature(f)") + FreeCADGui.doCommand("f = None") + FreeCAD.ActiveDocument.commitTransaction() + + +# -------------------------- /common stuff -------------------------------------------------- + +# -------------------------- Gui command -------------------------------------------------- + +class _CommandProjectArray: + "Command to create Lattice ProjectArray feature" + + def GetResources(self): + return {'Pixmap' : getIconPath("Lattice_ProjectArray.svg"), + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Lattice_ProjectArray","Project Array"), + 'Accel': "", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Lattice_ProjectArray","Project Array: alter placements based on their proximity to a shape.")} + + def Activated(self): + sel = FreeCADGui.Selection.getSelectionEx() + if len(sel) == 2: + CreateLatticeProjectArray(name= "Project") + else: + mb = QtGui.QMessageBox() + mb.setIcon(mb.Icon.Warning) + mb.setText(translate("Lattice_ProjectArray", "Select one lattice object to be projected, and one shape to project onto, first!", None)) + mb.setWindowTitle(translate("Lattice_ProjectArray","Bad selection", None)) + mb.exec_() + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + +FreeCADGui.addCommand('Lattice_ProjectArray', _CommandProjectArray()) + +exportedCommands = ['Lattice_ProjectArray'] + +# -------------------------- /Gui command --------------------------------------------------