Macro Sketch Constraint From Spreadsheet

Description
'''Work in progress. It work for me. Tell me if anything is wrong.'''

Macro which, by a simple click on a spreadsheet cell, adds a length constraint to a line or between 2 points using a spreadsheet cell alias or name (ex. C2). Future changes to the spreadsheet will update the constraint.

Just select 1 line, 2 points or a constraint, click on a spreadsheet cell and run the macro. You can select lines, points at the ends of a line, points, circle, arc of circle.



Usage
1) Select:
 * a line,
 * two points (end of a line, center of a circle, etc.)
 * or a length constraint



2) Click on a spreadsheet cell, with or without an alias, that has a numerical value:



3) Run the macro.

4) Select the desired type of constraint:



If the cell has an alias, the length property of the constraint will be something like 'Spreadsheet.alias'. Otherwise, something like 'Spreadsheet.D4'.



5) If the constraint causes a conflict in the sketch and the "conflict detection" box is checked, the macro will offer to delete the new constraint:



If you select an existing constraint, you can change the value for a cell in the spreadsheet:



Links

 * Forum discussion (French)
 * Macros recipes
 * How to install macros
 * How to customize toolbars

Credits
Thanks to openBrain, mario52 and onekk for help on the code!

Script
ToolBar Icon

Code
ver 00.02.1 28/02/2023 by 2cv001 sketchConstraintFromSpreadsheet.FCMacro

__author__ = "2cv001" __title__  = "Macro Sketch Constraint From Spreadsheet" __date__   = "2023/02/27"    #YYYY/MM/DD __version__ = __date__ __icon__   = "https://wiki.freecad.org/File:SketchConstraintFromSpreadsheet.svg" __Wiki__   = "https://wiki.freecad.org/Macro_sketchConstraintFromSpreadsheet"
 * 1) !/usr/bin/env python
 * 2) -*- coding: utf-8 -*-
 * 3) Macro Sketch Constraint From Spreadsheet
 * 4) == Adds a length constraint to a line or between 2 points              ==
 * 5) == using a spreadsheet cell alias or name (ex. C2).                    ==
 * 6) == Future changes to the spreadsheet will update the constraint.       ==
 * 7) == USE:                                                                ==
 * 8) == 1) Select 1 line, 2 points or a constraint                          ==
 * 9) == 2) Click on a spreadsheet cell                                      ==
 * 10) == 3) Launch the macro                                                 ==
 * 11) ==    if the cell has an alias, the length property will be something  ==
 * 12) ==    like 'Spreadsheet.alias'.                                        ==
 * 13) ==    if not, just something like 'Spreadsheet.C2'                     ==
 * 14) == You can select lines, points line, points, circle...                ==
 * 1) ==    like 'Spreadsheet.alias'.                                        ==
 * 2) ==    if not, just something like 'Spreadsheet.C2'                     ==
 * 3) == You can select lines, points line, points, circle...                ==

import FreeCAD, FreeCADGui from PySide import QtGui, QtCore import PySide2 from PySide2.QtGui import QGuiApplication import PySide from PySide2 import QtWidgets import Draft, Part, Sketcher import itertools import configparser import os

class getConstraintType(QtGui.QDialog): def __init__(self): super(getConstraintType, self).__init__ self.initUI
 * 1) Dialog box
 * 2) Ask user which sort of constraint is required
 * 1) Ask user which sort of constraint is required

def initUI(self): self.setWindowIcon(QtGui.QIcon('dialog_icon.png')) gridLayout = QtGui.QGridLayout #macroDirectory=FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro").GetString("MacroPath")+"\\" # LINUX ? option1Button = QtGui.QPushButton(QtGui.QIcon(":/icons/constraints/Constraint_HorizontalDistance.svg"), "") option2Button = QtGui.QPushButton(QtGui.QIcon(":/icons/constraints/Constraint_VerticalDistance.svg"), "") option3Button = QtGui.QPushButton(QtGui.QIcon(":/icons/constraints/Constraint_Length.svg"), "") option1Button.setText("Lenght constrainte X") option2Button.setText("Lenght constrainte Y") option3Button.setText("Lenght constrainte") option1Button.setToolTip("Lenght constrainte X") option2Button.setToolTip("Lenght constrainte Y") option3Button.setToolTip("Lenght constrainte") option1Button.clicked.connect(self.onOption1) option2Button.clicked.connect(self.onOption2) option3Button.clicked.connect(self.onOption3) option4Button = QtGui.QPushButton(QtGui.QIcon(":/icons/application-exit.svg"), "Cancel") option4Button.setToolTip("Option 4 tooltip") option4Button.clicked.connect(self.onOption4)

gridLayout.addWidget(option1Button, 0, 0) gridLayout.addWidget(option2Button, 0, 1) gridLayout.addWidget(option3Button, 1, 0) gridLayout.addWidget(option4Button, 1, 1)

self.setLayout(gridLayout) self.setGeometry(250, 250, 0, 50) self.setWindowTitle("Choose a constraint type") self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.choiceConstraint = '' # Add checkbox self.checkBox = QtGui.QCheckBox("Conflict detection") self.checkBox.setChecked(True) gridLayout.addWidget(self.checkBox, 2, 0, 1, 2) self.checkBox.clicked.connect(self.onOptionCheckBox) # read ini file to get last checkBoxState config = configparser.ConfigParser macroDirectory = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro").GetString("MacroPath") + "\\" try : config.read(macroDirectory + 'constraintFromTabIcone.ini') # read ini file to know last time state lasChecked=config.getboolean('DEFAULT', 'save_checkbox_state') self.checkBox.setChecked(lasChecked) except : print('no ini file. Ini file will be create for next launch')

# window positioning centerPoint = QGuiApplication.screens[0].geometry.center self.move(centerPoint - self.frameGeometry.center)

def onOption1(self): self.choiceConstraint = 'DistanceX' self.close

def onOption2(self): self.choiceConstraint = 'DistanceY' self.close

def onOption3(self): self.choiceConstraint = 'Distance' self.close def onOption4(self): self.choiceConstraint = 'Cancel' self.close def onOptionCheckBox(self): macroDirectory = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro").GetString("MacroPath") + "\\"

# Save checkbox state to file filePath = os.path.join(macroDirectory, "constraintFromTabIcone.ini") config = configparser.ConfigParser config['DEFAULT'] = {'save_checkbox_state': str(int(self.getCheckBoxState))} with open(filePath, 'w') as configfile: config.write(configfile)

def getCheckBoxState(self): return self.checkBox.isChecked def activateSketchEditingWindow: def searchForNode(tree, childName, maxLevel=0): return recursiveSearchForNode(tree, childName, maxLevel, 1)
 * 1) Give the focus to editing sketch window
 * 2) no parameter
 * 3) use : activateSketchEditingWindow
 * 1) use : activateSketchEditingWindow

def recursiveSearchForNode(tree, childName, maxLevel, currentLevel): try: if tree.getByName(childName): return True; elif maxLevel > 0 and currentLevel >= maxLevel: return False; else: for child in tree.getChildren: if recursiveSearchForNode(child, childName, maxLevel, currentLevel+1): return True except: pass return False

doc = Gui.ActiveDocument if not doc: QtWidgets.QMessageBox.information(Gui.getMainWindow, "Activate window", "No active document") return views = Gui.ActiveDocument.mdiViewsOfType("Gui::View3DInventor") if not views: QtWidgets.QMessageBox.information(Gui.getMainWindow, "Activate window", "No 3D view opened for active document") return editView=None for view in views: if searchForNode(view.getSceneGraph, "Sketch_EditRoot",3): editView = view break if not editView: QtWidgets.QMessageBox.information(Gui.getMainWindow, "Activate window", "No 3D view has sketch in edit mode for active document") return for win in Gui.getMainWindow.centralWidget.subWindowList: if editView.graphicsView in win.findChildren(QtWidgets.QGraphicsView): Gui.getMainWindow.centralWidget.setActiveSubWindow(win) break


 * 1) to get necessary values for the constraint
 * 2) Parameters :
 * 3) sel : selection of objects (a line, 2 points..).
 * 4) numOrdreObjSelected if we want the first objetc selected or the second.
 * 5) indexExtremite if we want the start point (1) or the end point (2), if exist, of the sel
 * 1) indexExtremite if we want the start point (1) or the end point (2), if exist, of the sel

def featuresObjSelected (mySketch,sel,numOrdreObjSelected,indexExtremite) : indexExtremiteLine=1 indexObjectHavingPoint=-10 typeIdGeometry=None x,y=0,0 itemName=sel.SubElementNames[numOrdreObjSelected] # ex Edge5 ( line) if itemName=='RootPoint' : typeInSubElementName='RootPoint' indexObjectHavingPoint=-1 typeIdGeometry=mySketch.Geometry[indexObjectHavingPoint].TypeId x,y=0,0 else : typeInSubElementName, numInNameStr = [''.join(c) for _, c in itertools.groupby(itemName, str.isalpha)] # Edge5 renvoie'Edge' et '5' numInName=int(numInNameStr) # index de ce qui a été sélectionné =numInName1-1 car commence à 0
 * 1) return features of a point
 * 2) - typeInSubElementName
 * 3) - indexObjectHavingPoint index of the object having the point (line...)
 * 4) - indexExtremiteLine index of the ends (points) of the line (=start point or end point)
 * -x,y : coordinates of the point

# only one selected object if typeInSubElementName == 'Edge'and len(sel.SubElementNames)==1: # selection is only one line indexObjectHavingPoint=numInName-1 indexExtremiteLine=indexExtremite typeIdGeometry=mySketch.Geometry[indexObjectHavingPoint].TypeId # typeIdGeometry : 'Part::GeomCircle'ou 'Part::GeomLineSegment' if typeIdGeometry in ['Part::GeomLineSegment'] : if indexExtremite == 1 : x=mySketch.Geometry[indexObjectHavingPoint].StartPoint.x               y=mySketch.Geometry[indexObjectHavingPoint].StartPoint.y            elif indexExtremite == 2 : x=mySketch.Geometry[indexObjectHavingPoint].EndPoint.x               y=mySketch.Geometry[indexObjectHavingPoint].EndPoint.y                if typeInSubElementName=='Vertex' : # selection is 2 points. sel is a vertex (a point of a line) : indexObjectHavingPoint, indexExtremiteLine = sel.Object.getGeoVertexIndex(numInName-1) typeIdGeometry=mySketch.Geometry[indexObjectHavingPoint].TypeId if mySketch.Geometry[indexObjectHavingPoint].TypeId=='Part::GeomLineSegment' : if indexExtremiteLine==1 : x=mySketch.Geometry[indexObjectHavingPoint].StartPoint.x               y=mySketch.Geometry[indexObjectHavingPoint].StartPoint.y            if indexExtremiteLine==2: x=mySketch.Geometry[indexObjectHavingPoint].EndPoint.x               y=mySketch.Geometry[indexObjectHavingPoint].EndPoint.y         if mySketch.Geometry[indexObjectHavingPoint].TypeId=='Part::GeomPoint' : x=mySketch.Geometry[indexObjectHavingPoint].X           y=mySketch.Geometry[indexObjectHavingPoint].Y                         if mySketch.Geometry[indexObjectHavingPoint].TypeId in ['Part::GeomCircle','Part::GeomArcOfCircle'] : x=mySketch.Geometry[indexObjectHavingPoint].Center.x           y=mySketch.Geometry[indexObjectHavingPoint].Center.y

if typeInSubElementName=='Constraint' and len(sel.SubElementNames)==1 : indexConstraint=numInName-1 indexObjectHavingPoint=indexConstraint typeIdGeometry='Constraint'

return typeIdGeometry,typeInSubElementName, indexObjectHavingPoint, indexExtremiteLine, x ,y

def procEnd: activateSketchEditingWindow
 * 1) call at end
 * 1) call at end

def getGuiObjsSelect(type=''): tabGObjSelect=[] selections=Gui.Selection.getCompleteSelection for sel in (selections): if hasattr(sel, 'Object'):  # depend freecad version if type=='' or sel.Object.TypeId==type : tabGObjSelect.append(sel.Object) else : obj=App.ActiveDocument.getObject(sel.Name) if type=='' or obj.TypeId==type : tabGObjSelect.append(obj) return tabGObjSelect
 * 1) function returning selected objects at GUI level
 * 2) =Sketch, SpreadSheet ....
 * 3) parameter :
 * 4) '' = no filter
 * 5) 'Spreadsheet::Sheet' for spreadsheets only
 * 6) 'Sketcher::SketchObject' for sketches etc...
 * 7) output: an array of sketch objects, spreadsheets etc.
 * 1) output: an array of sketch objects, spreadsheets etc.

def main: #initialization sheckBoxConstraintConflicState=False indexConstraint=-1 try : mySketch=ActiveSketch except : QtWidgets.QMessageBox.information(None,"Warning","Select object must be done in edition mode") return mySketchName=mySketch.Name #actually not use # Part SpreadSheet #-   sheets=getGuiObjsSelect('Spreadsheet::Sheet') for sheet in sheets : Gui.Selection.removeSelection(FreeCAD.ActiveDocument.Name,sheet.Name) try : mySpreadSheet=Gui.ActiveDocument.ActiveView.getSheet except : QtWidgets.QMessageBox.information(None,"Warning",               "1- Select a line or 2 points"+                "\n 2- go to a spreadsheet"+                 "\n 3- select the cell containing the value."+                "\n 4- stay in the spreadsheet and launch the macro") return mySpreadSheetName = mySpreadSheet.Name # select the Spreadsheet To be able to retrieve the selected cell : mySpreadSheet.ViewObject.doubleClicked # retrieve the selected cell ci = lambda :Gui.getMainWindow.centralWidget.activeSubWindow.widget.findChild(QtGui.QTableView).currentIndex cellCode = '{}{}{}'.format(chr(ci.column//28 + 64) if ci.column//26 > 0 else '', chr(ci.column%26+65), ci.row+1) try: cellContents = float(mySpreadSheet.get(cellCode)) except: QtWidgets.QMessageBox.information(None,"Warning",                "Click on a cell with a numeric value before returning to the sketch") return cellAlias= App.ActiveDocument.getObject(mySpreadSheetName).getAlias(cellCode) # Part sketch #   sels = Gui.Selection.getSelectionEx if len(sels[0].SubElementNames)==0 : QtWidgets.QMessageBox.information(None,"Warning","Anathing is select.\n"+             "Select 1 line, 2 points or a constraint in a sketch before selecting a cell in the spreadsheet") return elif len(sels[0].SubElementNames) >2 : QtWidgets.QMessageBox.information(None,"Warning","Too many objects selected.\n"+             "Select 1 line, 2 points or a constraint in a sketch before selecting a cell in the spreadsheet") return else : if len(sels[0].SubElementNames)==1 : # only one obj selected #startPoint of the line (typeIdGeometry1,typeInSubElementName1, indexObjectHavingPoint1, indexExtremiteLine1, x1 ,y1)=featuresObjSelected (ActiveSketch, sels[0],0,1) print('(typeIdGeometry1,typeInSubElementName1', typeIdGeometry1,typeInSubElementName1)           if typeInSubElementName1=='Constraint' and len(sels[0].SubElementNames)==1 :                indexConstraint=indexObjectHavingPoint1            elif typeIdGeometry1=='Part::GeomLineSegment' :                (typeIdGeometry2, typeInSubElementName2, indexObjectHavingPoint2, indexExtremiteLine2, x2 ,y2)=featuresObjSelected (ActiveSketch, sels[0],0,2)        if len(sels[0].SubElementNames)== 2: # two obj selected            (typeIdGeometry1,typeInSubElementName1, indexObjectHavingPoint1, indexExtremiteLine1, x1 ,y1)=featuresObjSelected (ActiveSketch, sels[0],0,1)            (typeIdGeometry2,typeInSubElementName2, indexObjectHavingPoint2, indexExtremiteLine2, x2 ,y2)=featuresObjSelected (ActiveSketch, sels[0],1,1)            if typeInSubElementName1 not in ('Vertex', 'RootPoint') or typeInSubElementName2 not in ('Vertex','RootPoint') : QtWidgets.QMessageBox.information(None,"Warning","2 objects are selected but not 2 points .\n"+               "Select 1 line, 2 points or a constraint in a sketch before selecting a cell in the spreadsheet") return
 * 1) Main proceddure
 * 1) Main proceddure

#--   # line or points have been selected have a look if we need to swap points # -     if ((len(sels[0].SubElementNames)== 1 and typeIdGeometry1 in['Part::GeomLineSegment'] )           or (len(sels[0].SubElementNames)== 2 and typeIdGeometry1 in['Part::GeomLineSegment','Part::GeomCircle', 'Part::GeomArcOfCircle', 'Part::GeomPoint'] )) : print('tests si swap') # ask the user what kind of constraint he wants #       form = getConstraintType form.exec_ # is the checkboxSheced ? sheckBoxConstraintConflicState=form.getCheckBoxState if form.choiceConstraint in ('Cancel','') : return myConstraint=form.choiceConstraint # 'DistanceX' or 'DistanceY' or 'Distance' if (myConstraint == 'DistanceX' and x1>x2) or (myConstraint == 'DistanceY' and y1>y2) : indexObjectHavingPoint1,indexObjectHavingPoint2=indexObjectHavingPoint2,indexObjectHavingPoint1 indexExtremiteLine1,indexExtremiteLine2=indexExtremiteLine2,indexExtremiteLine1

# create constraint #=================================   if cellAlias==None : cellExpression= mySpreadSheetName+'.'+cellCode else : cellExpression= mySpreadSheetName+'.'+cellAlias if (len(sels[0].SubElementNames)== 1 and typeIdGeometry1 in['Part::GeomCircle','Part::GeomArcOfCircle'] ) : print('398 indexObjectHavingPoint1',indexObjectHavingPoint1) indexConstraint=mySketch.addConstraint(Sketcher.Constraint('Diameter', indexObjectHavingPoint1, cellContents)) elif typeIdGeometry1!='Constraint' : # no selected constraint, just line or points #create the constraint indexConstraint=mySketch.addConstraint(Sketcher.Constraint(myConstraint , indexObjectHavingPoint1,indexExtremiteLine1,indexObjectHavingPoint2,indexExtremiteLine2, cellContents)) # for all type, set the constraint'formula' (ex : 'spreadSheet.unAlias'   mySketch.setExpression('Constraints['+str(indexConstraint)+']',cellExpression)    # put Sketch window ahead    activateSketchEditingWindow    FreeCADGui.Selection.clearSelection        # FreeCAD.ActiveDocument.recompute        ActiveSketch.recompute    #if Gui.ActiveDocument.getInEdit == Gui.ActiveDocument.Sketch:        #Gui.ActiveDocument.Sketch.doubleClicked

# is ther constraintes conflicts ? if sheckBoxConstraintConflicState : #if App.activeDocument.isTouched: # isTouched is not ok in Daily Freecad if 'Invalid' in mySketch.State : a=QtWidgets.QMessageBox.question(None, "",                "Constraints conflic detected. Cancel constraint ? ",                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if a == QtWidgets.QMessageBox.Yes: mySketch.delConstraint(indexConstraint) FreeCAD.ActiveDocument.recompute

return

if __name__ == '__main__': main procEnd