Macro Sketch Constraint From Spreadsheet/fr: Difference between revisions

From FreeCAD Documentation
(Created page with "Description")
(No difference)

Revision as of 15:18, 14 March 2023

Other languages:

Macro Sketch Constraint From Spreadsheet

Description
Macro which, with a simple click on a spreadsheet cell, adds a length constraint to a line or between 2 points using a spreadsheet cell alias or address (ex. C2).

Macro version: 00.01
Last modified: 2023-02-22
FreeCAD version: All
Download: ToolBar icon
Author: 2cv001
Author
2cv001
Download
ToolBar icon
Links
Macro Version
00.01
Date last modified
2023-02-22
FreeCAD Version(s)
All
Default shortcut
None
See also
None

Description

Work in progress. It works for me. Tell me if anything is wrong.

Macro which, with a simple click on a spreadsheet cell, adds a length constraint to a line or between 2 points using a spreadsheet cell alias or address (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:


To make things even more refined, if, for example, a line is horizontal rather than vertical, when the dialogue box opens, the focus will be on the button for imposing a horizontal constraint. If the line is vertical rather than horizontal, the focus will be on the button to impose a vertical constraint. In both cases, simply press the enter key if you are happy with this choice.


Links

Credits

Thanks to openBrain, mario52 and onekk for their help on the code!
Thanks to Roy043 and David69 for the various reviews and improvements to the wiki.

Script

ToolBar Icon

Code

ver 00.02.3 02/03/2023 by 2cv001 Macro_Sketch_Constraint_From_Spreadsheet.FCMacro


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

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

#=======================================================
# Dialog box
# Ask user which sort of constraint is required
#=======================================================
class getConstraintType(QtGui.QDialog):
    def __init__(self, widgetToFocus=None):
        super(getConstraintType, self).__init__()
        self.widgetToFocus = widgetToFocus
        self.initUI()

    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 = ''

        option1Button.setFocusPolicy(QtCore.Qt.NoFocus)
        option2Button.setFocusPolicy(QtCore.Qt.NoFocus)
        option3Button.setFocusPolicy(QtCore.Qt.NoFocus)
        option4Button.setFocusPolicy(QtCore.Qt.NoFocus)
        
        # set focus to specified widget
        if self.widgetToFocus == 'DistanceX':
            option1Button.setFocus()
        elif self.widgetToFocus == 'DistanceY':
            option2Button.setFocus()
        elif self.widgetToFocus == 'Distance':
            option3Button.setFocus()
        
        
        
        # 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()
  
#=======================================================
# Give the focus to editing sketch window 
# no parameter
# use : activateSketchEditingWindow()
#=======================================================
def activateSketchEditingWindow():
    def searchForNode(tree, childName, maxLevel=0):
        return recursiveSearchForNode(tree, childName, maxLevel, 1)

    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






######################################################################################
# to get necessary values for the constraint
# Parameters :
# sel : selection of objects (a line, 2 points..).
# numOrdreObjSelected if we want the first objetc selected or the second.
# indexExtremite if we want the start point (1) or the end point (2), if exist, of the sel 
# return features of a point
# - typeInSubElementName 
# - indexObjectHavingPoint index of the object having the point (line...)
# - indexExtremiteLine index of the ends (points) of the line (=start point or end point)
# -x,y : coordinates of the point 
#####################################################################################
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

    # 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    
 
    # We selected a circle but for center (two obs selected)
    if typeInSubElementName == 'Edge'and len(sel.SubElementNames)==2:
        indexObjectHavingPoint=numInName-1
        typeIdGeometry=mySketch.Geometry[indexObjectHavingPoint].TypeId
        if typeIdGeometry in  ['Part::GeomCircle'] :
            x=mySketch.Geometry[indexObjectHavingPoint].Location.x
            y=mySketch.Geometry[indexObjectHavingPoint].Location.y 
            indexExtremiteLine=3 # 3 for center

    # We selected a vertex            
    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  
        # we select a vertex Circle (so the center)
        if mySketch.Geometry[indexObjectHavingPoint].TypeId in ['Part::GeomCircle','Part::GeomArcOfCircle'] :  
            x=mySketch.Geometry[indexObjectHavingPoint].Location.x
            y=mySketch.Geometry[indexObjectHavingPoint].Location.y 

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

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


##########################################
# call at end
##########################################
def procEnd():
    activateSketchEditingWindow()


##########################################
# function returning selected objects at GUI level
# =Sketch, SpreadSheet ....
# parameter : 
# '' = no filter
# 'Spreadsheet::Sheet' for spreadsheets only
# 'Sketcher::SketchObject' for sketches etc...
# output: an array of sketch objects, spreadsheets etc.
##########################################
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
        

##########################################
# Main proceddure
##########################################
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 :
    
        # only one obj selected    
        #------------------------
        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)
                    
        # two obj selected    
        #------------------------ 
        if len(sels[0].SubElementNames)== 2: # two obj selected
            (typeIdGeometry1,typeInSubElementName1, indexObjectHavingPoint1, indexExtremiteLine1, x1 ,y1)=featuresObjSelected (ActiveSketch, sels[0],0,1)
            
            print('(typeIdGeometry1,typeInSubElementName1, indexObjectHavingPoint1, indexExtremiteLine1, x1 ,y1)',(typeIdGeometry1,typeInSubElementName1, indexObjectHavingPoint1, indexExtremiteLine1, x1 ,y1))
            
            (typeIdGeometry2,typeInSubElementName2, indexObjectHavingPoint2, indexExtremiteLine2, x2 ,y2)=featuresObjSelected (ActiveSketch, sels[0],1,1)
            print(' 368 typeInSubElementName1',typeInSubElementName1)
            print(' 368 typeIdGeometry1',typeIdGeometry1)            
            if ((typeInSubElementName1 not in ('Vertex' , 'RootPoint') or typeInSubElementName2 not in ('Vertex','RootPoint'))
                    and not(typeIdGeometry1 == 'Part::GeomCircle' and typeInSubElementName1 in ['Edge'])) : 
                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
            # to do : if one is a circle, reolace with it's center.
            #if typeIdGeometry1 in ('Part::GeomCircle', 'Part::GeomArcOfCircle')
                
                
    

  
    #--------------------------------------
    # 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'] )) :

        # ask the user what kind of constraint he wants
        #------------------------------------
        # to give focus on the good button 
        # (Button DistanceX if the two points are more horizontal than vertical)
        if abs(x1-x2) > abs(y1-y2) : 
            buttonHavingFocus='DistanceX'
        else :
            buttonHavingFocus='DistanceY'
        form = getConstraintType(buttonHavingFocus)
        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.touch()    
    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()