diff --git a/CD_A2plusupdater.py b/CD_A2plusupdater.py new file mode 100644 index 00000000..b5bb237b --- /dev/null +++ b/CD_A2plusupdater.py @@ -0,0 +1,716 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2020 Dan Miel * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* 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 * +#* * +#*************************************************************************** +# This is to be used with A2plus Assembly WorkBench +# + +import sys +import os +import FreeCADGui +import FreeCAD +from PySide import QtUiTools +from PySide.QtGui import * +from PySide import QtGui, QtCore +import a2plib +import a2p_solversystem +from a2p_solversystem import solveConstraints +import CD_importpart +import CD_checkconstraints +import CD_ConstraintDiagnostics + + +class globaluseclass: + + def __init__(self, name): + self.roundto = 5 + self.partlabel = '' + self.partname = '' + self.notfoundfeatures = [] + self.foundfeatures = [] + self.dictOldNew = {} + self.alldicts = {} + self.clist = [] + self.partobj = None + self.test = [] + self.cylfaces = [] + self.notcylfaces = [] + self.partlog = [] + def resetvars(self): + self.partlabel = '' + self.partname = '' + self.notfoundfeatures = [] + self.foundfeatures = [] + self.dictOldNew = {} + self.alldicts = {} + #self.usedfeatures = [] + self.clist = [] + self.partobj = None + self.test = [] + self.cylfaces = [] + self.notcylfaces = [] + self.repaired = 0 +g = globaluseclass("g") + + +class sideFuncs1(): + def __init__(self): + pass + def opendoccheck(self): + doc = None + doc = FreeCAD.activeDocument() + + if doc is None: + msg = 'A file must be selected to start this selector\nPlease open a file and try again' + mApp(msg) + return('Nostart') + + return() +sideFuncs = sideFuncs1() + + + +class classFuncs(): + def __init__(self): + pass + + + def runinorder(self): + partname = 'fff' + partname = self.selectfiles() + if partname == 'No': + return + if partname == 'fff': + return + self.secondrun(partname,False) + + + def selectfiles(self): + ret =sideFuncs.opendoccheck() + if ret == 'Nostart': + return('No') + doc = FreeCAD.activeDocument() + partslist = FreeCADGui.Selection.getSelection() + if len(partslist) == 0: + mApp('1 No parts were selected to update.\nSelect one part and try again.') + return('No') + if len(partslist) > 1: + mApp('I have limited the number of parts that can be updaated to 1.\nSelect one part and try again.') + return('No') + CD_ConstraintDiagnostics.statusform.show() + CD_ConstraintDiagnostics.statusform.txtboxstatus.setText('Updating Assembly.') + CD_ConstraintDiagnostics.statusform.update() + + for num in range(0,len(partslist)): + partobj = partslist[num] + partname = self.firstrun(partobj) + self.secondrun(False) + + def firstrun(self,partobj): + g.resetvars() # reset Variables + partname = 'none' + g.partobj = partobj + g.partlabel =partobj.Label + partname = partobj.Name + g.partname = partobj.Name + g.shape1 = partobj.Shape + self.getfeatstomove() + FreeCADGui.updateGui() + return(partname) + + def secondrun(self, newpart = False): + doc = FreeCAD.activeDocument() + + + if newpart is False: + newobj = None + CD_importpart.updateImportedParts(doc, True) + newobj = g.partobj + FreeCADGui.updateGui() + g.shape2 = newobj.Shape + getfacelists() + self.runpostchange() + doc.recompute() + FreeCADGui.updateGui() + CD_ConstraintDiagnostics.statusform.Closeme() + + def runpostchange(self): + + doc = FreeCAD.activeDocument() + self.findfeats_attempt1() + doc.recompute() + FreeCADGui.updateGui() + doc = FreeCAD.activeDocument() + FreeCADGui.updateGui() + + clist = [] + for e in g.notfoundfeatures: + cobj = FreeCAD.ActiveDocument.getObject(e[0]) + clist.append(cobj) + if len(clist) != 0: + CD_ConstraintDiagnostics.form1.show() + CD_ConstraintDiagnostics.form1.loadtable(clist) + else: + mApp('Update complete. All surfaces found') + print('update complete') + + print('Repaired constraints = ' + (str(g.repaired - len(g.notfoundfeatures)))) + + def getfeatstomove(self): + doc = FreeCAD.activeDocument() + clist = selectforpart(g.partlabel) + g.clist = clist + featname = '' + di = {} + for cobj in clist: + # get feature info before update + partname = g.partname + featname = '' + subElement = "" + subElement = "" + subobj1 = doc.getObject(cobj.Object1) + subobj2 = doc.getObject(cobj.Object2) + frompart = [g.partlabel, g.partname] + for i in range(0,len(frompart)): + partname = frompart[i] + if subobj1.Label == partname: + subElement = "SubElement1" + featname = cobj.SubElement1 + if subobj2.Label == partname: + subElement = "SubElement2" + featname = cobj.SubElement2 + if featname != '': + break + dir = 'N' + if hasattr(cobj,'directionConstraint'): + dir = cobj.directionConstraint + # dict is basic info for constraint + # these next functions adds info for the subelements + if 'Face' in featname: + # add face info + facenum = int(featname[4:]) + di = self.getfacebynum(facenum-1,g.shape1) + if 'Edge' in featname: + # add edge info + num = int(featname[4:]) + num = num - 1 + di = self.getedgebynum(num,g.shape1) + if 'V' in featname: + # add Vertex info + num = int(featname[6:]) + num = num - 1 + di = self.getvertexbynum(num, g.shape1) + dict = {'Name':cobj.Name,'cname':cobj.Name,'featname':featname,'subElement':subElement,'dir':dir,'newname':''} + dict.update(di) + + g.alldicts[cobj.Name] = dict # Save the info to a larger dictionary + + + def getfacebynum (self,facenum,shape): + """Get face info.""" + face = shape.Faces[facenum] + area = rondnum(face.Area) + surftype = face.Surface + facepoints = [] + points = [] + center = -1 + + + edge1 = face.Edges[0] + + eeee = face.Edges + numofpoints = len(face.Vertexes) + for f0 in face.Vertexes: # Search the Vertexes of the face + point = FreeCAD.Vector(f0.Point.x,f0.Point.y,f0.Point.z) + x,y,z = point + loc = rondlist([x,y,z]) + facepoints.append(loc) + volume = rondnum(face.Volume) + radius = -1 + + surftype = face.Surface + surfstr = str(surftype) + + if'Cylinder' in surfstr: + surfstr = 'Cylinder' + radius = rondnum(surftype.Radius) + center = rondlist(face.Edges[0].CenterOfMass) + + if'Plane' in surfstr: + surfstr = 'Plane' + dict ={'surftype':surfstr,'area':area,'facepoints':facepoints,'center':center,'radius':radius,'edges':eeee} + + return(dict) + + + + def getedgebynum (self,num,shape): + + pnt1 = None + pnt2 = None + edge = shape.Edges[num] + length = edge.Length + length = rondnum(edge.Length) + center = edge.CenterOfMass + center = rondlist(center) + pnt1 = edge.Vertexes[0] # Basepoints + x1 = pnt1.Point.x + y1 = pnt1.Point.y + z1 = pnt1.Point.z + startpoint = rondlist([x1,y1,z1]) + a = FreeCAD.Vector(x1,y1,z1) + b = FreeCAD.Vector() + try: + pnt2 = edge.Vertexes[1] # Basepoints + x2 = pnt2.Point.x + y2 = pnt2.Point.y + z2 = pnt2.Point.z + endpoint = [x2,y2,z2] + b=FreeCAD.Vector(x2,y2,z2) + endpoint = rondlist([x2,y2,z2]) + except: + endpoint = ["-","-","-"] + + + radius = -1 + vector = None + curvetype = '' + center = -1 + tstr = str(edge.Curve) + if 'Line' in tstr: + curvetype ='line' + if 'Circle' in tstr: + curvetype ='circle' + radius = rondnum(edge.Curve.Radius) + center = rondlist(edge.CenterOfMass) + if 'Spline' in tstr: + curvetype ='spline' # A2 is not using these + dict = {'curvetype':curvetype,'obj':edge,'length':length,'startpoint':startpoint,'center':center,'endpoint':endpoint,'radius':radius,'vector':vector} + + return(dict) + + + def getvertexbynum(self,num,shape): + v=shape.Vertexes[num] + x=v.Point.x + y=v.Point.y + z=v.Point.z + xyz = [x,y,z] + + xyz= rondlist([x,y,z]) + return({'xyz':xyz}) + + ## post functions*********************************** + + def findfeats_attempt1(self): + """Try to find features after the update.""" + doc = FreeCAD.activeDocument() + for k,dict in g.alldicts.items(): + dict = dict + newfeat = '' + featname =dict.get('featname') + if featname in g.foundfeatures: + newfeat = g.dictOldNew.get(featname) + else: + if 'Face' in featname: + newfeat = self.findnewface_attempt1(dict) + if 'Edge' in featname: + newfeat = self.findnewedge_attempt1(dict) + if 'Vertex' in featname: + newfeat = self.findnewvertex_attempt1(dict) + if newfeat =='' or newfeat == 'No': + g.notfoundfeatures.append([dict.get('Name'),dict]) + pass + else: + if newfeat in g.foundfeatures == False: + g.foundfeatures.append(newfeat) # + g.dictOldNew[featname] = newfeat + + self.swapfeature(newfeat,dict) + doc.recompute() + + + if len(g.notfoundfeatures) > 0: + self.findfeats_attempt2() + + + def swapfeature(self,newfeat,dict): + """Add the new feature to the constraint.""" + cname = dict.get('cname') + g.partlog.append('Found ' + newfeat) + cobj = FreeCAD.ActiveDocument.getObject(cname) + mobj = FreeCAD.ActiveDocument.getObject(cname+'_mirror') + SubElement = dict.get('subElement') + if SubElement == 'SubElement1': + if cobj.SubElement1 != newfeat: + cobj.SubElement1 = newfeat + mobj.SubElement1 = newfeat + g.repaired = g.repaired + 1 + if SubElement == 'SubElement2': + if cobj.SubElement2 != newfeat: + cobj.SubElement2 = newfeat + mobj.SubElement2 = newfeat + g.repaired = g.repaired + 1 + dir = dict.get('dir') + if hasattr(cobj,'directionConstraint'): + cobj.directionConstraint = dir + if hasattr(mobj,'directionConstraint'): + mobj.directionConstraint = dir + return + + # If not found on first attempt try again + def findfeats_attempt2(self): + newfeat = '' + notfoundtemp = g.notfoundfeatures + + + g.notfoundfeatures = [] + for ea in notfoundtemp: + dict = ea[1] + featname = dict.get('featname') + if featname in g.foundfeatures: + newfeat = g.dictOldNew.get(featname) + else: + if 'Face' in featname: + newfeat = self.findnewface_attempt2(dict) + if 'Edge' in featname: + newfeat = self.findnewedge_attempt2(dict) + if newfeat is 'No' or newfeat == '': + g.notfoundfeatures.append([dict.get('Name'),dict]) + newfeat = 'None' + else: + if newfeat in g.foundfeatures == False: + g.foundfeatures.append(newfeat) + g.dictOldNew[featname] = newfeat + self.swapfeature(newfeat,dict) + + + def findnewface_attempt1(self,dict): + # First attempt to find a face. Perfect fit is area the same all points the same + face = '' + # newfeat = '' + # surftype = dict.get('surftype') + if dict.get('surftype') == 'Cylinder': + face = self.findCylinderattempt1(dict) + else: + for num in range(0,len(g.shape2.Faces)): + testdict = self.getfacebynum(num,g.shape2) + if testdict.get('surftype') != 'Cylinder': + if dict.get('area') == testdict.get('area')\ + and dict.get('facepoints') == testdict.get('facepoints'): + face = 'Face' + str(num +1) + break + return(face) + + + + def findnewface_attempt2(self,dict): + face = '' + # second attempt ignores area; looks for points + face = '' + if dict.get('surftype') == 'Cylinder': + face = self.findCylinderattempt2(dict) + + + else: + + for num in range(0,len(g.shape2.Faces)): + testdict = self.getfacebynum(num,g.shape2) + if dict.get('surftype') != 'Cylinder': + + points = dict.get('facepoints') + testpoints = testdict.get('facepoints') + + # points = points[1] + # testpoints =testpoints [1] + if len(points) < len(testpoints): + list1 = points + list2 = testpoints + else: + list1 = testpoints + list2 = points + match = 0 + for vert in list1: + if vert in list2: + match = match+1 + if match == len(list1): + face = 'Face' + str(num+1) + break + + if face == '': + + dedges = dict.get('edges') + edge = dedges[0] + pnt1 = edge.Vertexes[0] # Basepoints + x = pnt1.Point.x + y = pnt1.Point.y + z = pnt1.Point.z + dlist = [rondnum(edge.Length),x,y,z] + for num in range(0,len(g.shape2.Faces)): + + testdict = self.getfacebynum(num,g.shape2) + if dict.get('surftype') != 'Cylinder': + ed = testdict.get('edges') + ary = [] + for e in ed: + pnt1 = e.Vertexes[0] # Basepoints + x = pnt1.Point.x + y = pnt1.Point.y + z = pnt1.Point.z + tlist = [e.Length,x,y,z] + if dlist == tlist: + face = 'Face' + str(num+1) + break + return(face) + + + def findCylinderattempt1(self,dict): + face = '' + for num in g.cylfaces: + testdict = self.getfacebynum(num,g.shape2) + if dict.get('facepoints') == testdict.get('facepoints') and\ + dict.get('radius') == testdict.get('radius'): + face = 'Face' + str(num +1) + + break + return(face) + + + def findCylinderattempt2(self,dict): + #First attempt to find a face. Perfect fit is area the same all points the same + face = '' + ver1 = dict.get('center') + for num in g.cylfaces: + testdict = self.getfacebynum(num,g.shape2) + ver2 = testdict.get('center') + if ver1 == ver2: + face = 'Face' + str(num+1) + break + return(face) + + + def findnewedge_attempt1(self,dict): + edge = '' + if dict.get('curvetype') == 'circle': + center1 = dict.get('center') + for num in range(0,len(g.shape2.Edges)): + testdict = self.getedgebynum(num,g.shape2) + center2 = testdict.get('center') + if dict.get('radius') == testdict.get('radius')\ + and dict.get('center') == testdict.get('center'): + edge ='Edge' + str(num +1) + break + + + + + else: + for num in g.notcylfaces: + testdict = self.getfacebynum(num,g.shape2) + if dict.get('length') == testdict.get('length')\ + and dict.get('center') == testdict.get('center'): + edge ='Edge' + str(num +1) + break + + return(edge) + + + def findnewedge_attempt2(self,dict): + edge = '' + if dict.get('curvetype') == 'circle': + center1 = dict.get('center') + for num in range(0,len(g.shape2.Edges)): + + testdict = self.getedgebynum(num,g.shape2) + + center2 = testdict.get('center') + if center1 == center2: + edge ='Edge' + str(num +1) + break + + + for num in range(0,len(g.shape2.Edges)): + testdict = self.getedgebynum(num,g.shape2) + if dict.get('curvetype') == 'circle': + if testdict.get('curvetype') == 'circle': + + if dict.get('startpoint') == testdict.get('startpoint'): + edge ='Edge' + str(num +1) + break + + return(edge) + + + #pnts = g.shape2.Vertexes + #mypoint = dict.get('mypoint') + #ent = None + #for p in g.shape2.Vertexes: + # if p.Point == mypoint.Point: + # ent = p + # break + #mApp('found mypoint') + #allegdes = [] + #for e in g.shape2.edges: + # if e.Vertex[0].isSame(ent) or e.Vertex[1].isSame(ent): + # alledges.append(e) + # FreeCADGui.Selection.addSelection(e) + #return(edge) + + + + + def findnewvertex_attempt1(self,dict): + vertex = '' + featname = dict.get('featname') + for num in range(0,len(g.shape2.Vertexes)): + test = self.getvertexbynum(num,g.shape2) + if dict.get('xyz') == test.get('xyz'): + vertex = Vertex + str(num +1) + return(vertex) + +funcs = classFuncs() + +def getfacelists(): + g.cylfaces = [] + g.notcylfaces= [] + + for num in range(0,len(g.shape2.Faces)): + if str(g.shape2.Faces[num].Surface) == '': + g.cylfaces.append(num) + elif str(g.shape2.Faces[num].Surface) == '': + g.notcylfaces.append(num) + + + +def selectforpart(partlabel,selectType = 'std'): + #find the constraints for the part selected + doc = FreeCAD.activeDocument() + clist = [] + pnamelist =[] + pnamelist.append(partlabel) + for obj in FreeCAD.ActiveDocument.Objects: # Select constraints + if 'ConstraintInfo' in obj.Content: + if '_mirror' not in obj.Name: + subobj1 = doc.getObject(obj.Object1) + subobj2 = doc.getObject(obj.Object2) + part1name = subobj1.Label + part2name = subobj2.Label + + if selectType == 'betweenonly': + clist.append(obj) + else: + if part1name in pnamelist or part2name in pnamelist: + clist.append(obj) + return(clist) + + +def rondnum(num,rndto = g.roundto,mmorin='mm'): + # round a number to digits in global + # left in mm for accuracy. + rn = round(num,rndto) + if mmorin == 'in': + rn = rn/25.4 + + return(rn) + + +def rondlist(list,inch = False): + x = list[0] + y = list[1] + z = list[2] + x = rondnum(x) + y = rondnum(y) + z = rondnum(z) + if inch: + x = x/25.4 + y = y/25.4 + z = z/25.4 + + + return([x,y,z]) + + + +class mApp(QWidget): + # for error messages + def __init__(self,msg,msgtype = 'ok'): + super().__init__() + self.title = 'PyQt5 messagebox' + self.left = 600 + self.top = 100 + self.width = 320 + self.height = 200 + self.initUI(msg) + + def initUI(self,msg,msgtype = 'ok'): + self.setWindowTitle(self.title) + self.setGeometry(800, 300, 300, 400) + if msgtype == 'ok': + buttonReply = QMessageBox.question(self, 'PyQt5 message', msg, QMessageBox.Ok | QMessageBox.Ok) + if msgtype == 'yn': + buttonReply = QMessageBox.question(self, 'PyQt5 message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if buttonReply == QMessageBox.Yes: + return('y') + else: + return('n') + + + + + +toolTipText = \ +''' +Updates the A2plus.assembly when parts are modified. +To update the assembly, select the part that you have modified and press the icon. +When the update has finished run the A2plus solver to vereify if there are broken constraints. +This is an attempt to reduce the number of broken constraints caused +when modifying a part from FreeCAD’s A2plus assembly program. This records the +constraint’s mating surfaces immediately before the update and tries to +reconnect them after the update. +If this fails you can undo this update by using the undo button +and running the standard A2plus updater. +''' + +class rnp_Update_A2pParts: + + def Activated(self): + #funcs.runinorder() + funcs.selectfiles() + + def Deactivated(): + """This function is executed when the workbench is deactivated""" + FreeCADGui.Selection.clearSelection() + return + + + + def GetResources(self): + mypath = os.path.dirname(__file__) + return { + 'Pixmap' : mypath + "/icons/updateA2.svg", + 'MenuText': 'Updates parts from the A2plus program that has been modified', + 'ToolTip': 'Updates modifed parts.' + } + +FreeCADGui.addCommand('rnp_Update_A2pParts',rnp_Update_A2pParts()) +#============================================================================== + +#2020-08-06 Changed Lines 162 to 172 to open the viewer if the are missing features. + + diff --git a/CD_ConstraintDiagnostics.py b/CD_ConstraintDiagnostics.py new file mode 100644 index 00000000..c8f6184c --- /dev/null +++ b/CD_ConstraintDiagnostics.py @@ -0,0 +1,1368 @@ + +#*************************************************************************** +#* * +#* Copyright (c) 2020 Dan Miel * +#* * +#* 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 * +#* * +#*************************************************************************** +# This is to be used with A2plus Assembly WorkBench +# Tries to find constraints that are conflicting with each other. + +__version__ = 0.1 + +import sys +import os +import FreeCAD +import FreeCADGui +import math + + +from PySide import QtGui, QtCore +from PySide import QtUiTools +from PySide.QtGui import * +import a2plib +import CD_checkconstraints +import a2p_solversystem +import CD_featurelabels + + + +class globaluseclass: + def __init__(self,name): + self.checkingnum =0 + self.roundto = 4 + self.labelexist = False + self.movedconsts = [] + self.exname = '' + self.chgpart = '' + self.clist = [] + self.selfeat = [] +g = globaluseclass("g") + + +class mApp(QWidget): + + # for error messages + def __init__(self,msg,msgtype = 'ok'): + super().__init__() + self.title = 'PyQt5 messagebox - pythonspot.com' + self.left = 100 + self.top = 100 + self.width = 320 + self.height = 200 + self.initUI(msg) + + def initUI(self,msg,msgtype = 'ok'): + self.setWindowTitle(self.title) + self.setGeometry(self.left, self.top, self.width, self.height) + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + if msgtype == 'ok': + buttonReply = QMessageBox.question(self, 'PyQt5 message', msg, QMessageBox.Ok | QMessageBox.Ok) + if msgtype == 'yn': + buttonReply = QMessageBox.question(self, 'PyQt5 message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if buttonReply == QMessageBox.Yes: + pass + #print('Yes clicked.') + else: + pass + #print('No clicked.') + + self.show() + def clearvector(): + o.pointlist = [] + + + +class ShowPartProperties(QtGui.QWidget): + + def __init__(self): + QtGui.QWidget.__init__(self) + self.drt() + self.oldcell = '' + #self.listObjects = None + + def drt(self): + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.setGeometry(100, 50, 700, 280)#xy,wh + self.setWindowTitle("Constraint Viewer") + self.setStyleSheet("font: 11pt arial MS") + + formx=self.width() + formy = self.height()-30 + + + + layout = QtGui.QHBoxLayout() + bar = QtGui.QMenuBar(self) + + + file = bar.addMenu("Labels") + file.addAction("Open Dialog") + file.addAction("Delete labels") + file.triggered[QAction].connect(self.process_menus) + + tbinfo = bar.addMenu("Info") + tbinfo.addAction("Places of accuracy = " + str(g.roundto)) + tbinfo.triggered[QAction].connect(self.process_menus) + + file = bar.addMenu("Help") + file.addAction("Open Help") + file.triggered[QAction].connect(self.process_menus) + + file = bar.addMenu("Misc") + file.addAction("Change Part") + file.triggered[QAction].connect(self.process_menus) + + #self.chkboxrefreshonopen=QtGui.QCheckBox('refresh on open',self) + ##Saves to a text file whether to refresh the tree on open + #self.chkboxrefreshonopen.move(200, 5) + #self.chkboxrefreshonopen.setChecked(False) + #self.chkboxrefreshonopen.setFixedHeight(18) + #self.chkboxrefreshonopen.released.connect(lambda:self.saveRefresh()) + + + self.txtboxMainerror = QtGui.QLineEdit(self) + self.txtboxMainerror.move(250,60) + self.txtboxMainerror.setFixedHeight(80) + self.txtboxMainerror.setFixedWidth(250) + self.txtboxMainerror.setText('Status') + self.txtboxMainerror.hide() + self.txtboxMainerror.setStyleSheet("font: 20pt arial MS") + + #Main Table + self.tm = QtGui.QTableWidget(self) + self.tm.setGeometry(10, 120, 650, 50) # xy,wh + self.tm.setWindowTitle("Broken Mates") + self.tm.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) + self.tm.setRowCount(0) + self.tm.setColumnCount(10) + self.tm.setMouseTracking(True) + #self.tm.cellEntered.connect(self.onCellEntered) + self.tm.cellClicked.connect(self.cell_was_clicked) + self.tm.setHorizontalHeaderLabels(['Direction', 'Suppress', 'Run','Constraint name','Prt1 feat', 'Prt2 feat', 'F1', 'Part1', 'F2', 'Part2']) #, 'Part moved', 'Dist x', 'Dist y', 'Dist z']) + self.tm.horizontalHeader().sectionClicked.connect(self.fun) + for row in range(self.tm.rowCount()): + item = QtGui.QStandardItem(text) + model.setItem(row, column, item) + + + #ready for buttons + + + updatemsg=\ +''' +When a part is edited select it and press.\n +more that one can be selected but 1 is recommended. +''' + + self.btns=[] + btnLabels = [ + ['Transparent On','Toggles Transparency of parts'], + ['Find w label','Press to toggle a label for selected feature.'] + ] + self.createButtonColumn(5,btnLabels) + + + btnLabels = [ + + ['Import from part','Select a part and import \nall of the constraints for that part'], + ['Import from Tree','Copy selected constraints from the Tree'] + ] + self.createButtonColumn(140,btnLabels) + + + btnLabels = [ + ['Clear Table','Clear the table'], + ['Attach to','Select the feature to change in table.\nselect surface to change to.\n'] + ] + self.createButtonColumn(280,btnLabels) + + btnLabels = [ + ['Std Solver','Same as the solver above.\n'], + #['Solver No Chk','Does not check for errors while solving constraints.'] + ['No Option','Does not check for errors while solving constraints.'] + ] + self.createButtonColumn(420,btnLabels) + + + btnLabels = [ + ['Find in Tree','Finds the constraint in the tree\nfor the select row in table.'], + ['Clear Tree','Remove search color from tree.\n'] + ] + self.createButtonColumn(550,btnLabels) + + def createButtonColumn(self,xloc,btnLabels): + for row in range(0,len(btnLabels)): + btny = 30 +(26*row) + self.btn= QtGui.QPushButton( str(btnLabels[row][0]), self) + self.btn.move(xloc,btny) + self.btn.setFixedWidth(130) + self.btn.setFixedHeight(25) + self.btn.setToolTip(btnLabels[row][1]) + self.btn.released.connect(self.button_pushed) # pressed + self.btns.append(self.btn) + + def button_pushed(self): + index = self.btns.index(self.sender()) + buttext=self.btns[index].text() + if 'Trans' in buttext: + self.transparentOn(index) + if buttext == 'Import from part': + conflicts.selectforpart('std') + if buttext == 'Import from Tree': + conflicts.selectforTree() + + if 'Find in Tree' in buttext: + print('in') + searchterm =lastclc.cname + print('searchterm' + searchterm) + search.startsearch(searchterm,0) + + if 'Clear Tree' in buttext: + search.reset1() + if 'Clear Table' in buttext: + self.clearTable() + + + if buttext == 'Attach to': + # attaches leg to selected surface + sidefuncs.swapselectedleg() + + + if buttext == 'Std Solver': + self.stdSolve() + + if buttext == 'Solver No Chk': + mApp('Option not available') + print('Not working') + #conflicts.solveNOoerrorchecking() + cons = CD_checkconstraints.CheckConstraints.getallconstraints() + print(cons) + CD_checkconstraints.CheckConstraints.checkformovement(cons,False) + + if buttext == 'Find w label': + #createlabel for single part + if g.labelexist: + CD_featurelabels.labels.deletelabels() + g.labelexist = False + return + fname = lastclc.text + if lastclc.column == 4: + pname = self.tm.item(lastclc.row,7).text() + elif lastclc.column == 5: + pname = self.tm.item(lastclc.row,9).text() + else: + mApp('A part feature must be selected in the table') + return + + doc = FreeCAD.activeDocument() + sels =partobj = FreeCAD.ActiveDocument.getObjectsByLabel(pname) + for e in sels: + try: + partobj=e + except: + mApp('The table has lost focus. \nPlease reselect in the table.') + return + s= FreeCADGui.Selection.getSelectionEx()[0] + try: + ent = s.SubObjects[0] + except: + mApp('The selected text in the table is not a proper feature name.\n' + fname + ' ' + pname) + return + #featurelabels.labels.attachtotable(fname,ent) + CD_featurelabels.labels.labelForTable(ent,fname) + g.labelexist = True + if buttext == 'rotatepart': + move.startrotate() + + if buttext == 'Find Constraint': + + search.startsearch(lastclc.cname,0) + + def rotatepart(self): + rotate.tm.setRowCount(0) + + + def clearTable(self): + self.tm.setRowCount(0) + + + + def process_menus(self,q): + #process the menu according to their text + menutext = q.text() + if q.text() == "Open Dialog": + CD_featurelabels.form1.showme() + if q.text() == "Delete labels": + CD_featurelabels.labels.deletelabels() + if q.text() == "Change Part": + sidefuncs.replacepart1() + + + + def process_misc_menus(self,q): + menutext = q.text() + + if menutext == "Solve without error checking": + conflicts.solveNOerrorchecking() + + + + + + def stdSolve(self): + print('B') + doc = FreeCAD.activeDocument() + a2p_solversystem.solveConstraints(doc) + + + def fun4(self,Ncol): + self.tm = self.tm.sort_values(self.tm.headers[Ncol],ascending= Qt.AscendingOrder) + + + def fun(self,i): + # click in column header to sort column + self.tm.sortByColumn(i) + + def loadtable(self,listObjects): + # fill the table with infomation from a list of constraints + self.tm.setRowCount(0) + + doc = FreeCAD.activeDocument() + + row = 0 + rnum = 0 + for constraint in reversed(listObjects): + try: + cname = constraint.Name + mate = doc.getObject(cname) + except: + print('no name') + continue + ob1 = doc.getObject(mate.Object1) + if hasattr(ob1,'fixedPosition') ==False: + fixed1 = 'N' + else: + fixed1 = str(ob1.fixedPosition) + fixed1 = fixed1[0:1] + ob2 = doc.getObject(mate.Object2) + if hasattr(ob2,'fixedPosition') ==False: + fixed2 = 'N' + else: + ob2 = doc.getObject(mate.Object2) + fixed2 = str(ob2.fixedPosition) + fixed2 = fixed2[0:1] + + part1 = doc.getObject(mate.Object1) + part2 = doc.getObject(mate.Object2) + + try: + dirs = mate.directionConstraint + except: + dirs = 'None' + self.tm.insertRow(0) + + fn1 = mate.SubElement1 + fn2 = mate.SubElement2 + if len(fn1 ) == 0: + fn1 = 'None' + if len(fn2) == 0: + fn2 = 'None' + + dir =QtGui.QTableWidgetItem(str(dirs[0])) + rnum =QtGui.QTableWidgetItem(str('')) + sup = QtGui.QTableWidgetItem(str(mate.Suppressed)) + run = QtGui.QTableWidgetItem(str('Run')) # hhh + name = QtGui.QTableWidgetItem(cname) + fixed1 = QtGui.QTableWidgetItem(fixed1[0]) + Part1 = QtGui.QTableWidgetItem(part1.Label) + fname1 = QtGui.QTableWidgetItem(fn1) + fixed2 = QtGui.QTableWidgetItem(fixed2[0]) + Part2 = QtGui.QTableWidgetItem(part2.Label) + fname2 = QtGui.QTableWidgetItem(fn2) + + + self.tm.setItem(0, 0, dir) + self.tm.setItem(0, 1, sup) + self.tm.setItem(0, 2, run) + self.tm.setItem(0, 3, name) + self.tm.setItem(0, 4, fname1) + self.tm.setItem(0, 5, fname2) + self.tm.setItem(0, 6, fixed1) + self.tm.setItem(0, 7, Part1) + self.tm.setItem(0, 8, fixed2) + self.tm.setItem(0, 9, Part2) + + + + if self.tm.item(0, 4).text() == 'None': + self.tm.item(0, 4).setBackground(QBrush(QColor('yellow'))) + + if self.tm.item(0, 5).text() == 'None': + self.tm.item(0, 5).setBackground(QBrush(QColor('yellow'))) + row=row+1 + + if cname in CD_checkconstraints.g.allErrors: + if CD_checkconstraints.g.allErrors[cname].get('errortype') == 'Direction': + self.tm.item(0, 3).setBackground(QBrush(QColor('yellow'))) + # print(checkconstraints.g.allErrors[cname].get('errortype')) + #print(checkconstraints.g.allErrors) + header = self.tm.horizontalHeader() + header.setResizeMode(QtGui.QHeaderView.ResizeToContents) + self.current_hover = [0, 0] + self.hoveronoff(True) + + self.oldcell = self.tm.item(2, 1) + self.tm.current_hover = [0, 0] + for row in range(self.tm.rowCount()): + self.tm.setRowHeight(row, 15) + + + def onCellEntered(self, row, column): + return # i don't want the surfaces flickering + # cells highlight the constraint as mouse moved over + header = self.tm.horizontalHeaderItem(column).text() + item = self.tm.item(row, column) + txt = item.text() + citem = self.tm.item(row, 2) + cname = citem.text() + constraint = FreeCAD.ActiveDocument.getObject(cname) + FreeCADGui.Selection.clearSelection() + partobj1 = FreeCAD.ActiveDocument.getObject(constraint.Object1) + partobj2 = FreeCAD.ActiveDocument.getObject(constraint.Object2) + + + if header == 'Constraint name': + FreeCADGui.Selection.addSelection(partobj1,constraint.SubElement1) + FreeCADGui.Selection.addSelection(partobj2,constraint.SubElement2) + Print('Constraint name header found ') + if header == 'Prt1 feat': + FreeCADGui.Selection.addSelection(partobj1,constraint.SubElement1) + Print('Prt1 header found ') + if header == 'Prt2 feat': + FreeCADGui.Selection.addSelection(partobj2,constraint.SubElement2) + + if header == 'Part1 Label': + FreeCADGui.Selection.addSelection(partobj1) + + if header == 'Part2 Label': + FreeCADGui.Selection.addSelection(partobj2) + + if header == 'F1 Label': + pass + if header == 'F2 Label': + pass + + if str(self.oldcell) is 'None': + self.oldcell = self.tm.item(0, 0) + if column >-1 and column < form1.tm.columnCount()-1: + self.oldcell.setBackground(QBrush(QColor('white'))) + + item.setBackground(QBrush(QColor('yellow'))) + self.oldcell = item + + + def hoveronoff(self,bool): + self.tm.setMouseTracking(bool) + + def cell_was_clicked(self, row, column): + header = self.tm.horizontalHeaderItem(column).text() + item = self.tm.item(row, 3) + #g.lastclickeditem = [row,column,header] + lastclc.cellpicked(row,column) + cname = item.text() + try: + constraint = FreeCAD.ActiveDocument.getObject(cname) + partobj1 = FreeCAD.ActiveDocument.getObject(constraint.Object1) + partobj2 = FreeCAD.ActiveDocument.getObject(constraint.Object2) + except: + mApp('Constraint is not in file. Was it deleted?') + return + FreeCADGui.Selection.clearSelection() + if header == 'Run': + print(constraint.Name) + conflicts.checkformovement([constraint]) + FreeCADGui.Selection.addSelection(partobj1,constraint.SubElement1) + FreeCADGui.Selection.addSelection(partobj2,constraint.SubElement2) + + + if header == 'Constraint name': + FreeCADGui.Selection.addSelection(partobj1,constraint.SubElement1) + FreeCADGui.Selection.addSelection(partobj2,constraint.SubElement2) + if header == 'Prt1 feat': + FreeCADGui.Selection.addSelection(partobj1,constraint.SubElement1) + g.lastclickedFeat = FreeCADGui.Selection.getSelection() + if header == 'Prt2 feat': + FreeCADGui.Selection.addSelection(partobj2,constraint.SubElement2) + g.lastclickedFeat = FreeCADGui.Selection.getSelection() + FreeCADGui.Selection.setPreselection + if header == 'Part1': + FreeCADGui.Selection.addSelection(partobj1) + + if header == 'Part2': + FreeCADGui.Selection.addSelection(partobj2) + + + + if header == 'Suppress': + + if constraint.Suppressed == False: + constraint.Suppressed = True + else: + constraint.Suppressed = False + bool=constraint.Suppressed + boo=str(bool) + item2 = self.tm.item(row, column) + #item2.setText(boo[0]) + item2.setText(boo) + if header == 'Direction': + item2 = self.tm.item(row, column) + if item2.text()!= 'N': + dir=constraint.directionConstraint + if dir =='opposed': + newdir='aligned' + else: + newdir='opposed' + constraint.directionConstraint=newdir + dir=constraint.directionConstraint + item2 = self.tm.item(row, column) + item2.setText(dir[0]) + conflicts.checkformovement([constraint]) + + + + + def transparentOnold(self,index): + buttext =self.btns[index].text() + initialTransparencyState = a2plib.isTransparencyEnabled() + if 'On' in buttext: + doc = FreeCAD.ActiveDocument + initialTransparencyState = a2plib.isTransparencyEnabled() + if not initialTransparencyState: + a2plib.setTransparency() + self.btns[index].setText('Transparent Off') + if 'Off' in buttext: + self.btns[index].setText('Transparent On') + a2plib.restoreTransparency( ) + + def transparentOn(self,index): + buttext = self.btns[index].text() + tval = 0 + if 'On' in buttext: + tval = 80 + self.btns[index].setText('Transparent Off') + + if 'Off' in buttext: + tval = 0 + self.btns[index].setText('Transparent On') + for obj in FreeCAD.ActiveDocument.Objects: + if hasattr(obj,'ViewObject'): + if hasattr(obj.ViewObject,'Transparency'): + if obj.Name.startswith("Line") == False: + if obj.Name.startswith("Sketch") == False: + obj.ViewObject.Transparency = tval + + + + + + def showme(self): + #watch.onAcceptConstraint() + ret = self.checkforactiveview()# checks to see if a file is opened + if ret == 'Nostart': + return + + self.clearTable() + self.show() + lastclc.clear + + + def checkforactiveview(self): + # check that a file is open + try: + self.view =FreeCADGui.activeDocument().activeView() + except: + msg = 'A file must be opened to start this selector\nPlease open a file and try again' + mApp(msg) + return('Nostart') + return + + + def Closeme(self): + #close window and ensure that obsever is off + selObv.SelObserverOFF() + self.close() + + + + def closeEvent(self, event): + selObv.SelObserverOFF() + form1.Closeme() + self.close() + + + def resizeEvent(self, event): + #resize table + formx = self.width() + formy = self.height() + + self.tm.resize(formx -20,formy -120) + +form1=ShowPartProperties() + + + + +class classconflictreport(): + def __init__(self,name): + self.name = None + self.dictAllPlacements = {} + self.ConstraintsAll =[] + self.ConstraintsBad=[] + + self.Listofallparts = [] + self.worklist = [] + + def selectforTree(self,selectType = 'std'): + doc = FreeCAD.activeDocument() + clist = [] + sels = FreeCADGui.Selection.getSelectionEx() + if len(sels) == 0: + form1.clearTable() + mApp('Nothing was selected in the Tree.') + return + numofParts = len(sels) + for sel in sels: + cname = sel.Object.Name + cname = cname.replace('_mirror','') + cobj =doc.getObject(cname) + if 'ConstraintInfo' in cobj.Content: + clist.append(cobj) + if len(clist) == 0: + form1.clearTable() + mApp('There were no constraints selected in the Tree.\nSelect one or more constraints and try again.') + return + form1.loadtable(clist) + + + #select a part in the Gui and the attached constraints are sent to the form. + def selectforpart(self,selectType = 'std'): + pnamelist =[] + doc = FreeCAD.activeDocument() + clist = [] + #partlist = [] + sels = FreeCADGui.Selection.getSelectionEx() + if len(sels) ==0: + mApp('No parts weres selected in the window.') + return + numofParts = len(sels) + + if numofParts == 2 and selectType == 'betweenonly': + msg44 = 'Two or more parts needed for constraints to go between them' + form1.txtboxMainerror.setText(msg44) + mApp(msg44) + return + + + sels = FreeCADGui.Selection.getSelectionEx() + for sel in sels: + pnamelist.append(sel.Object.Label) + + for obj in FreeCAD.ActiveDocument.Objects: # Select constraints + if 'ConstraintInfo' in obj.Content: + if '_mirror' not in obj.Name: + subobj1 = doc.getObject(obj.Object1) + subobj2 = doc.getObject(obj.Object2) + part1name = subobj1.Label + part2name = subobj2.Label + + if selectType == 'betweenonly': + clist.append(obj) + else: + if part1name in pnamelist or part2name in pnamelist: + clist.append(obj) + if len(clist) == 0: + mApp('There are no constraints for this part.') + return + if selectType == 'list': + return(clist) + form1.loadtable(clist) + + + + def getallconstraints(self): + doc = FreeCAD.activeDocument() + constraints = [] + for obj in doc.Objects: + if 'ConstraintInfo' in obj.Content: + if not 'mirror' in obj.Name: + constraints.append(obj) + if len(constraints) == 0: + mApp('I can not find any contraints in this file.') + return(None) + return(constraints) + + + + def checkformovement(self,constraintlist,putPartBack = False,continuelist =False): + shakenpart =[] + movedobj = [] + g.movedconsts= [] + doc = FreeCAD.activeDocument() + failedcObjects= [] + failedpartnames = [] + partmoved = '' + partsmoved = [] + typemoved = '' + Bothpartsfixed=[] +# g.movedconsts =[] + if continuelist: + start = g.checkingnum + else: + start = 0 + for g.checkingnum in range(start,len(constraintlist)): + #for g.checkingnum in range(len(constraintlist),start,-1): + + cobj = constraintlist[g.checkingnum] + #corder.append(cobj.Name) + #norder.append(g.checkingnum) + subobj1 = cobj.getPropertyByName('Object1') + subobj2 = cobj.getPropertyByName('Object2') + part1 = doc.getObject(subobj1) # Save Position and fixed + part2 = doc.getObject(subobj2) + p1fix = part1.fixedPosition + p2fix = part2.fixedPosition + + + + if part1.fixedPosition and part2.fixedPosition: + Bothpartsfixed.append(cobj.Name) + elif part1.fixedPosition or part2.fixedPosition: + pass + elif part1.Label not in partsmoved and part2.Label not in partsmoved: + part1.fixedPosition = True + elif part1.Label not in partsmoved: + part1.fixedPosition = True + else: + part2.fixedPosition = True + + + #recording the location of part before move*** + preloc1 = part1.Placement.Base + preloc2 = part2.Placement.Base + + preBase1 = part1.Placement.Base # Round vectors to 6 places + preBase2 = part2.Placement.Base + preRot1 = part1.Placement.Rotation.Axis + preRot2 = part2.Placement.Rotation.Axis + preAngle1 = part1.Placement.Rotation.Angle + preAngle2 = part2.Placement.Rotation.Angle + + + + + preBasePt1 = part1.Placement.Base # Round vectors to 6 places + preBasePt2 = part2.Placement.Base + preRotPt1 = part1.Placement.Rotation.Axis + preRotPt2 = part2.Placement.Rotation.Axis + preAnglePt1 = part1.Placement.Rotation.Angle + preAnglePt2 = part2.Placement.Rotation.Angle + #xx + solved = self.solvelist([cobj]) # solve a single constraint + #doc.recompute() + #FreeCADGui.updateGui() + part1.fixedPosition = p1fix # reset parts fixed + part2.fixedPosition = p2fix + # Recording location after move + postBasePt1 = part1.Placement.Base # Round vectors to 6 places + postBasePt2 = part2.Placement.Base + postRotPt1 = part1.Placement.Rotation.Axis + postRotPt2 = part2.Placement.Rotation.Axis + postAnglePt1 = part1.Placement.Rotation.Angle + postAnglePt2 = part2.Placement.Rotation.Angle + + + disMoved = 0.0 + # Compares before and after the constraint is run + # Did part move and what kind of movment + #if self.isMoved(preBasePt1,postBasePt1,cobj,part1.Label): + if self.partMoved(preBasePt1.sub(postBasePt1),' ',cobj,part1.Label): + typemoved = 'xyz' + + if self.partMoved(preBasePt2.sub(postBasePt2),' ',cobj,part2.Label): + #failedpartnames.append(part2.Label) + typemoved = 'xyz' + + + + + if self.partMoved(preRotPt1.sub(postRotPt1),' ',cobj,part1.Label): + #failedpartnames.append(part1.Label) + typemoved = 'Rotate' + + if self.partMoved(preRotPt2.sub(postRotPt2),' ',cobj,part2.Label): + #failedpartnames.append(part2.Label) + typemoved = 'Rotate' + + + if self.partMoved(preAnglePt1,postAnglePt1,cobj,part1.Label): + typemoved = 'Angle' + + if self.partMoved(preAnglePt2,postAnglePt2,cobj,part2.Label): + typemoved = 'Angle' + + if putPartBack: + #Places part back in original location if put back is True + part1.Placement.Base = preBase1 + part1.Placement.Rotation.Axis = preRot1 + part1.Placement.Rotation.Angle = preAngle1 + part2.Placement.Base = preBase2 + part2.Placement.Rotation.Axis = preRot2 + part2.Placement.Rotation.Angle = preAngle2 + + + + #create list with cobj names + failedcObjects = g.movedconsts + + for e in failedcObjects: + failedpartnames.append(e.Name) + + + return([g.movedconsts,failedpartnames,Bothpartsfixed]) + + def partMoved(self,vec1,vec2,cobj,pname22): + dis = 'a' + if cobj in g.movedconsts: + return(False) + + if vec2 == ' ': + dis = vec1.Length + else: + dis = vec1 - vec2 + disMoved = rondnum(dis) + if disMoved != 0.0: + if cobj not in g.movedconsts: + g.movedconsts.append(cobj) + + #checks if part moved more than once + return(True) + return(False) + + + def solveNOoerrorchecking(self): + cons = self.getallconstraints() + self.checkformovement(cons,False) + + + def solvelist(self,list): + #add 1 at a time then solve allSolve + workList =[] + solved = 'no run' + doc = FreeCAD.activeDocument() + for c in list: + workList.append(c) + print(workList) + solved = a2p_solversystem.solveConstraints(doc,matelist = workList, showFailMessage=False) +conflicts = classconflictreport('conflicts') + +class classsidefunctions(): + def __init__(self,name): + self.name = name + self.sel1 = '' + self.featname1 = '' + + def swapselectedleg(self): + #starts observer to select a new enity + if lastclc.column < 4 or lastclc.column > 5: + mApp('Surfaces can only be replaced in columns/nPart1 feat or Part2 feat') + return + if len(FreeCADGui.Selection.getSelectionEx()) == 0 and lastclc.text is not 'None': + mApp('No feature has been selected') + return + + else: + sel =FreeCADGui.Selection.getSelectionEx()[0] + featname = sel.SubElementNames[0] + selObv.SelObserverON() + + + def trunoffobserv(self,objname, sub): + #Turns observer off and selects both features + + doc = FreeCAD.activeDocument() + selObv.SelObserverOFF() + if g.chgpart == 'part': + self.replacepart2() + else: + self.swap1leg() + + def swap1leg(self): + print('In swap one leg***') + feat2name = "" + if len(FreeCADGui.Selection.getSelectionEx()) == 0: + return + sel = FreeCADGui.Selection.getSelectionEx()[0] + if lastclc.text is 'None': + feat2name = sel.SubElementNames[0] + else: + feat2name = sel.SubElementNames[0] + cname = lastclc.cname + + FreeCADGui.Selection.clearSelection() + dict = {'cname':cname,'SubElement':lastclc.SubElement,'dir':lastclc.dir,'newfeat':feat2name} + self.swapfeature(dict) + cobj =FreeCAD.ActiveDocument.getObject(cname) + + + partobj1 = FreeCAD.ActiveDocument.getObject(cobj.Object1) + partobj2 = FreeCAD.ActiveDocument.getObject(cobj.Object2) + print('cobj.object1') + print(cobj.Object1) + print(cobj.Object2) + + if sel.Object.Name != partobj1.Name and sel.Object.Name != partobj2.Name: + mApp('The constraint can only be moved to another surface of the same part') + return + FreeCADGui.Selection.addSelection(partobj1,cobj.SubElement1) + FreeCADGui.Selection.addSelection(partobj2,cobj.SubElement2) + + item = form1.tm.item(lastclc.row, lastclc.column).setText(feat2name) + + def swapfeature(self,dict): + print('In swapfeatuer') + #changes a legs mating feature + newfeat = dict.get('newfeat') + leg = '' + mleg = '' + cname = dict.get('cname') + cobj = FreeCAD.ActiveDocument.getObject(cname) + mobj = FreeCAD.ActiveDocument.getObject(cname+'_mirror') + + SubElement = dict.get('SubElement') + + if SubElement == 'SubElement1': + leg = cobj.SubElement1 = newfeat + mleg = mobj.SubElement1 = newfeat + if SubElement == 'SubElement2': + leg = cobj.SubElement2 = newfeat + mleg = mobj.SubElement2 = newfeat + dir = dict.get('dir') + if hasattr(cobj,'directionConstraint'): + cobj.directionConstraint = dir + if hasattr(mobj,'directionConstraint'): + mobj.directionConstraint = dir + + return + + def replacepart1(self): + if len(FreeCADGui.Selection.getSelectionEx()) == 0: + mApp('No part has been selected') + return + g.clist = conflicts.selectforpart('list') + sel =FreeCADGui.Selection.getSelectionEx()[0] + g.exname = sel.Object + g.chgpart = 'part' + g.selfeat = [sel.Object,] #.append([sel.object,featname]) + selObv.SelObserverON() + + + #def trunoffpartobserv(self, obj): + # selObv.SelObserverOFF() + + + def replacepart2(self): + if object is None: + return + g.chgpart = '' + doc = FreeCAD.activeDocument() + + oldpart = g.exname + oldname = oldpart.Name + + sel = FreeCADGui.Selection.getSelectionEx()[0] + newpart = sel.Object + newname = newpart.Name + + base1 = oldpart.Placement.Base + rotate = oldpart.Placement.Rotation.Axis + angle = oldpart.Placement.Rotation.Angle + tempshape = oldpart.Shape + + tempshape = oldpart.Shape + tempbase = newpart.Placement.Base + tempRotation = newpart.Placement.Rotation.Axis + tempangle = newpart.Placement.Rotation.Angle + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(oldpart) + + oldpart.Shape = newpart.Shape + oldpart.Placement.Base = base1# = newpart.Placement.Base + oldpart.Placement.Rotation.Axis = rotate #= newpart.Placement.Rotation.Axis + oldpart.Placement.Rotation.Angle = angle + + g.chgpart == '' + newpart.Shape = tempshape + newpart.Placement.Base = tempbase + newpart.Placement.Rotation.Axis = tempRotation + newpart.Placement.Rotation.Angle = tempangle + + def ChangePart(self): + sels = FreeCADGui.Selection.getSelectionEx() + if len(sels) != 2: + mApp('You need to select 2 parts for this feature') + return + part1 = sels[0] + part2 = sels[1] + shape1 = part1.Shape + shape2 = part2.Shape + part1.shape = shape2 + part2.shape = shape1 + +sidefuncs = classsidefunctions('sidefuncs') + + +class SelObserver: + def __init__(self): + pass + + def SelObserverON(self): + o=FreeCADGui.Selection.addObserver(selObv) + def SelObserverOFF(self): + print('SelObserverOFF') + try: + FreeCADGui.Selection.removeObserver(selObv) + except: + print('removeObserver failed in C checker') + + def setPreselection(self,doc,obj,sub): # Preselection object + pass + def addSelection(self,doc,obj,sub,pnt): # Selection object + sidefuncs.trunoffobserv( obj, sub) + + + pass + def removeSelection(self,doc,obj,sub): # Delete the selected object + pass + def setSelection(self,doc): + #this is sent from menu + funcs.constraintselected('table') + +selObv =SelObserver() +#selObv.SelObserverON() +#selObv.SelObserverOFF() + + +class classsearch(): + def __init__(self): + self.founditems = [] + def startsearch(self,searchterm,colnum): + #col0 = Labcobj Label + #col2 = cobj.Name + #self.reset1() + mw = FreeCADGui.getMainWindow() + tab = mw.findChild(QtGui.QTabWidget, u'combiTab') + tree = tab.widget(0).findChildren(QtGui.QTreeWidget)[0] + top = tree.topLevelItem(0) + for idx in range(top.childCount()): + self.searchTreeItem(tree, top.child(idx), searchterm,colnum) + + def searchTreeItem(self, tree, item, searchterm,colnum): + for idx in range(item.childCount()): + itm = item.child(idx) + if searchterm in itm.text(colnum): + itm.setBackground(0, QtGui.QColor(255, 255, 0, 100)) + self.expandParent(tree, itm) + self.searchTreeItem(tree, item.child(idx),searchterm,colnum) + + def expandParent(self, tree, item): + parent = item.parent() + if parent: + tree.expandItem(parent) + self.expandParent(tree, parent) + + + def resetAll(self, item): + for idx in range(item.childCount()): + itm = item.child(idx) + self.founditems.append(itm) + itm.setBackground(0, QBrush()) + #itm.setBackground(QBrush(QColor('white'))) + self.resetAll(itm) + + + def reset1(self): + + mw = FreeCADGui.getMainWindow() + tab = mw.findChild(QtGui.QTabWidget, u'combiTab') + tree = tab.widget(0).findChildren(QtGui.QTreeWidget)[0] + top = tree.topLevelItem(0) + for idx in range(top.childCount()): + self.resetAll(top.child(idx)) + + def removecolor(self): + for e in self.founditems: + itm.setBackground(0, QBrush()) + #self.oldcell.setBackground(QBrush(QColor('white'))) +search = classsearch() + + + + +def rondlist(list,inch = False): + x = list[0] + y = list[1] + z = list[2] + x = rondnum(x) + y = rondnum(y) + z = rondnum(z) + if inch: + x = x/25.4 + y = y/25.4 + z = z/25.4 + + + return([x,y,z]) + + +def rondnum(num,rndto = g.roundto,mmorin='mm'): + #round a number to digits in global + #left in mm for accuracy. + rn = round(num,g.roundto) + if mmorin == 'in': + rn=rn/25.4 + + return(rn) + + + + + +class classlastclickeditem: + def __init__(self,name): + self.row = -1 + self.column = -1 + self.header = '' + self.cname = '' + self.cobj = None + self.dir = 'N' + self.text = '' + self.SubElement = '' + + def clear(self): + self.row = -1 + self.column = -1 + self.header = '' + self.cname = '' + self.cobj = None + self.dir= 'N' + + def cellpicked(self,row,column): + doc = FreeCAD.activeDocument() + item = form1.tm.item(row, column) + + self.item = item + self.row = row + self.column = column + self.text = item.text() + + + + self.header = form1.tm.horizontalHeaderItem(column).text() + citem = form1.tm.item(self.row, 3) + cname = citem.text() + self.cname = cname + + self.cobj = FreeCAD.ActiveDocument.getObject(self.cname) + if hasattr(self.cobj,'directionConstraint'): + self.dir = self.cobj.directionConstraint +# def attachto(self): + + if self.column == 4: + self.SubElement = 'SubElement1' + if self.column == 5: + self.SubElement = 'SubElement2' + + return(self.SubElement) +lastclc=classlastclickeditem("lastclc") + + +#This class looks for mouse clicks +class ViewObserver: + def __init__(self): + + self.view =None + self.o = None + self.c = None + def vostart(self): + try: + self.view =FreeCADGui.activeDocument().activeView() + #print(str(type(FreeCADGui.activeDocument().activeView()))) + #if 'None' in str(type(FreeCADGui.activeDocument().activeView())): + except: + msg = 'A file must be opened to start this selector\nPlease open a file and try again' + mApp(msg) + return('Nostart') + #self.view=FreeCADGui.activeDocument().activeView() + self.o=ViewObserver() + self.c = self.view.addEventCallback("SoMouseButtonEvent",self.o.logPosition) + def vooff(self): + print('vooff') + try: + self.view.removeEventCallback("SoMouseButtonEvent",self.c) + except Exception as e: + print(str(e)) + + def logPosition(self, myinfo): + down = (myinfo["State"] == "DOWN") + up = (myinfo["State"] == "UP") + pos = myinfo["Position"] + if up: + pass + if (down): + + if myinfo['Button']=='BUTTON1': + selcontrol + info.readTreeclick() + partinfo =FreeCADGui.activeDocument().activeView().getObjectInfo(pos) + if partinfo == None: + + info.clearsels() + + + else: + pass + + if up: + pass +viewob = ViewObserver() #(None) +#viewob = ViewObserver(FreeCADGui.activeDocument().activeView()) +#viewob =ViewObserver(FreeCADGui.activeDocument().activeView()) + +class formReport(QtGui.QDialog): + + def __init__(self,name): + self.name = name + row1 = 30 + row2 = 65 + + super(formReport,self).__init__() + self.setWindowTitle('Constraint Checker') + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.setGeometry(300,100,300,200)#xy,wh + self.setStyleSheet("font: 10pt arial MS") + #self.setmoldal(True) + + self.txtboxstatus = QtGui.QTextEdit(self) + self.txtboxstatus.move(5,30) + self.txtboxstatus.setFixedWidth(250) + self.txtboxstatus.setFixedHeight(60) + + self.lblviewlabel = QtGui.QLabel(self) + self.lblviewlabel.setText('Status"') + self.lblviewlabel.move(5,5) + self.lblviewlabel.setFixedWidth(250) + self.lblviewlabel.setFixedHeight(20) + self.lblviewlabel.setStyleSheet("font: 13pt arial MS") + + + + + #def resizeEvent(self, event): + # #resize table + # formx=self.width() + # formy = self.height() + # form1.tm.resize(formx -20,formy -200) + + def showme(self,msg): + print('show') + self.show() + #self.txtboxstatus.setText(msg) + #self.txtboxstatus.update() + #self.txtboxstatus.repaint() + + #return + + def Closeme(self): + self.close() + + def closeEvent(self, event): + self.close() + +statusform = formReport('statusform') + + +toolTipText = \ +''' +Select geometry to be constrained +within 3D View ! + +Suitable Constraint buttons will +get activated. + +Please also read tooltips of each +button. +''' + + +toolTipText = \ +''' +View constraints +''' + +class rnp_Constraint_Viewer: + + def Activated(self): + process.startprog() + + def onDeleteConstraint(self): + self.constraintValueBox.deleteLater() + a2plib.setConstraintEditorRef(None) + FreeCADGui.Selection.clearSelection() + + + def Deactivated(): + """This function is executed when the workbench is deactivated""" + #FreeCADGui.Selection.clearSelection() + + return + + + + def GetResources(self): + mypath = os.path.dirname(__file__) + return { + 'Pixmap' : mypath + "/icons/ConstraintDiagnostics.svg", + 'MenuText': 'Edit selected constraint', + 'ToolTip': toolTipText + } + +FreeCADGui.addCommand('rnp_Constraint_Viewer',rnp_Constraint_Viewer()) +#============================================================================== + + +class process: + def __init__(self,name): + self.name = name + + def startprog(): + # formg.write('startprog') + form1.showme() + +#form1.showme() + diff --git a/CD_checkconstraints.py b/CD_checkconstraints.py new file mode 100644 index 00000000..bc27672d --- /dev/null +++ b/CD_checkconstraints.py @@ -0,0 +1,505 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2020 Dan Miel * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* 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 * +#* * +#*************************************************************************** +# This is to be used with A2plus Assembly WorkBench +# Tries to find constraints that are conflicting with each other. + +__version__ = 0.1 + +import sys +import os +import FreeCAD +import FreeCADGui +from PySide import QtGui, QtCore +from PySide.QtGui import * +import a2p_solversystem +import a2p_constraintServices +import CD_ConstraintDiagnostics + +class globaluseclass: + def __init__(self,name): + self.checkingnum =0 + self.roundto = 6 + self.labelexist =False + self.movedconsts =[] + self.test =[] + self.allErrors = {} +g = globaluseclass("g") + + +class mApp(QWidget): + + # for error messages + def __init__(self,msg): + super().__init__() + self.title = 'PyQt5 messagebox - pythonspot.com' + self.left = 100 + self.top = 100 + self.width = 320 + self.height = 200 + self.initUI(msg) + + def initUI(self,msg,msgtype = 'ok'): + self.setWindowTitle(self.title) + self.setGeometry(self.left, self.top, self.width, self.height) + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + if msgtype == 'ok': + buttonReply = QMessageBox.question(self, 'PyQt5 message', msg, QMessageBox.Ok | QMessageBox.Ok) + if msgtype == 'yn': + buttonReply = QMessageBox.question(self, 'PyQt5 message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if buttonReply == QMessageBox.Yes: + pass + #print('Yes clicked.') + else: + pass + #print('No clicked.') + + self.show() + + def clearvector(): + o.pointlist = [] + +class formMain(QtGui.QMainWindow): + + def __init__(self,name): + self.name = name + super(formMain,self).__init__() + self.setWindowTitle('Constraint Checker') + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.setGeometry(300,100,600,140) # xy,wh + self.setStyleSheet("font: 11pt arial MS") + + self.txtboxReport = QtGui.QTextEdit(self) + self.txtboxReport.move(5,5) + self.txtboxReport.setFixedWidth(600) + self.txtboxReport.setFixedHeight(75) + + self.lblviewlabel = QtGui.QLabel(self) + self.lblviewlabel.setText('To view the constraints, press "Open Viewer"') + self.lblviewlabel.move(5,90) + self.lblviewlabel.setFixedWidth(250) + self.lblviewlabel.setFixedHeight(20) + self.lblviewlabel.setStyleSheet("font: 13pt arial MS") + + self.btnOpenViewer = QtGui.QPushButton(self) + self.btnOpenViewer.move(365,90) + self.btnOpenViewer.setFixedWidth(100) + self.btnOpenViewer.setFixedHeight(28) + self.btnOpenViewer.setIcon(QtGui.QIcon('./icons/ConstraintDiagnostics.svg')) + self.btnOpenViewer.setToolTip("View constraints the assembly.") + self.btnOpenViewer.setText("Open Viewer") + self.btnOpenViewer.clicked.connect(lambda:self.openViewer()) + + self.btnCloseForm = QtGui.QPushButton(self) + self.btnCloseForm.move(475,90) + self.btnCloseForm.setFixedWidth(100) + self.btnCloseForm.setFixedHeight(28) + self.btnCloseForm.setToolTip("Close this form.") + self.btnCloseForm.setText("Close") + self.btnCloseForm.clicked.connect(lambda:self.Closeme()) + + def openViewer(self): + clist = [] + doc = FreeCAD.activeDocument() + for (k,v) in g.allErrors.items(): + cobj = doc.getObject(k) + clist.append(cobj) + + CD_ConstraintDiagnostics.form1.show() + CD_ConstraintDiagnostics.form1.loadtable(clist) + + + def resizeEvent(self): + #resize table + formx = self.width() + formy = self.height() + self.txtboxReport.resize(formx -20, formy -120) + + def showme(self,msg): + self.txtboxReport.setText(msg) + self.show() + + + def Closeme(self): + self.close() + + def closeEvent(self, event): + form1.Closeme() + self.close() + +form1 = formMain('form1') + + +class classfilecheck(): + def __init__(self): + pass + def opendoccheck(self): + doc = None + doc = FreeCAD.activeDocument() + + if doc is None: + msg = 'A file must be selected to start this selector\nPlease open a file and try again' + mApp(msg) + return('Nostart') + + return() +filecheck = classfilecheck() + + +class classCheckConstraints(): + def __init__(self): + self.name = None + + self.dictAllPlacements = {} + self.ConstraintsAll = [] + self.ConstraintsBad = [] + + self.Listofallparts = [] + self.worklist = [] + self.test = [] + self.dir_errors = [] + self.rigids = [] + + + def startcheck(self,constraints= 'all'): + if filecheck.opendoccheck() == 'Nostart': + return + doc = FreeCAD.activeDocument() + #g.allErrors = {} + if constraints == 'all': + constraints = self.getallconstraints() + if len(constraints) == 0: + return + + #ConstraintDiagnostics.statusform.showme('messg') + CD_ConstraintDiagnostics.statusform.show() + CD_ConstraintDiagnostics.statusform.txtboxstatus.setText('Running Checker.') + + CD_ConstraintDiagnostics.statusform.update() + + + ss = a2p_solversystem.SolverSystem() + + ss.loadSystem(doc) + ss.assignParentship(doc) + rigids=ss.rigids + + for e in rigids: # get rigid parts + self.rigids.append(e.objectName) + + + + + self.dir_errors = a2p_constraintServices.redAdjustConstraintDirections(doc, constraints) + self.checkformovement(constraints, True) + if len(g.allErrors) != 0: + msg = '' + for e in g.allErrors: + line = str(g.allErrors.get(e)) + msg = msg + line + '\n' + form1.showme(msg) + else: + print('Zero errors') + CD_ConstraintDiagnostics.statusform.Closeme() + + + def checkformovement(self,constraintlist,putPartBack = True): + doc = FreeCAD.activeDocument() + partmoved = '' + partsmoved = [] + typemoved = '' + Bothpartsfixed = False + + for checkingnum in range(0,len(constraintlist)): + cobj = constraintlist[checkingnum] + + if cobj.Name in self.dir_errors: + errortype = '' + len1 = 0 + len2 = 0 + + if len(cobj.SubElement1) == 0: + errortype = 'Feat 1 missing' + if len(cobj.SubElement2) == 0: + errortype = 'Feat 2 missing' + if errortype == '': + errortype = 'Direction' + self.addError(cobj,errortype,'') + continue + subobj1 = cobj.getPropertyByName('Object1') + subobj2 = cobj.getPropertyByName('Object2') + part1 = doc.getObject(subobj1) # Save Position and fixed + part2 = doc.getObject(subobj2) + p1fix = False + p2fix = False + if hasattr(part1,"fixedPosition"): + p1fix =part1.fixedPosition + if hasattr(part2,"fixedPosition"): + p2fix = part2.fixedPosition + + if hasattr(part1,"fixedPosition") and hasattr(part2,"fixedPosition"): + if part1.fixedPosition and part2.fixedPosition: + Bothpartsfixed = True + self.addError(cobj,'Both fixed','') + elif part1.fixedPosition or part2.fixedPosition: + pass + if part1.Label not in partsmoved and part2.Label not in partsmoved: + part1.fixedPosition = True + elif part1.Label not in partsmoved: + part1.fixedPosition = True + else: + part2.fixedPosition = True + + + elif hasattr(part1,"fixedPosition") and hasattr(part2,"fixedPosition") == False: + if part1.Label not in partsmoved: + part1.fixedPosition =True + elif hasattr(part1,"fixedPosition") == False and hasattr(part2,"fixedPosition"): + if part2.Label not in partsmoved and part1.Label not in partsmoved: + part2.fixedPosition =True + + + #recording the location of part before move*** + preBase1 = part1.Placement.Base + preBase2 = part2.Placement.Base + preRot1 = part1.Placement.Rotation.Axis + preRot2 = part2.Placement.Rotation.Axis + preAngle1 = part1.Placement.Rotation.Angle + preAngle2 = part2.Placement.Rotation.Angle + + + + + preBasePt1 = part1.Placement.Base + preBasePt2 = part2.Placement.Base + preRotPt1 = part1.Placement.Rotation.Axis + preRotPt2 = part2.Placement.Rotation.Axis + preAnglePt1 = part1.Placement.Rotation.Angle + preAnglePt2 = part2.Placement.Rotation.Angle + #xx + + solved = self.solvelist([cobj]) # solve a single constraint + if hasattr(part1,"fixedPosition"): + part1.fixedPosition = p1fix #reset parts fixed + if hasattr(part2,"fixedPosition"): + part2.fixedPosition = p2fix + + + # Recording location after move + postBasePt1 = part1.Placement.Base # Round vectors to 6 places + postBasePt2 = part2.Placement.Base + postRotPt1 = part1.Placement.Rotation.Axis + postRotPt2 = part2.Placement.Rotation.Axis + postAnglePt1 = part1.Placement.Rotation.Angle + postAnglePt2 = part2.Placement.Rotation.Angle + localmove = False + + moved = self.partMoved(preBasePt1,postBasePt1,'xyz',cobj,part1.Label) + if moved: + localmove = True + pass + + moved = self.partMoved(preBasePt2,postBasePt2,'xyz',cobj,part2.Label) + if moved: + localmove = True + pass + + moved = self.partMoved(preRotPt1,postRotPt1,'Rotate',cobj,part1.Label) + if moved: + localmove = True + pass + + moved = self.partMoved(preRotPt2,postRotPt2,'Rotate',cobj,part2.Label) + if moved: + localmove = True + pass + + moved = self.partMoved(preAnglePt1, postAnglePt1,'Angle',cobj,part1.Label) + + if moved: + localmove = True + pass + moved = self.partMoved(preAnglePt2, postAnglePt2,'Angle',cobj,part2.Label) + if moved: + localmove = True + pass + partsmoved.append(part1.Label) + partsmoved.append(part2.Label) + + + if putPartBack: + #Places part back in origial location if put back is True + part1.Placement.Base = preBase1 + part1.Placement.Rotation.Axis = preRot1 + part1.Placement.Rotation.Angle = preAngle1 + part2.Placement.Base = preBase2 + part2.Placement.Rotation.Axis = preRot2 + part2.Placement.Rotation.Angle = preAngle2 + + + + def partMoved(self,vec1,vec2,movetype,cobj): + + if cobj.Name in g.allErrors.keys(): + return(False) + #dis = 'a' + errortype = '' + foundError = False + moved = '' + + if movetype == 'Angle': + dis1=rondnum(vec1) + dis2=rondnum(vec2) + if dis1 != dis2: + foundError = True + errortype = 'Conflict' + moved = movetype + + else: + + v1=FreeCAD.Vector(rondlist(vec1)) + v2=FreeCAD.Vector(rondlist(vec2)) + x,y,z = vec1 + v1 = [x,y,z] + v1=FreeCAD.Vector(rondlist(v1)) + + x,y,z = vec2 + v2 = [x,y,z] + v2=FreeCAD.Vector(rondlist(v2)) + if v1 != v2: + self.test.append(cobj.Name) + self.test.append([v1]) + self.test.append([v2]) + foundError = True + errortype = 'Conflict' + moved = movetype + if foundError: + self.addError(cobj,errortype,moved) + return(foundError) + def addError(self,cobj,errortype,movetype): + dict = {'Name':cobj.Name,'errortype':errortype,'movetype':movetype} + g.allErrors[cobj.Name] = dict + def getallconstraints(self): + doc = FreeCAD.activeDocument() + constraints = [] + for obj in doc.Objects: + if 'ConstraintInfo' in obj.Content: + if not 'mirror' in obj.Name: + constraints.append(obj) + + if len(constraints) == 0: + mApp('Cannot find any contraints in this file.') + return(None) + return(constraints) + + + def solveNOoerrorchecking(self): + cons = self.getallconstraints() + print(cons) + + self.checkformovement(cons,False) + + def solvelist(self,list): + # add 1 at a time then solve allSolve + workList =[] + solved = 'no run' + doc = FreeCAD.activeDocument() + for c in list: + print(c.Name) + workList.append(c) + solved = a2p_solversystem.solveConstraints(doc,None,False,matelist = workList, showFailMessage=False) + return(solved) +CheckConstraints = classCheckConstraints() + + + +def rondlist(list,inch = False): + x = list[0] + y = list[1] + z = list[2] + x=rondnum(x) + y=rondnum(y) + z=rondnum(z) + if inch: + x=x/25.4 + y=y/25.4 + z=z/25.4 + + + return([x,y,z]) + + +def rondnum(num,rndto = g.roundto,mmorin='mm'): + # round a number to digits in global + # left in mm for accuracy. + rn = round(num,g.roundto) + if mmorin == 'in': + rn=rn/25.4 + + return(rn) + + + + +toolTipText = \ +''' +Select geometry to be constrained +within 3D View ! + +Suitable Constraint buttons will +get activated. + +Please also read tooltips of each +button. +''' + + +toolTipText = \ +''' +check constraints. +''' + +class rnp_Constraint_Checkeralone: + + def Activated(self): + CheckConstraints.startcheck() + + def onDeleteConstraint(self): + pass + + def Deactivated(): + pass + #"""This function is executed when the workbench is deactivated""" + + + def GetResources(self): + mypath = os.path.dirname(__file__) + return { + 'Pixmap' : mypath + "/icons/ConflictCheckeralone.svg", + 'MenuText': 'Checks constraints', + 'ToolTip': 'Checks constraints' + } + +FreeCADGui.addCommand('rnp_Constraint_Checkeralone', rnp_Constraint_Checkeralone()) +#============================================================================== + + + + diff --git a/CD_featurelabels.py b/CD_featurelabels.py new file mode 100644 index 00000000..0185b331 --- /dev/null +++ b/CD_featurelabels.py @@ -0,0 +1,233 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2020 Dan Miel * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* 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 * +#* * +#*************************************************************************** +# This is to be used with A2plus Assembly WorkBench +# + +import sys +import os +from FreeCAD import Console +import FreeCADGui +import FreeCAD +from PySide import QtUiTools +from PySide.QtGui import * +from PySide import QtGui, QtCore + +import PySide + + +class formMain(QtGui.QMainWindow): + + def __init__(self,name): + self.name = name + super(formMain,self).__init__() + self.setWindowTitle('Create Labels') + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.setGeometry(300,100,200,300) # xy,wh + self.setStyleSheet("font: 11pt arial MS") + + self.btnLabels = [['Add Face Labels', 'Add labels to all of the faces on a selected part'], + ['Add Edge Labels', 'Add labels to all of the edges on a selected part'], + ['Add Vertex Labels', 'Add labels to all of the vertexes on a selected part'], + ['Delete Labels', 'Delete all labels'], + ['Attach to', 'Enter a name in the texbox and press to add a label'], + ['Selected Labels', 'Labels added to selected features'], + ['Close', ''] # + ] + + + + self.txtboxaddlabel = QtGui.QLineEdit(self) + self.txtboxaddlabel.move(10, 25) + self.txtboxaddlabel.setFixedHeight(25) + self.txtboxaddlabel.setFixedWidth(180) + self.txtboxaddlabel.setText('Feature name') + + + + self.btns=[] + BtnNum = 0 + for row in range(0,len(self.btnLabels)) : + btny = 70 +(28*row) + + self.btn= QtGui.QPushButton( str(self.btnLabels[row][0]), self) + self.btn.move(5,btny) + self.btn.setFixedWidth(190) + self.btn.setFixedHeight(25) + self.btn.setToolTip(self.btnLabels[row][1]) + self.btn.released.connect(self.button_pushed) # pressed + self.btns.append(self.btn) + BtnNum = BtnNum + 1 + + + def button_pushed(self): + index = self.btns.index(self.sender()) + btext=self.btns[index].text() + print(btext) + if btext == 'Add Face Labels': + labels.addlabels('Face') + if btext == 'Add Edge Labels': + labels.addlabels('Edge') + if btext == 'Add Vertex Labels': + labels.addlabels('Vertex') + + if btext == 'Delete Labels': + labels.deletelabels() + if btext == 'Attach to': + labels.attachto() + if btext == 'Close': + self.Closeme() + + if btext == 'Selected Labels': + labels.selectedlabels() + + + def hideMe(self): + Gui.Selection.clearSelection() + self.close() + + + def showme(self): + self.show() + + def Closeme(self): + self.close() + + def closeEvent(self, event): + form1.Closeme() + self.close() + +form1 = formMain('form1') + +class classLabels(): + def __init__(self): + self.labelGroup = None + pass + # self.name = name + + def checkselection(self): + """Checks to see if labels already exist.""" + doc = FreeCAD.activeDocument() + # loc = None + print('checking for label') + self.labelGroup = doc.getObject("partLabels") + if self.labelGroup is None: + self.labelGroup=doc.addObject("App::DocumentObjectGroup", "partLabels") + + if len(FreeCADGui.Selection.getSelection()) == 0: + # dlib.mApp('Please select One part.') + return(False) + return(True) + + + def addlabels(self,feat): + sel = self.checkselection() + if not sel: + return + sel = FreeCADGui.Selection.getSelection() # Select an object + if feat == 'Face': + features = sel[0].Shape.Faces + if feat == 'Edge': + features = sel[0].Shape.Edges + if feat == 'Vertex': + features = sel[0].Shape.Vertexes + + + for num in range(0,len(features)): + ent = features[num] + if feat == 'Vertex': + loc = ent.Point + else: + loc = ent.CenterOfMass + partLabel = self.makelabel(ent,feat+str(num+1),loc) + self.labelGroup.addObject(partLabel) + + + def makelabel(self,ent,name,loc): + partLabel = FreeCAD.ActiveDocument.addObject("App::AnnotationLabel","partLabel") + partLabel.LabelText = name + partLabel.BasePosition.x = loc[0] + partLabel.BasePosition.y = loc[1] + partLabel.BasePosition.z = loc[2] + partLabel.ViewObject.BackgroundColor = (1.0,1.0,0.0) + partLabel.ViewObject.TextColor = (0.0,0.0,0.0) + return(partLabel) + + + + def deletelabels(self): + for obj in FreeCAD.ActiveDocument.Objects: + if "partLabel" in obj.Label: + FreeCAD.ActiveDocument.removeObject(obj.Name) + + + def attachto(self,sel = None,featname = ''): + + sel = self.checkselection() + if not sel: + return + if featname == '': + featname = form1.txtboxaddlabel.text() + if sel is None: + sel = FreeCADGui.Selection.getSelection()[0] + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(sel,featname) + s= FreeCADGui.Selection.getSelectionEx()[0] + ent = s.SubObjects[0] + self.makelabel(ent,name,loc) + + + + + + def getEntLoc(self,ent,featname): + if 'V' in featname: + loc = ent.Point + else: + loc = ent.CenterOfMass + partLabel = self.makelabel(ent,featname,loc) + self.labelGroup.addObject(partLabel) + self.makelabel(ent,featname,loc) + + + def labelForTable(self,ent, featname): + """Create a label to find a part.""" + sel = self.checkselection() + self.getEntLoc(ent,featname) + + + + def selectedlabels(self): + sel = self.checkselection() + if not sel: + return + sels = FreeCADGui.Selection.getSelectionEx() # Select an object + for sel in sels: + featname= sel.SubElementNames[0] + ent = sel.SubObjects[0] + self.getentloc(ent,featname) + + + +labels=classLabels() + + +#form1.showme() + diff --git a/CD_importpart.py b/CD_importpart.py new file mode 100644 index 00000000..b8118f73 --- /dev/null +++ b/CD_importpart.py @@ -0,0 +1,1967 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 kbwbe * +#* * +#* Portions of code based on hamish's assembly 2 * +#* * +#* 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 FreeCADGui,FreeCAD +from PySide import QtGui, QtCore +import os +import os.path +import copy +import sys +import platform +import a2plib +from a2p_MuxAssembly import muxAssemblyWithTopoNames +from a2p_versionmanagement import A2P_VERSION +import a2p_solversystem +from a2plib import getRelativePathesEnabled +import a2p_importedPart_class + +from a2p_topomapper import ( + TopoMapper + ) + +import a2p_lcs_support +from a2p_importedPart_class import Proxy_importPart, ImportedPartViewProviderProxy +import a2p_constraintServices + +PYVERSION = sys.version_info[0] + +#============================================================================== +class DataContainer(): + def __init__(self): + self.tx = None +#============================================================================== +class ObjectCache: + ''' + An assembly could use multiple instances of then same importPart. + Cache them here so fileImports have to be executed only one time... + ''' + def __init__(self): + self.objects = {} # dict, key=fileName, val=object + + def cleanUp(self,doc): + for key in self.objects.keys(): + try: + doc.removeObject(self.objects[key].Name) #remove temporaryParts from doc + except: + pass + self.objects = {} # dict, key=fileName + + def add(self,fileName,obj): # pi_obj = PartInformation-Object + self.objects[fileName] = obj + + def get(self,fileName): + obj = self.objects.get(fileName,None) + if obj: + return obj + else: + return None + + def isCached(self,fileName): + if fileName in self.objects.keys(): + return True + else: + return False + + def len(self): + return len(self.objects.keys()) + +objectCache = ObjectCache() + +#============================================================================== +class a2p_shapeExtractDialog(QtGui.QDialog): + ''' + select a label from shape which has to be imported from a file + ''' + Deleted = QtCore.Signal() + Accepted = QtCore.Signal() + + + def __init__(self,parent,labelList = [], data = None): + super(a2p_shapeExtractDialog,self).__init__(parent=parent) + #super(a2p_shapeExtractDialog,self).__init__() + self.labelList = labelList + self.data = data + self.initUI() + + def initUI(self): + self.resize(400,100) + self.setWindowTitle('select a shape to be imported') + self.mainLayout = QtGui.QGridLayout() # a VBoxLayout for the whole form + + self.shapeCombo = QtGui.QComboBox(self) + + l = sorted(self.labelList) + self.shapeCombo.addItems(l) + + self.buttons = QtGui.QDialogButtonBox(self) + self.buttons.setOrientation(QtCore.Qt.Horizontal) + self.buttons.addButton("Cancel", QtGui.QDialogButtonBox.RejectRole) + self.buttons.addButton("Choose", QtGui.QDialogButtonBox.AcceptRole) + self.connect(self.buttons, QtCore.SIGNAL("accepted()"), self, QtCore.SLOT("accept()")) + self.connect(self.buttons, QtCore.SIGNAL("rejected()"), self, QtCore.SLOT("reject()")) + + self.mainLayout.addWidget(self.shapeCombo,0,0,1,1) + self.mainLayout.addWidget(self.buttons,1,0,1,1) + self.setLayout(self.mainLayout) + + def accept(self): + if self.data != None: + self.data.tx = self.shapeCombo.currentText() + self.deleteLater() + + def reject(self): + self.deleteLater() + +#============================================================================== +def importPartFromFile( + _doc, + filename, + extractSingleShape = False, # load only a single user defined shape from file + desiredShapeLabel=None, + importToCache=False, + cacheKey = "" + ): + doc = _doc + #------------------------------------------- + # Get the importDocument + #------------------------------------------- + + # look only for filenames, not paths, as there are problems on WIN10 (Address-translation??) + importDoc = None + importDocIsOpen = False + requestedFile = os.path.split(filename)[1] + for d in FreeCAD.listDocuments().values(): + recentFile = os.path.split(d.FileName)[1] + if requestedFile == recentFile: + importDoc = d # file is already open... + importDocIsOpen = True + break + + if not importDocIsOpen: + if filename.lower().endswith('.fcstd'): + importDoc = FreeCAD.openDocument(filename) + elif filename.lower().endswith('.stp') or filename.lower().endswith('.step'): + import ImportGui + fname = os.path.splitext(os.path.basename(filename))[0] + FreeCAD.newDocument(fname) + newname = FreeCAD.ActiveDocument.Name + FreeCAD.setActiveDocument(newname) + ImportGui.insert(filename,newname) + importDoc = FreeCAD.ActiveDocument + else: + msg = "A part can only be imported from a FreeCAD '*.FCStd' file" + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Value Error", msg ) + return + + #------------------------------------------- + # recalculate imported part if requested by preferences + # This can be useful if the imported part depends on an + # external master-spreadsheet + #------------------------------------------- + if a2plib.getRecalculateImportedParts(): + for ob in importDoc.Objects: + ob.recompute() + importDoc.save() # useless without saving... + + #------------------------------------------- + # Initialize the new TopoMapper + #------------------------------------------- + topoMapper = TopoMapper(importDoc) + + #------------------------------------------- + # Get a list of the importable Objects + #------------------------------------------- + importableObjects = topoMapper.getTopLevelObjects(allowSketches=True) + + if len(importableObjects) == 0: + msg = "No visible Part to import found. Aborting operation" + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Import Error", + msg + ) + return + + #------------------------------------------- + # if only one single shape of the importdoc is wanted.. + #------------------------------------------- + labelList = [] + dc = DataContainer() + + if extractSingleShape: + if desiredShapeLabel is None: # ask for a shape label + for io in importableObjects: + labelList.append(io.Label) + dialog = a2p_shapeExtractDialog( + QtGui.QApplication.activeWindow(), + labelList, + dc) + dialog.exec_() + if dc.tx is None: + msg = "Import of a shape reference aborted by user" + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Import Error", + msg + ) + return + else: # use existent shape label + dc.tx = desiredShapeLabel + + #------------------------------------------- + # Discover whether we are importing a subassembly or a single part + #------------------------------------------- + subAssemblyImport = False + if all([ 'importPart' in obj.Content for obj in importableObjects]) == 1: + subAssemblyImport = True + + #------------------------------------------- + # create new object + #------------------------------------------- + if importToCache: + partName = 'CachedObject_'+str(objectCache.len()) + newObj = doc.addObject("Part::FeaturePython",partName) + newObj.Label = partName + else: + partName = a2plib.findUnusedObjectName( importDoc.Label, document=doc ) + if extractSingleShape == False: + partLabel = a2plib.findUnusedObjectLabel( importDoc.Label, document=doc ) + else: + partLabel = a2plib.findUnusedObjectLabel( + importDoc.Label, + document=doc, + extension=dc.tx + ) + if PYVERSION < 3: + newObj = doc.addObject( "Part::FeaturePython", partName.encode('utf-8') ) + else: + newObj = doc.addObject( "Part::FeaturePython", str(partName.encode('utf-8')) ) # works on Python 3.6.5 + newObj.Label = partLabel + + Proxy_importPart(newObj) + if FreeCAD.GuiUp: + ImportedPartViewProviderProxy(newObj.ViewObject) + + newObj.a2p_Version = A2P_VERSION + assemblyPath = os.path.normpath(os.path.split(doc.FileName)[0]) + absPath = os.path.normpath(filename) + if getRelativePathesEnabled(): + if platform.system() == "Windows": + prefix = '.\\' + else: + prefix = './' + relativePath = prefix+os.path.relpath(absPath, assemblyPath) + newObj.sourceFile = relativePath + else: + newObj.sourceFile = absPath + + if dc.tx is not None: + newObj.sourcePart = dc.tx + + newObj.setEditorMode("timeLastImport",1) + newObj.timeLastImport = os.path.getmtime( filename ) + if a2plib.getForceFixedPosition(): + newObj.fixedPosition = True + else: + newObj.fixedPosition = not any([i.fixedPosition for i in doc.Objects if hasattr(i, 'fixedPosition') ]) + newObj.subassemblyImport = subAssemblyImport + newObj.setEditorMode("subassemblyImport",1) + + if subAssemblyImport: + if extractSingleShape: + newObj.muxInfo, newObj.Shape, newObj.ViewObject.DiffuseColor, newObj.ViewObject.Transparency = \ + muxAssemblyWithTopoNames(importDoc,desiredShapeLabel = dc.tx) + else: + newObj.muxInfo, newObj.Shape, newObj.ViewObject.DiffuseColor, newObj.ViewObject.Transparency = \ + muxAssemblyWithTopoNames(importDoc) + else: + # TopoMapper manages import of non A2p-Files. It generates the shapes and appropriate topo names... + if extractSingleShape: + newObj.muxInfo, newObj.Shape, newObj.ViewObject.DiffuseColor, newObj.ViewObject.Transparency = \ + topoMapper.createTopoNames(desiredShapeLabel = dc.tx) + else: + newObj.muxInfo, newObj.Shape, newObj.ViewObject.DiffuseColor, newObj.ViewObject.Transparency = \ + topoMapper.createTopoNames() + + newObj.objectType = 'a2pPart' + if extractSingleShape == True: + if a2plib.isA2pSketch(newObj): + newObj.objectType = 'a2pSketch' + newObj.setEditorMode("objectType",1) + + doc.recompute() + + if importToCache: # this import is used to update already imported parts + objectCache.add(cacheKey, newObj) + else: # this is a first time import of a part + if not a2plib.getPerFaceTransparency(): + # turn of perFaceTransparency by accessing ViewObject.Transparency and set to zero (non transparent) + newObj.ViewObject.Transparency = 1 + newObj.ViewObject.Transparency = 0 # import assembly first time as non transparent. + + + lcsList = a2p_lcs_support.getListOfLCS(doc,importDoc) + + + if not importDocIsOpen: + FreeCAD.closeDocument(importDoc.Name) + + if len(lcsList) > 0: + #========================================= + # create a group containing imported LCS's + lcsGroupObjectName = 'LCS_Collection' + lcsGroupLabel = 'LCS_Collection' + + if PYVERSION < 3: + lcsGroup = doc.addObject( "Part::FeaturePython", lcsGroupObjectName.encode('utf-8') ) + else: + lcsGroup = doc.addObject( "Part::FeaturePython", str(lcsGroupObjectName.encode('utf-8')) ) # works on Python 3.6.5 + lcsGroup.Label = lcsGroupLabel + + a2p_lcs_support.LCS_Group(lcsGroup) + a2p_lcs_support.VP_LCS_Group(lcsGroup.ViewObject) + + for lcs in lcsList: + lcsGroup.addObject(lcs) + + lcsGroup.Owner = newObj.Name + + newObj.addProperty("App::PropertyLinkList","lcsLink","importPart").lcsLink = lcsGroup + newObj.Label = newObj.Label # this is needed to trigger an update + lcsGroup.Label = lcsGroup.Label + + #========================================= + + return newObj + + +#============================================================================== +toolTip = \ +''' +Add a single shape out of an external file +to the assembly +''' + +class a2p_ImportShapeReferenceCommand(): + + def GetResources(self): + return {'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_ShapeReference.svg', + #'Accel' : "Shift+A", # a default shortcut (optional) + 'MenuText': "Add a single shape out of an external file", + 'ToolTip' : toolTip + } + + def Activated(self): + if FreeCAD.ActiveDocument is None: + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "No active Document found", + '''First create an empty file and\nsave it under desired name''' + ) + return + # + if FreeCAD.ActiveDocument.FileName == '': + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Unnamed document", + '''Before inserting first part,\nplease save the empty assembly\nto give it a name''' + ) + FreeCADGui.SendMsgToActiveView("Save") + return + + doc = FreeCAD.activeDocument() + guidoc = FreeCADGui.activeDocument() + view = guidoc.activeView() + + dialog = QtGui.QFileDialog( + QtGui.QApplication.activeWindow(), + "Select FreeCAD document to import part from" + ) + # set option "DontUseNativeDialog"=True, as native Filedialog shows + # misbehavior on Unbuntu 18.04 LTS. It works case sensitively, what is not wanted... + if a2plib.getNativeFileManagerUsage(): + dialog.setOption(QtGui.QFileDialog.DontUseNativeDialog, False) + else: + dialog.setOption(QtGui.QFileDialog.DontUseNativeDialog, True) + dialog.setNameFilter("Supported Formats (*.FCStd *.fcstd *.stp *.step);;All files (*.*)") + if dialog.exec_(): + if PYVERSION < 3: + filename = unicode(dialog.selectedFiles()[0]) + else: + filename = str(dialog.selectedFiles()[0]) + else: + return + + if not a2plib.checkFileIsInProjectFolder(filename): + msg = \ +''' +The part you try to import is +outside of your project-folder ! +Check your settings of A2plus preferences. +''' + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Import Error", + msg + ) + return + + #TODO: change for multi separate part import + importedObject = importPartFromFile(doc, filename, extractSingleShape=True) + + if not importedObject: + a2plib.Msg("imported Object is empty/none\n") + return + + mw = FreeCADGui.getMainWindow() + mdi = mw.findChild(QtGui.QMdiArea) + sub = mdi.activeSubWindow() + if sub != None: + sub.showMaximized() + + # WF: how will this work for multiple imported objects? + # only A2p AI's will have property "fixedPosition" + if importedObject and a2plib.isA2pSketch(importedObject): + importedObject.fixedPosition = True + if importedObject and not a2plib.isA2pSketch(importedObject) and not importedObject.fixedPosition: + PartMover( view, importedObject, deleteOnEscape = True ) + else: + self.timer = QtCore.QTimer() + QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.GuiViewFit) + self.timer.start( 200 ) #0.2 seconds + return + + def IsActive(self): + doc = FreeCAD.activeDocument() + if doc is None: return False + return True + + def GuiViewFit(self): + FreeCADGui.SendMsgToActiveView("ViewFit") + self.timer.stop() + + +FreeCADGui.addCommand('a2p_ImportShapeReferenceCommand',a2p_ImportShapeReferenceCommand()) + +#============================================================================== +toolTip = \ +''' +Restore transparency to +active document objects +''' + +class a2p_Restore_Transparency_Command(): + + def GetResources(self): + return {'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_Restore_Transparency.svg', + 'Accel' : "Shift+T", # a default shortcut (optional) + 'MenuText': "Restore transparency to active document objects", + 'ToolTip' : toolTip + } + + def Activated(self): + doc = FreeCAD.ActiveDocument + if doc is None: + FreeCAD.Console.Print("No active document found") + return + else: + for obj in doc.Objects: + if hasattr (obj, 'ViewObject'): + if hasattr (obj.ViewObject, 'Transparency'): + if obj.ViewObject.Transparency < 100: + transparency = obj.ViewObject.Transparency + obj.ViewObject.Transparency = transparency + 1 + obj.ViewObject.Transparency = transparency + return + + def IsActive(self): + doc = FreeCAD.activeDocument() + if doc is None: return False + return True + +FreeCADGui.addCommand('a2p_Restore_Transparency',a2p_Restore_Transparency_Command()) + +#============================================================================== +toolTip = \ +''' +Add a part from an external file +to the assembly +''' + +class a2p_ImportPartCommand(): + + def GetResources(self): + return {'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_ImportPart.svg', + 'Accel' : "Shift+A", # a default shortcut (optional) + 'MenuText': "Add a part from an external file", + 'ToolTip' : toolTip + } + + def Activated(self): + if FreeCAD.ActiveDocument is None: + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "No active Document found", + '''First create an empty file and\nsave it under desired name''' + ) + return + # + if FreeCAD.ActiveDocument.FileName == '': + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Unnamed document", + '''Before inserting first part,\nplease save the empty assembly\nto give it a name''' + ) + FreeCADGui.SendMsgToActiveView("Save") + return + + doc = FreeCAD.activeDocument() + guidoc = FreeCADGui.activeDocument() + view = guidoc.activeView() + + dialog = QtGui.QFileDialog( + QtGui.QApplication.activeWindow(), + "Select FreeCAD document to import part from" + ) + # set option "DontUseNativeDialog"=True, as native Filedialog shows + # misbehavior on Unbuntu 18.04 LTS. It works case sensitively, what is not wanted... + if a2plib.getNativeFileManagerUsage(): + dialog.setOption(QtGui.QFileDialog.DontUseNativeDialog, False) + else: + dialog.setOption(QtGui.QFileDialog.DontUseNativeDialog, True) + dialog.setNameFilter("Supported Formats (*.FCStd *.fcstd *.stp *.step);;All files (*.*)") + if dialog.exec_(): + if PYVERSION < 3: + filename = unicode(dialog.selectedFiles()[0]) + else: + filename = str(dialog.selectedFiles()[0]) + else: + return + + if not a2plib.checkFileIsInProjectFolder(filename): + msg = \ +''' +The part you try to import is +outside of your project-folder ! +Check your settings of A2plus preferences. +''' + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Import Error", + msg + ) + return + + #TODO: change for multi separate part import + importedObject = importPartFromFile(doc, filename) + + if not importedObject: + a2plib.Msg("imported Object is empty/none\n") + return + + mw = FreeCADGui.getMainWindow() + mdi = mw.findChild(QtGui.QMdiArea) + sub = mdi.activeSubWindow() + if sub != None: + sub.showMaximized() + +# WF: how will this work for multiple imported objects? +# only A2p AI's will have property "fixedPosition" + if importedObject and not importedObject.fixedPosition: + PartMover( view, importedObject, deleteOnEscape = True ) + else: + self.timer = QtCore.QTimer() + QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.GuiViewFit) + self.timer.start( 200 ) #0.2 seconds + return + + def IsActive(self): + doc = FreeCAD.activeDocument() + if doc is None: return False + return True + + def GuiViewFit(self): + FreeCADGui.SendMsgToActiveView("ViewFit") + self.timer.stop() + + +FreeCADGui.addCommand('a2p_ImportPart',a2p_ImportPartCommand()) +#============================================================================== + + + + +def updateImportedParts(doc, partial=False): #changed to true Dan + if doc is None: + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "No active document found!", + "Before updating parts, you have to open an assembly file." + ) + return + + doc.openTransaction("updateImportParts") + objectCache.cleanUp(doc) + + + selectedObjects=[] + selection = [s for s in FreeCADGui.Selection.getSelection() + if s.Document == FreeCAD.ActiveDocument and + (a2plib.isA2pPart(s) or a2plib.isA2pSketch()) + ] + if selection and len(selection)>0: + if partial==True: + response = QtGui.QMessageBox.Yes + else: + flags = QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.No + msg = u"Do you want to update only the selected parts?" + response = QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + u"ASSEMBLY UPDATE", + msg, + flags + ) + if response == QtGui.QMessageBox.Yes: + for s in selection: + selectedObjects.append(s) + + if len(selectedObjects) >0: + workingSet = selectedObjects + else: + workingSet = doc.Objects + + for obj in workingSet: + if hasattr(obj, 'sourceFile') and a2plib.to_str(obj.sourceFile) != a2plib.to_str('converted'): + + + #repair data structures (perhaps an old Assembly2 import was found) + if hasattr(obj,"Content") and 'importPart' in obj.Content: # be sure to have an assembly object + if obj.Proxy is None: + #print (u"Repair Proxy of: {}, Proxy: {}".format(obj.Label, obj.Proxy)) + Proxy_importPart(obj) + ImportedPartViewProviderProxy(obj.ViewObject) + + assemblyPath = os.path.normpath(os.path.split(doc.FileName)[0]) + absPath = a2plib.findSourceFileInProject(obj.sourceFile, assemblyPath) + + if absPath is None: + QtGui.QMessageBox.critical( QtGui.QApplication.activeWindow(), + u"Source file not found", + u"Unable to find {}".format( + obj.sourceFile + ) + ) + if absPath != None and os.path.exists( absPath ): + newPartCreationTime = os.path.getmtime( absPath ) + if ( + newPartCreationTime > obj.timeLastImport or + obj.a2p_Version != A2P_VERSION or + a2plib.getRecalculateImportedParts() # open always all parts as they could depend on spreadsheets + ): + cacheKeyExtension = obj.sourcePart + if cacheKeyExtension is None: + cacheKeyExtension = "AllShapes" + elif cacheKeyExtension == "": + cacheKeyExtension = "AllShapes" + cacheKeyExtension = '-' + cacheKeyExtension + cacheKey = absPath+cacheKeyExtension + + if not objectCache.isCached(cacheKey): # Load every changed object one time to cache + if obj.sourcePart is not None and obj.sourcePart != '': + importPartFromFile( + doc, + absPath, + importToCache=True, + cacheKey = cacheKey, + extractSingleShape = True, + desiredShapeLabel = obj.sourcePart + ) # the version is now in the cache + else: + importPartFromFile( + doc, + absPath, + importToCache=True, + cacheKey = cacheKey + ) # the version is now in the cache + + newObject = objectCache.get(cacheKey) + obj.timeLastImport = newPartCreationTime + if hasattr(newObject, 'a2p_Version'): + obj.a2p_Version = A2P_VERSION + importUpdateConstraintSubobjects( doc, obj, newObject ) # do this before changing shape and mux + #if hasattr(newObject, 'muxInfo'): + # obj.muxInfo = newObject.muxInfo + # save Placement because following newObject.Shape.copy() isn't resetting it to zeroes... + savedPlacement = obj.Placement + obj.Shape = newObject.Shape.copy() + if a2plib.isA2pSketch(obj): + pass + else: + obj.Placement = savedPlacement # restore the old placement + a2plib.copyObjectColors(obj,newObject) + + #repair constraint directions if for e.g. face-normals flipped around during updating of parts. + a2p_constraintServices.redAdjustConstraintDirections(doc) + + mw = FreeCADGui.getMainWindow() + mdi = mw.findChild(QtGui.QMdiArea) + sub = mdi.activeSubWindow() + if sub != None: + sub.showMaximized() + objectCache.cleanUp(doc) + #a2p_solversystem.autoSolveConstraints( + # doc, + # useTransaction = False, + # callingFuncName = "updateImportedParts" + # ) #transaction is already open... + doc.recompute() + doc.commitTransaction() + + + +toolTip = \ +''' +Update parts, which have been +imported to the assembly. + +(If you modify a part in an +external file, the new shape +is taken to the assembly by +this function.) +''' + +class a2p_UpdateImportedPartsCommand: + + def Activated(self): + doc = FreeCAD.ActiveDocument + updateImportedParts(doc) + + def GetResources(self): + return { + 'Pixmap' : a2plib.path_a2p + '/icons/a2p_ImportPart_Update.svg', + 'MenuText': 'Update parts imported into the assembly', + 'ToolTip': toolTip + } + +FreeCADGui.addCommand('a2p_updateImportedParts', a2p_UpdateImportedPartsCommand()) + + + +def duplicateImportedPart( part ): + doc = FreeCAD.ActiveDocument + + nameBase = part.Label + partName = a2plib.findUnusedObjectName(nameBase,document=doc) + partLabel = a2plib.findUnusedObjectLabel(nameBase,document=doc) + if PYVERSION >= 3: + newObj = doc.addObject("Part::FeaturePython", str(partName.encode("utf-8")) ) + else: + newObj = doc.addObject("Part::FeaturePython", partName.encode("utf-8") ) + + newObj.Label = partLabel + + Proxy_importPart(newObj) + ImportedPartViewProviderProxy(newObj.ViewObject) + + + newObj.a2p_Version = part.a2p_Version + newObj.sourceFile = part.sourceFile + newObj.sourcePart = part.sourcePart + newObj.timeLastImport = part.timeLastImport + newObj.setEditorMode("timeLastImport",1) + newObj.fixedPosition = False + newObj.updateColors = getattr(part,'updateColors',True) + newObj.muxInfo = part.muxInfo + newObj.subassemblyImport = part.subassemblyImport + newObj.Shape = part.Shape.copy() + + for p in part.ViewObject.PropertiesList: #assuming that the user may change the appearance of parts differently depending on their role in the assembly. + if hasattr(part.ViewObject, p) and p not in ['DiffuseColor','Proxy','MappedColors']: + setattr(newObj.ViewObject, p, getattr( part.ViewObject, p)) + + newObj.ViewObject.DiffuseColor = copy.copy( part.ViewObject.DiffuseColor ) + newObj.ViewObject.Transparency = part.ViewObject.Transparency + newObj.Placement.Base = part.Placement.Base + newObj.Placement.Rotation = part.Placement.Rotation + return newObj + + + +toolTip = \ +''' +Make a duplicate of a +part, which is already +imported to the assembly. + +Select a imported part and hit +this button. A duplicate +will be created and can be +placed somewhere by mouse. + +Hold "Shift" for doing this +multiple times. +''' + +class a2p_DuplicatePartCommand: + + def __init__(self): + self.partMover = None + + def Activated(self): + doc = FreeCAD.activeDocument() + selection = [s for s in FreeCADGui.Selection.getSelectionEx() if s.Document == doc ] + self.partMover = PartMover( + FreeCADGui.activeDocument().activeView(), + duplicateImportedPart(selection[0].Object), + deleteOnEscape = True + ) + self.timer = QtCore.QTimer() + QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.onTimer) + self.timer.start( 100 ) + + def onTimer(self): + if self.partMover != None: + if self.partMover.objectToDelete != None: + FreeCAD.activeDocument().removeObject(self.partMover.objectToDelete.Name) + self.partMover.objectToDelete = None + self.timer.start(100) + + def IsActive(self): + doc = FreeCAD.activeDocument() + if doc is None: return False + # + selection = [s for s in FreeCADGui.Selection.getSelectionEx() if s.Document == doc ] + if len(selection) != 1: return False + # + obj = selection[0].Object + if not a2plib.isA2pPart(obj): return False + # + return True + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_DuplicatePart.svg', + 'MenuText': 'Create duplicate of a part', + 'ToolTip': toolTip + } + +FreeCADGui.addCommand('a2p_duplicatePart', a2p_DuplicatePartCommand()) + + + +toolTip = \ +''' +Edit an imported part. + +Select an imported part +and hit this button. + +The appropriate FCStd file, +linked to this part will +be opened and you can modify +this part at this place. + +After editing and saving, +you have to use the function +'update imported parts' in +order to see the new shape +within the assembly. +''' + +class a2p_EditPartCommand: + def Activated(self): + doc = FreeCAD.activeDocument() + #==================================================== + # Is there an open Doc ? + #==================================================== + if doc is None: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + u"No active document found!", + u"Before editing a part, you have to open an assembly file." + ) + return + + #==================================================== + # Is something been selected ? + #==================================================== + selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ] + if not selection: + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + u"Selection Error", + u"You must select a part to edit first." + ) + return + + #==================================================== + # Has the selected object an editable a2p file ? + #==================================================== + obj = selection[0] + if not a2plib.isEditableA2pPart(obj): + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + u"Edit: Selection invalid!", + u"This object is no imported part!" + ) + return + + #==================================================== + # Does the file exist ? + #==================================================== + obj = selection[0] + FreeCADGui.Selection.clearSelection() # very important! Avoid Editing the assembly the part was called from! + assemblyPath = os.path.normpath(os.path.split(doc.FileName)[0]) + fileNameWithinProjectFile = a2plib.findSourceFileInProject(obj.sourceFile, assemblyPath) + if fileNameWithinProjectFile is None: + msg = \ +''' +You want to edit a file which +is not found below your project-folder. +This is not allowed when using preference +"Use project Folder" +''' + QtGui.QMessageBox.critical( + QtGui.QApplication.activeWindow(), + "File error ! ", + msg + ) + return + + #==================================================== + # Open the file for editing and switch the window + #==================================================== + + #Workaround to detect open files on Win10 (Address Translation problem??) + importDocIsOpen = False + requestedFile = os.path.split(fileNameWithinProjectFile)[1] + for d in FreeCAD.listDocuments().values(): + recentFile = os.path.split(d.FileName)[1] + if requestedFile == recentFile: + importDoc = d # file is already open... + importDocIsOpen = True + break + + if not importDocIsOpen: + if fileNameWithinProjectFile.lower().endswith('.stp') or fileNameWithinProjectFile.lower().endswith('.step'): + import ImportGui + fname = os.path.splitext(os.path.basename(fileNameWithinProjectFile))[0] + FreeCAD.newDocument(fname) + newname = FreeCAD.ActiveDocument.Name + ImportGui.open(fileNameWithinProjectFile, newname) + FreeCAD.ActiveDocument.Label = fname + FreeCADGui.SendMsgToActiveView("ViewFit") + msg = "Editing a STEP file as '*.FCStd' file\nPlease export the saved file as \'.step\'\n" + fileNameWithinProjectFile + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Info", msg ) + else: + FreeCAD.open(fileNameWithinProjectFile) + else: + name = importDoc.Name + # Search and activate the corresponding document window.. + mw=FreeCADGui.getMainWindow() + mdi=mw.findChild(QtGui.QMdiArea) + sub=mdi.subWindowList() + for s in sub: + mdi.setActiveSubWindow(s) + if FreeCAD.activeDocument().Name == name: break + + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_EditPart.svg', + 'MenuText': 'Edit an imported part (open linked FCStd file)', + 'ToolTip': toolTip + } + +FreeCADGui.addCommand('a2p_editImportedPart', a2p_EditPartCommand()) + + + + +#=============================================================================== +class PartMover: + def __init__(self, view, obj, deleteOnEscape): + self.obj = obj + self.initialPosition = self.obj.Placement.Base + self.view = view + self.deleteOnEscape = deleteOnEscape + self.callbackMove = self.view.addEventCallback("SoLocation2Event",self.moveMouse) + self.callbackClick = self.view.addEventCallback("SoMouseButtonEvent",self.clickMouse) + self.callbackKey = self.view.addEventCallback("SoKeyboardEvent",self.KeyboardEvent) + self.objectToDelete = None # object reference when pressing the escape key + + def moveMouse(self, info): + newPos = self.view.getPoint( *info['Position'] ) + self.obj.Placement.Base = newPos + + def removeCallbacks(self): + self.view.removeEventCallback("SoLocation2Event",self.callbackMove) + self.view.removeEventCallback("SoMouseButtonEvent",self.callbackClick) + self.view.removeEventCallback("SoKeyboardEvent",self.callbackKey) + + def clickMouse(self, info): + if info['Button'] == 'BUTTON1' and info['State'] == 'DOWN': + #if not info['ShiftDown'] and not info['CtrlDown']: #struggles within Inventor Navigation + if not info['ShiftDown']: + self.removeCallbacks() + FreeCAD.activeDocument().recompute() + elif info['ShiftDown']: + self.obj = duplicateImportedPart(self.obj) + self.deleteOnEscape = True + + def KeyboardEvent(self, info): + if info['State'] == 'UP' and info['Key'] == 'ESCAPE': + self.removeCallbacks() + if not self.deleteOnEscape: + self.obj.Placement.Base = self.initialPosition + else: + self.objectToDelete = self.obj #This can be asked by a timer in a calling func... + #This causes a crash in FC0.19/Qt5/Py3 + #FreeCAD.activeDocument().removeObject(self.obj.Name) +#=============================================================================== +toolTip = \ +''' +Move the selected part. + +Select a part and hit this +button. The part can be moved +around by mouse. + +If the part is constrained, it +will jump back by next solving +of the assembly. +''' + +class a2p_MovePartCommand: + + def __init__(self): + self.partMover = None + + def Activated(self): + doc = FreeCAD.activeDocument() + selection = [s for s in FreeCADGui.Selection.getSelectionEx() if s.Document == doc ] + FreeCADGui.ActiveDocument.setEdit(selection[0].Object) + + def IsActive(self): + doc = FreeCAD.activeDocument() + if doc is None: return False + # + selection = [s for s in FreeCADGui.Selection.getSelectionEx() if s.Document == doc ] + if len(selection) != 1: return False + # + obj = selection[0].Object + if not a2plib.isA2pPart(obj): return False + # + return True + + def GetResources(self): + return { + #'Pixmap' : ':/assembly2/icons/MovePart.svg', + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_MovePart.svg', + 'MenuText': 'Move the selected part', + 'ToolTip': toolTip + } + +FreeCADGui.addCommand('a2p_movePart', a2p_MovePartCommand()) +#=============================================================================== +class ConstrainedPartsMover: + def __init__(self, view): + self.obj = None + self.view = view + self.doc = FreeCAD.activeDocument() + self.callbackMove = self.view.addEventCallback("SoLocation2Event",self.onMouseMove) + self.callbackClick = self.view.addEventCallback("SoMouseButtonEvent",self.onMouseClicked) + self.callbackKey = self.view.addEventCallback("SoKeyboardEvent",self.KeyboardEvent) + self.motionActivated = False + + def setPreselection(self,doc,obj,sub): + if not self.motionActivated: + doc = FreeCAD.activeDocument() + self.obj = doc.getObject(obj) + + def addSelection(self,doc,obj,sub,pnt): + pass + + def removeSelection(self,doc,obj,sub): + pass + + def clearSelection(self,doc): + pass + + def onMouseMove(self, info): + if self.obj is None: return + if self.motionActivated: + newPos = self.view.getPoint( *info['Position'] ) + self.obj.Placement.Base = newPos + a2plib.setSimulationState(True) + systemSolved = a2p_solversystem.solveConstraints(self.doc, useTransaction = False) + a2plib.setSimulationState(False) + if systemSolved == False: + self.doc.commitTransaction() + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Animation problem detected", + "Use system undo if necessary." + ) + self.removeCallbacks() + + def removeCallbacks(self): + self.view.removeEventCallback("SoLocation2Event",self.callbackMove) + self.view.removeEventCallback("SoMouseButtonEvent",self.callbackClick) + self.view.removeEventCallback("SoKeyboardEvent",self.callbackKey) + FreeCADGui.Selection.removeObserver(self) + + def onMouseClicked(self, info): + if self.obj is None: return + if info['Button'] == 'BUTTON1' and info['State'] == 'DOWN': + if hasattr(self.obj, 'fixedPosition') and self.obj.fixedPosition == True: + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Invalid selection", + '''A2plus will not move a part with property fixedPosition == True''' + ) + self.removeCallbacks() + del self + else: + self.motionActivated = not self.motionActivated + if self.motionActivated == True: + self.doc.openTransaction("drag constrained parts") + if self.motionActivated == False: + # Solve last time with high accuracy to finish + a2plib.setSimulationState(False) + a2p_solversystem.solveConstraints(self.doc, useTransaction = False) + self.doc.commitTransaction() + self.removeCallbacks() + + def KeyboardEvent(self, info): + doc = FreeCAD.activeDocument() + if info['State'] == 'UP' and info['Key'] == 'ESCAPE': + doc.commitTransaction() + self.removeCallbacks() +#=============================================================================== +toolTip = \ +''' +Move the a part under rule of constraints. + +1) Hit this button +2) Click a part and it is glued to the cursor and can be moved +3) Click again (or press ESC) and the command terminates +''' + +class a2p_MovePartUnderConstraints: + + def __init__(self): + self.partMover = None + + def Activated(self): + self.partMover = ConstrainedPartsMover( + FreeCADGui.activeDocument().activeView() + ) + FreeCADGui.Selection.addObserver(self.partMover) + + def IsActive(self): + doc = FreeCAD.activeDocument() + if doc is None: return False + # + #selection = [s for s in FreeCADGui.Selection.getSelectionEx() if s.Document == doc ] + #if len(selection) != 1: return False + # + #obj = selection[0].Object + #if not a2plib.isA2pPart(obj): return False + # + return True + + def GetResources(self): + return { + #'Pixmap' : ':/assembly2/icons/MovePart.svg', + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_MovePartUnderConstraints.svg', + 'MenuText': 'Move the selected part under constraints', + 'ToolTip': toolTip + } + +FreeCADGui.addCommand('a2p_MovePartUnderConstraints', a2p_MovePartUnderConstraints()) +#=============================================================================== + + + + +toolTipText = \ +''' +Delete all constraints +of a selected part. + +Select exact one part +and hit this button. + +A confirmation dialog pops +up, showing all constraints +related to the selected part. + +After confirmation all related +constraints are deleted +at once. +''' + +class DeleteConnectionsCommand: + def Activated(self): + selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ] + #if len(selection) == 1: not required as this check is done in initGui + # WF: still get 'list index out of range' if nothing selected. + if len(selection) != 1: + QtGui.QMessageBox.critical( + QtGui.QApplication.activeWindow(), + "Selection Error", + "Select exactly 1 part") + return + part = selection[0] + deleteList = [] + for c in FreeCAD.ActiveDocument.Objects: + if 'ConstraintInfo' in c.Content: + if part.Name in [ c.Object1, c.Object2 ]: + deleteList.append(c) + if len(deleteList) == 0: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Info", 'No constraints refer to "%s"' % part.Name) + else: + flags = QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.No + msg = u"Delete {}'s constraint(s):\n - {}?".format( + part.Label, + u'\n - '.join( c.Name for c in deleteList) + ) + response = QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + "Delete constraints?", + msg, + flags + ) + if response == QtGui.QMessageBox.Yes: + doc = FreeCAD.activeDocument() + doc.openTransaction("Deleting part's constraints") + for c in deleteList: + a2plib.removeConstraint(c) + doc.commitTransaction() + + def IsActive(self): + selection = FreeCADGui.Selection.getSelection() + if len(selection) != 1: + return False + + obj = selection[0] + if a2plib.isConstrainedPart(FreeCAD.activeDocument(), obj): + return True + else: + return False + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_DeleteConnections.svg', + 'MenuText': 'Delete all constraints of selected parts', + 'ToolTip': toolTipText + } +FreeCADGui.addCommand('a2p_DeleteConnectionsCommand', DeleteConnectionsCommand()) + +toolTip = \ +''' +Highlight both parts, which are +related to a selected constraint. + +Select a constraint within +the treeview and hit this button. + +The whole assembly is switched to +transparent mode and you can inspect +the desired constraint. +''' + +class ViewConnectionsCommand: + def Activated(self): + doc = FreeCAD.ActiveDocument + + selected = a2plib.getSelectedConstraint() + if selected is None: + return + + initialTransparencyState = a2plib.isTransparencyEnabled() + if not initialTransparencyState: + a2plib.setTransparency() + + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection( + doc.getObject(selected.Object1), selected.SubElement1) + + FreeCADGui.Selection.addSelection( + doc.getObject(selected.Object2), selected.SubElement2) + + # Add observer to remove the transparency when the selection is changing or removing + FreeCADGui.Selection.addObserver(ViewConnectionsObserver(initialTransparencyState)) + + def IsActive(self): + #return (a2plib.getSelectedConstraint() is not None and a2plib.isTransparencyEnabled() == False) + return (a2plib.getSelectedConstraint() is not None) + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_ViewConnection.svg', + 'MenuText': 'Highlight both constrained parts', + 'ToolTip': toolTip, + } + +FreeCADGui.addCommand('a2p_ViewConnectionsCommand', ViewConnectionsCommand()) + +class ViewConnectionsObserver: + def __init__(self,initialTransparencyState): + self.ignoreClear = False + self.initialTransparencyState = initialTransparencyState + a2plib.setConstraintViewMode(True) + + def clearSelection(self, doc): + if self.ignoreClear: + self.ignoreClear = False + else: + # remove observer at once, as restoreTransparency would trigger it again... + FreeCADGui.Selection.removeObserver(self) + # + if a2plib.isTransparencyEnabled() and not self.initialTransparencyState: + a2plib.restoreTransparency() + a2plib.setConstraintViewMode(False) + + def setSelection(self, doc): + selected = a2plib.getSelectedConstraint() + if selected is not None: + self.ignoreClear = True + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection( + FreeCAD.ActiveDocument.getObject(selected.Object1), selected.SubElement1) + + FreeCADGui.Selection.addSelection( + FreeCAD.ActiveDocument.getObject(selected.Object2), selected.SubElement2) + +toolTip = \ +''' +Show only selected elements, +or all if none is selected. + +Select one or more parts, +which are the only ones you +want to see in a big assembly. + +Hit this button, and all other +parts will be made invisible. + +If you select nothing and hit +this button, all invisible parts +will be made visible again. +''' + +class a2p_isolateCommand: + + def hasFaces(self,ob): + if hasattr(ob,"Shape") and hasattr(ob.Shape,"Faces") and len(ob.Shape.Faces)>0: + return True + return False + + def Activated(self): + if FreeCAD.activeDocument() is None: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + "No active document found!", + "You have to open an assembly file first." + ) + return + selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ] + FreeCADGui.Selection.clearSelection() + doc = FreeCAD.ActiveDocument + + if len(selection) == 0: # Show all elements + for obj in doc.Objects: + if obj.Name == 'PartInformation': continue + if obj.Name[:4] == 'Page': continue + if obj.Name == 'SimpleAssemblyShape': continue + if not self.hasFaces(obj): continue + if hasattr(obj,'ViewObject'): + if hasattr(obj.ViewObject,'Visibility'): + obj.ViewObject.Visibility = True + else: # Show only selected elements + for obj in doc.Objects: + if obj.Name == 'PartInformation': continue + if obj.Name[:4] == 'Page': continue + if obj.Name == 'SimpleAssemblyShape': continue + if a2plib.isA2pConstraint(obj): continue + if hasattr(obj,'ViewObject'): + if hasattr(obj.ViewObject,'Visibility'): + if obj in selection: + obj.ViewObject.Visibility = True + else: + obj.ViewObject.Visibility = False + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_Isolate_Element.svg', + 'MenuText': 'Show only selected elements or all if none is selected', + 'ToolTip': toolTip + } + +FreeCADGui.addCommand('a2p_isolateCommand', a2p_isolateCommand()) + + + + + +class a2p_ToggleTransparencyCommand: + def Activated(self, checked): + if FreeCAD.activeDocument() is None: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + "No active document found!", + "You have to open an assembly file first." + ) + return + if a2plib.isTransparencyEnabled(): + a2plib.restoreTransparency() + else: + a2plib.setTransparency() + + def IsChecked(self): + return a2plib.isTransparencyEnabled() + + def IsActive(self): + return not a2plib.getConstraintViewMode() + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_ToggleTransparency.svg', + 'MenuText': 'Toggle transparency of assembly', + 'ToolTip': 'Toggles transparency of assembly', + 'Checkable': self.IsChecked() + } +FreeCADGui.addCommand('a2p_ToggleTransparencyCommand', a2p_ToggleTransparencyCommand()) + + + +toolTipMessage = \ +''' +Toggle AutoSolve + +By pressing this button you can +enable or disable automatic solving +after a constraint has been edited + +If automatic solving is disabled +you have to start it manually +by hitting the solvebutton + +''' + +class a2p_ToggleAutoSolveCommand: + + def Activated(self, checked): + a2plib.setAutoSolve(checked) + + def IsChecked(self): + return a2plib.getAutoSolveState() + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_ToggleAutoSolve.svg', + 'MenuText': 'Toggle auto solve', + 'ToolTip': toolTipMessage, + 'Checkable': self.IsChecked() + } +FreeCADGui.addCommand('a2p_ToggleAutoSolveCommand', a2p_ToggleAutoSolveCommand()) + + + +class a2p_TogglePartialProcessingCommand: + + def Activated(self, checked): + a2plib.setPartialProcessing(checked) + + def IsChecked(self): + return a2plib.isPartialProcessing() + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_TogglePartial.svg', + 'MenuText': 'Toggle partial processing', + 'ToolTip': 'Toggles partial processing', + 'Checkable': self.IsChecked() + } +FreeCADGui.addCommand('a2p_TogglePartialProcessingCommand', a2p_TogglePartialProcessingCommand()) + + + +toolTipMessage = \ +''' +Repair the treeview, if it +is damaged somehow. + +After pressing this button, +constraints will grouped under +corresponding parts again. +''' + +class a2p_repairTreeViewCommand: + + def Activated(self): + if FreeCAD.activeDocument() is None: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + "No active document found!", + "You have to open an assembly file first." + ) + return + a2plib.a2p_repairTreeView() + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_RepairTree.svg', + 'MenuText': 'Repair the tree view if it is somehow damaged', + 'ToolTip': toolTipMessage + } +FreeCADGui.addCommand('a2p_repairTreeViewCommand', a2p_repairTreeViewCommand()) + +toolTip = \ +''' +Flip direction of last constraint. + +If the last constraint, which has +been defined, has a property +'direction', its value will be +toggled between 'aligned' and +'opposed' (alignment of axis) +''' + + +class a2p_FlipConstraintDirectionCommand: + + def Activated(self): + if FreeCAD.activeDocument() is None: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + "No active document found!", + "You have to open an assembly file first." + ) + return + a2p_FlipConstraintDirection() + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_FlipConstraint.svg', + 'MenuText': 'Flip direction of last constraint', + 'ToolTip': toolTip + } +FreeCADGui.addCommand('a2p_FlipConstraintDirectionCommand', a2p_FlipConstraintDirectionCommand()) + +def a2p_FlipConstraintDirection(): + ''' updating constraints, deactivated at moment''' + constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects + if 'ConstraintInfo' in obj.Content ] + if len(constraints) == 0: + QtGui.QMessageBox.information( + QtGui.qApp.activeWindow(), + "Command Aborted", + 'Flip aborted since no a2p constraints in active document.' + ) + return + lastConstraintAdded = constraints[-1] + try: + if lastConstraintAdded.directionConstraint == 'aligned': + lastConstraintAdded.directionConstraint = 'opposed' + else: + lastConstraintAdded.directionConstraint = 'aligned' + a2p_solversystem.autoSolveConstraints(FreeCAD.activeDocument(), callingFuncName="a2p_FlipConstraintDirection") + except: + pass + + + + +class a2p_Show_Hierarchy_Command: + + def Activated(self): + doc = FreeCAD.activeDocument() + if doc is None: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + "No active document found!", + "You have to open an assembly file first." + ) + return + ss = a2p_solversystem.SolverSystem() + ss.loadSystem(doc) + ss.assignParentship(doc) + ss.visualizeHierarchy() + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_Treeview.svg', + 'MenuText': 'Generate HTML file with detailed constraining structure', + 'ToolTip': 'Generates HTML file with detailed constraining structure' + } +FreeCADGui.addCommand('a2p_Show_Hierarchy_Command', a2p_Show_Hierarchy_Command()) + + + +class a2p_Show_PartLabels_Command: + + def Activated(self, index): + doc = FreeCAD.activeDocument() + if index == 0: + '''remove labels from 3D view''' + dofGroup = doc.getObject("partLabels") + if dofGroup != None: + for lbl in dofGroup.Group: + doc.removeObject(lbl.Name) + doc.removeObject("partLabels") + else: + '''create or update labels within 3D view''' + a2pObjects = [] + for ob in doc.Objects: + if a2plib.isA2pPart(ob): + a2pObjects.append(ob) + if len(a2pObjects) == 0: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + "Nothing found to be labeled!", + "This document does not contain A2p-objects" + ) + return + + labelGroup = doc.getObject("partLabels") + if labelGroup is None: + labelGroup=doc.addObject("App::DocumentObjectGroup", "partLabels") + else: + for lbl in labelGroup.Group: + doc.removeObject(lbl.Name) + doc.removeObject("partLabels") + labelGroup=doc.addObject("App::DocumentObjectGroup", "partLabels") + + for ob in a2pObjects: + if ob.ViewObject.Visibility == True: + bbCenter = ob.Shape.BoundBox.Center + partLabel = doc.addObject("App::AnnotationLabel","partLabel") + partLabel.LabelText = a2plib.to_str(ob.Label) + partLabel.BasePosition.x = bbCenter.x + partLabel.BasePosition.y = bbCenter.y + partLabel.BasePosition.z = bbCenter.z + # + partLabel.ViewObject.BackgroundColor = a2plib.YELLOW + partLabel.ViewObject.TextColor = a2plib.BLACK + labelGroup.addObject(partLabel) + + def IsChecked(self): + doc = FreeCAD.activeDocument() + if not doc: return False + labelGroup = doc.getObject("partLabels") + return labelGroup != None + + def IsActive(self): + doc = FreeCAD.activeDocument() + return doc != None + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_PartLabel.svg', + 'MenuText': "Show part labels in 3D view", + 'ToolTip': "Toggle showing part labels in 3D view", + 'Checkable': False + } +FreeCADGui.addCommand('a2p_Show_PartLabels_Command', a2p_Show_PartLabels_Command()) + + +class a2p_Show_DOF_info_Command: + + def Activated(self, index): + if index == 0: + ''' Remove the existing labels from screen''' + doc = FreeCAD.activeDocument() + dofGroup = doc.getObject("dofLabels") + if dofGroup != None: + for lbl in dofGroup.Group: + doc.removeObject(lbl.Name) + doc.removeObject("dofLabels") + else: + ss = a2p_solversystem.SolverSystem() + ss.DOF_info_to_console() + + def IsActive(self): + doc = FreeCAD.activeDocument() + return doc != None + + def IsChecked(self): + doc = FreeCAD.activeDocument() + if not doc: return False + dofGroup = doc.getObject("dofLabels") + return dofGroup != None + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_DOFs.svg', + 'MenuText': 'Print detailed DOF information', + 'ToolTip': 'Toggle printing detailed DOF information', + 'Checkable': False + } +FreeCADGui.addCommand('a2p_Show_DOF_info_Command', a2p_Show_DOF_info_Command()) + + + +class a2p_absPath_to_relPath_Command: + def Activated(self): + doc = FreeCAD.activeDocument() + if doc is None: + QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), + "No active document found!", + "You have to open an assembly file first." + ) + return + assemblyPath = os.path.normpath( os.path.split( os.path.normpath(doc.FileName) )[0]) + importParts = [ob for ob in doc.Objects if "mportPart" in ob.Content] + for iPart in importParts: + if ( + iPart.sourceFile.startswith("./") or + iPart.sourceFile.startswith("../") or + iPart.sourceFile.startswith(".\\") or + iPart.sourceFile.startswith("..\\") + ): continue # path is already relative + filePath = os.path.normpath(iPart.sourceFile) + if platform.system() == "Windows": + prefix = '.\\' + else: + prefix = './' + iPart.sourceFile = prefix + os.path.relpath(filePath, assemblyPath) + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_SetRelativePathes.svg', + 'MenuText': 'Convert absolute paths of imported parts to relative ones', + 'ToolTip': 'Converts absolute paths of imported parts to relative ones' + } +FreeCADGui.addCommand('a2p_absPath_to_relPath_Command', a2p_absPath_to_relPath_Command()) + + + + +#============================================================================== +class a2p_SaveAndExit_Command: + def Activated(self): + doc = FreeCAD.activeDocument() + try: + doc.save() + FreeCAD.closeDocument(doc.Name) + except: + FreeCADGui.SendMsgToActiveView("Save") + if not FreeCADGui.activeDocument().Modified: # user really saved the file + FreeCAD.closeDocument(doc.Name) + # + mw = FreeCADGui.getMainWindow() + mdi = mw.findChild(QtGui.QMdiArea) + sub = mdi.activeSubWindow() + if sub != None: + sub.showMaximized() + + def IsActive(self): + return FreeCAD.activeDocument() != None + + def GetResources(self): + return { + 'Pixmap' : a2plib.pathOfModule()+'/icons/a2p_Save_and_exit.svg', + 'MenuText': 'Save and exit the active document', + 'ToolTip': 'Save and exit the active document' + } +FreeCADGui.addCommand('a2p_SaveAndExit_Command', a2p_SaveAndExit_Command()) + +#============================================================================== +toolTip = \ +''' +Migrate proxies of imported parts + +Very old A2plus assemblies do not +show the correct icons for imported +parts and have obsolete properties. + +With this function, you can migrate +the viewProviders of old imported parts +to the recent state. + +After running this function, you +should save and reopen your +assembly file. +''' + +class a2p_MigrateProxiesCommand(): + + def Activated(self): + flags = QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.No + response = QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + u"Migrate proxies of importedParts to recent version", + u"Make sure you have a backup of your files. Proceed?", + flags + ) + if response == QtGui.QMessageBox.Yes: + doc = FreeCAD.activeDocument() + for ob in doc.Objects: + if a2plib.isA2pPart(ob): + #setup proxies + a2p_importedPart_class.Proxy_importPart(ob) + if FreeCAD.GuiUp: + a2p_importedPart_class.ImportedPartViewProviderProxy(ob.ViewObject) + #delete obsolete properties + deleteList = [] + tmp = ob.PropertiesList + for prop in tmp: + if prop.startswith('pi_') or prop == 'assembly2Version': + deleteList.append(prop) + for prop in deleteList: + ob.removeProperty(prop) + + QtGui.QMessageBox.information( + QtGui.QApplication.activeWindow(), + u"The proxies have been migrated.", + u"Please save and reopen this assembly file" + ) + + + + def GetResources(self): + return { + 'Pixmap' : ':/icons/a2p_Upgrade.svg', + 'MenuText': 'Migrate proxies of imported parts', + 'ToolTip': toolTip + } + +FreeCADGui.addCommand('a2p_MigrateProxiesCommand', a2p_MigrateProxiesCommand()) +#============================================================================== + + + + + + + +def importUpdateConstraintSubobjects( doc, oldObject, newObject ): + if not a2plib.getUseTopoNaming(): return + + # return if there are no constraints linked to the object + if len([c for c in doc.Objects if 'ConstraintInfo' in c.Content and oldObject.Name in [c.Object1, c.Object2] ]) == 0: + return + + + # check, whether object is an assembly with muxInformations. + # Then find edgenames with mapping in muxinfo... + deletionList = [] #for broken constraints + if hasattr(oldObject, 'muxInfo'): + if hasattr(newObject, 'muxInfo'): + # + oldVertexNames = [] + oldEdgeNames = [] + oldFaceNames = [] + for item in oldObject.muxInfo: + if item[:1] == 'V': + oldVertexNames.append(item) + if item[:1] == 'E': + oldEdgeNames.append(item) + if item[:1] == 'F': + oldFaceNames.append(item) + # + newVertexNames = [] + newEdgeNames = [] + newFaceNames = [] + for item in newObject.muxInfo: + if item[:1] == 'V': + newVertexNames.append(item) + if item[:1] == 'E': + newEdgeNames.append(item) + if item[:1] == 'F': + newFaceNames.append(item) + # + partName = oldObject.Name + for c in doc.Objects: + if 'ConstraintInfo' in c.Content: + if partName == c.Object1: + SubElement = "SubElement1" + elif partName == c.Object2: + SubElement = "SubElement2" + else: + SubElement = None + + if SubElement: #same as subElement <> None + + subElementName = getattr(c, SubElement) + if subElementName[:4] == 'Face': + try: + oldIndex = int(subElementName[4:])-1 + oldConstraintString = oldFaceNames[oldIndex] + newIndex = newFaceNames.index(oldConstraintString) + newSubElementName = 'Face'+str(newIndex+1) + except: + newIndex = -1 + newSubElementName = 'INVALID' + + elif subElementName[:4] == 'Edge': + try: + oldIndex = int(subElementName[4:])-1 + oldConstraintString = oldEdgeNames[oldIndex] + newIndex = newEdgeNames.index(oldConstraintString) + newSubElementName = 'Edge'+str(newIndex+1) + except: + newIndex = -1 + newSubElementName = 'INVALID' + + elif subElementName[:6] == 'Vertex': + try: + oldIndex = int(subElementName[6:])-1 + oldConstraintString = oldVertexNames[oldIndex] + newIndex = newVertexNames.index(oldConstraintString) + newSubElementName = 'Vertex'+str(newIndex+1) + except: + newIndex = -1 + newSubElementName = 'INVALID' + + else: + newIndex = -1 + newSubElementName = 'INVALID' + + if newIndex >= 0: + setattr(c, SubElement, newSubElementName ) + print ( + "oldConstraintString (KEY) : {}".format( + oldConstraintString + ) + ) + print ("Updating by SubElement-Map: {} => {} ".format( + subElementName,newSubElementName + ) + ) + continue + # + # if code coming here, constraint is broken + if c.Name not in deletionList: + deletionList.append(c.Name) + + + if len(deletionList) > 0: # there are broken constraints.. + for cName in deletionList: + + flags = QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.Abort + message = "Constraint %s is broken. Delete constraint? Otherwise check for wrong linkage." % cName + #response = QtGui.QMessageBox.critical(QtGui.qApp.activeWindow(), "Broken Constraint", message, flags ) + response = QtGui.QMessageBox.critical(None, "Broken Constraint", message, flags ) + + if response == QtGui.QMessageBox.Yes: + FreeCAD.Console.PrintError("Removing constraint %s" % cName) + c = doc.getObject(cName) + a2plib.removeConstraint(c) + diff --git a/InitGui.py b/InitGui.py index f78aa1fd..6a4d1762 100644 --- a/InitGui.py +++ b/InitGui.py @@ -79,7 +79,7 @@ def Initialize(self): import a2p_bom # bom == bill of materials == partslist import a2p_constraintServices import a2p_searchConstraintConflicts - + import CD_A2plusupdater # for Constraint Diagnostic function if a2plib.getRecursiveUpdateEnabled(): partCommands = [ 'a2p_ImportPart', @@ -224,6 +224,15 @@ def Initialize(self): '/GuiA2p/Resources/ui/a2p_prefs.ui','A2plus' ) + DiagnosticCommands = [ + 'rnp_Constraint_Viewer', + 'rnp_Update_A2pParts', + ] + + self.appendToolbar( + 'A2Diagnostics', + DiagnosticCommands + ) def Activated(self): import a2p_observers diff --git a/a2p_constraintServices.py b/a2p_constraintServices.py index b2b59d80..097e18a8 100644 --- a/a2p_constraintServices.py +++ b/a2p_constraintServices.py @@ -36,10 +36,11 @@ def redAdjustConstraintDirections(doc): a2p-constraints of a document, in order to reach a solvable state if possible, especially used after updating of imported parts. """ + result = [] # Added for Constraint Diagnostic function unknown_constraints = [] constraints = [ obj for obj in doc.Objects if 'ConstraintInfo' in obj.Content] for c in constraints: - try: #process as much constraints as possible + try: # process as much constraints as possible if c.Type == 'pointIdentity': a2p_constraints.PointIdentityConstraint.recalculateMatingDirection(c) elif c.Type == 'pointOnLine': @@ -72,12 +73,14 @@ def redAdjustConstraintDirections(doc): unknown_constraints.append(c.Type) except: print("Errors occurred during processing of {}".format(c.Label)) + result.append(c.Name) # Added for Constraint Diagnostic function if len(unknown_constraints) > 0: print("redefineConstraintDirections(): Found unknown constraints: {}".format( set(unknown_constraints) ) ) + return(result) # Added for Constraint Diagnostic function #============================================================================== class a2p_reAdjustConstraintDirectionsCommand: diff --git a/icons/ConstraintDiagnostics.svg b/icons/ConstraintDiagnostics.svg new file mode 100644 index 00000000..58fa29d6 --- /dev/null +++ b/icons/ConstraintDiagnostics.svg @@ -0,0 +1,1109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/updateA2.svg b/icons/updateA2.svg new file mode 100644 index 00000000..4503bd35 --- /dev/null +++ b/icons/updateA2.svg @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + +