Macro pour obtenir un stl avec des arrondis parfaits et piloter octoprint

Forum destiné aux questions et discussions en français
Forum rules
Be nice to others! Read the FreeCAD code of conduct!
Post Reply
User avatar
flachyjoe
Veteran
Posts: 1891
Joined: Sat Mar 31, 2012 12:00 pm
Location: Limoges, France

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by flachyjoe »

2cv001 wrote: Tue Dec 15, 2020 1:01 pm mw ne doit pas correspondre à la bonne fenêtre donc
C'est surtout que l'argument de QtGui.QDialog.__init__ sert à indiquer où dessiner les contrôles. Dans ton cas, celui d'une boite de dialogue, c'est la fenêtre juste créée qui les reçoit donc il ne faut pas le renseigner. Là tu demandes de dessiner sur la fenêtre principale de FreeCAD.
- Flachy Joe -
Image
openBrain
Veteran
Posts: 9041
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by openBrain »

flachyjoe wrote: Tue Dec 15, 2020 1:21 pm C'est surtout que l'argument de QtGui.QDialog.__init__ sert à indiquer où dessiner les contrôles. Dans ton cas, celui d'une boite de dialogue, c'est la fenêtre juste créée qui les reçoit donc il ne faut pas le renseigner. Là tu demandes de dessiner sur la fenêtre principale de FreeCAD.
Non, le paramètre indique le parent du widget, donc c'est correct. Le problème vient d'ailleurs. ;)
2cv001 wrote: Tue Dec 15, 2020 1:01 pm D'une façon générale, j'avance dans les modifs, suite à ton message.

Pour ce point, un truc rigolo :

si je retire le mw et donc si je fais à la fin ceci
Là ça marche. mw ne doit pas correspondre à la bonne fenêtre donc
Ce que tu as fait est correct, cela révèle juste un problème sous-jacent (que j'ai aussi levé) sur la façon dont tu gères tes widgets.
Même si tu as l'impression d'empirer les choses, tu avances dans le bon sens.

J'espère que je vais arriver à expliquer clairement :
* Dans ce que tu avais avant, ton dialogue n'avait pas de parent et Qt le considérait donc comme une fenêtre "à part". Du coup il lui applique un fond.
* Avec la nouvelle méthode, la fenêtre principale de FC est parent de ton dialogue. Du coup Qt par défaut met un fond transparent
Comme tu crées tout un paquet de widgets indépendants que tu bouges manuellement sur le canevas, l'espace entre les widgets n'est pas rempli et tu vois en fait le fond.

La bonne façon de régler le problème est d'améliorer ta gestion des widgets.

Aujourd'hui tu as :

Code: Select all

_ Dialogue
 \_ Widget1
 \_ Widget2
 \_ etc...
Ce que tu devrais avoir c'est :

Code: Select all

_ Dialogue
 \_ Widget (un simple QWidget sans rien d'autre) -- ce widget est optionnel car le layout devrait remplir le fond, mais c'est une bonne habitude
   \_ QGridLayout
     \_ Widget1
     \_ Widget2
     \_ Widget3
Tu peux regarder cet exemple très simple (aussi basé sur un QDialog) pour voir comment les widgets sont ajoutés (d'ailleurs, je n'avais pas mis le widget optionnel en tête :oops:)
User avatar
2cv001
Posts: 484
Joined: Wed Jan 01, 2020 9:30 am

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by 2cv001 »

mario52 wrote: Mon Dec 14, 2020 8:44 pm Bonsoir

ici pour mettre un icône dans un bouton et un toolTip (avec image dans un fichier):

Code: Select all

        cancelButton.setIcon(QtGui.QIcon("completePath/nomIcone.png"))	# sur un bouton

        cancelButton.setToolTip("Cancel <img src= completePath/nomIcone.png" + " />")	# dans un toolTip

ToolTipIcon00.png

mario
Pas mal aussi ! Merci !
Là, ça oblige l'utilisateur qui télécharge la macro à télécharger aussi des fichiers images et de les installer au bon endroit. N' y a t il pas des icônes prédéfinies inclues dans python ou FreeCAD de façon à ne pas avoir cet inconvénient ?
Macro Sketch Constraint From Spreadsheet :
https://wiki.freecad.org/Macro_Sketch_C ... adsheet/fr
openBrain
Veteran
Posts: 9041
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by openBrain »

2cv001 wrote: Wed Dec 16, 2020 12:11 pm Là, ça oblige l'utilisateur qui télécharge la macro à télécharger aussi des fichiers images et de les installer au bon endroit. N' y a t il pas des icônes prédéfinies inclues dans python ou FreeCAD de façon à ne pas avoir cet inconvénient ?
Tu peux utiliser le format XPM pour créer des icônes intégrés dans le code. ;)
User avatar
2cv001
Posts: 484
Joined: Wed Jan 01, 2020 9:30 am

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by 2cv001 »

Merci openBrain pour les remarques !!!

Le nouveau code complet est à la fin de ce message.
openBrain wrote: Mon Dec 14, 2020 5:47 pm

.....................

Voili voilou
Comme d'hab, je fais les remarques en vrac. Je vais essayer de mettre les numéros de ligne.
* L29-30 : tu ne dois pas utiliser PySide et PySide2 en même temps. C'est un peu compliqué à expliquer, mais en version simple, PySide permet d'être compatible avec les vieilles versions FreeCAD. Concrètement, dans PySide les widgets sont dans QtGui, alors que dans PySide2 ils sont dans un namespace séparé nommé QtWidgets. Il faut que tu choisisses entre les 2.
Bon ça, j'ai pas encore attaqué. J'ai cherché, pais pas vraiment trouvé ce qu'il faut faire. A suivre.
* Comme tu utilises maintenant le GUI, il faudrait ajouter 'import FreeCADGui as Gui' dans les imports
Bon ça s'est facile. C'est fait


* L134 : c'est important dans Qt de respecter au mieux la parenté. Il faudrait donc que ta fonction init soit '__init__(self, parent=None)', puis la ligne du dessous : 'super(BoiteDialogueSTLGuiClass, self).__init__(parent)'. Ensuite en L457, tu instancies avec 'BoiteDialogueSTLGuiClass(mw)'
fait

Code: Select all

class BoiteDialogueSTLGuiClass(QtGui.QDialog):

    def  __init__(self, parent=None):
        super(BoiteDialogueSTLGuiClass, self).__init__(parent)
        self.initUI()
        
        ............
        
        
        
* L207-212 : pour les boutons OK/Cancel, on utilise plutôt une QDialogButtonBox qui permet d'afficher facilement les boutons standards de la manière traditionnelle de l'OS (et traduits), et on connecte simplement le signal 'accepted' au slot 'accept' du dialogue, et 'rejected' à 'reject'
* L458-459 : si tu utilises les boutons standard, tu peux utiliser le retour de 'exec_()' pour savoir ce qu'il s'est passé (documentation), ce qui fait que tu as juste à faire 'if not formBoiteDialogueSTL.exec_(): return' et tu n'as pas à gérer toi même la valeur de retour
J'ai fait cela :

Code: Select all

        cancelButton = QtGui.QDialogButtonBox (self)
        cancelButton.clicked.connect(self.onCancel)
       # cancelButton.setAutoDefault(True)
        cancelButton.setStandardButtons(QtGui.QDialogButtonBox.Cancel)
        lay.addWidget(cancelButton, posLigne+1, 1)
        # OK button
        okButton = QtGui.QDialogButtonBox (self)
        okButton.clicked.connect(self.onOk)
        okButton.setStandardButtons(QtGui.QDialogButtonBox.Ok)
        lay.addWidget(okButton, posLigne+1, 2)
        
Ca s'affiche correctement :
Capture.JPG
Capture.JPG (24.86 KiB) Viewed 1023 times
Par contre, mon exe

Code: Select all

    formBoiteDialogueSTL =BoiteDialogueSTLGuiClass(mw)
    print(formBoiteDialogueSTL.exec_())
me retourne toujours 0 quelque soit le bouton appuyé
* Pour ce qui est de l'affichage des widgets, tu devrais passer par les layouts (notamment dans ton cas QGridLayout). Ca permet aux éléments d'être toujours bien placés, et ça t'évite d'avoir à gérer toi-même la taille de la fenêtre ou la position avec les 'move()' partout...
: fait. Ca marche bien !
* L143 : le 'WindowsStayOnTopHint' n'a pas de raison d'être. En associant ton dialogue avec la mainwindow (voir au-dessus) et en utilisant la fonction 'exec_()', ton dialogue sera toujours au-dessus de la fenêtre FreeCAD. Là ce que tu rajoutes fait que ton dialogue sera au-dessus de toutes les fenêtres ouvertes (genre le navigateur, ...) ce qui est plus gênant qu'utile.
Tout à fait.
Fait
* L147+152+162 : les connexions signaux/slots ne servent pas à grand-chose à part polluer la vue rapport. Je pense que tu peux les supprimer.
mince, comme j'ai changé le code depuis ta version et je n'avais pas sauvé précisément la version en question, je ne sais pas à quoi ça correspond. Tu pourrais me donner les lignes ? soit les recopier, soit les num de ligne du code complet mies en bas de ce message ?
* L156 : :evil: Horreur ! La variable 'commandes' est une liste, donc la boucle for s'écrit simplement 'for cmd in commandes' et ensuite tu utilises le pointeur'cmd' pour récupérer les éléments
* L416 : :evil: Rehorreur ! voir plus haut
Tout à fait d'accord, c'est beaucoup plus propre ainsi.
Fait
* L182 : un QLineEdit n'est pas forcément le mieux pour récupérer un float. Il faut à minima lui mettre un Validator (voir ce lien ou un inputMask (comme tu as fait)), mais moi j'utiliserais plutôt un QDoubleSpinbox
trés bien le QDoubleSpinbox
Fait
* L200 : ne pas mettre 'self.show()' dans le créateur (sauf raison bien particulière). De toute façon tu utilises 'exec_()' plus loin, donc ça affichera ton dialogue
Fait
* L468 : comme ta macro gère tous les types d'objet, tu devrais remplacer 'body' par 'solide'
Fait
* L470 : je ne vois pas à quoi sert cette messagebox
C'est une bétise : je l'avais mis pour débugger
Fait

Le code actuel :

Code: Select all

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Macro qui permet de créer un fichier "stl" avec des arrondis parfaits
= sans facettes visibles.
Elle permet aussi de lancer des programmes de votre choix
Par exemple pour automatiser la cheine FreeCAD -> Slicer -> impression

Principe du lissage : elle modifie la propriété deviation des solide avant
génération du stl puis replace les anciennes valeures
A la fin, elle propose de lancer le fichier stl qui s'ouvrira par exemple sous
cura si l'extension stl a été associé à cura dans votre système d'exploitation.

Lancement d'autres programmes ou commande :
Vous pouvez lui demander tout programme ou commande que vous pourriez taper 
dans un terminal. Pour cela, vous devez modifier ce qu'il y a dans la section
"paramètres pouvant être changés". 
Exemples d'applications : 
- Allumer l'imprimante et la lumière (nécessite par exemple une prise commandée)
- Connecter octoprint à l'imprimante
- Lancer la préchauffe du plateau
- Sauvegarder votre fichier FreeCAD et stl
....

Fonctionne sous windows. Testé ausi sous Linux 
"""
import FreeCAD as App
import Mesh
from PySide2 import QtWidgets
from PySide import QtGui, QtCore
import os, sys, subprocess
import time

#================================
# Paramètres pouvant être changés
#================================

# Gestion du lissage
#--------------------

# deviation : valeur par défaut qui s'affichera dans la boite de dialogue. 
# 0.5 par défaut dans FreeCad, 0.05 permet un lissage plus fort.
# 0.01 est parfait du point de vue lissage. 
# plus la valeur est faible et plus la qualité est bonne, 
# mais plus la taille du fichier stl est grande
# la valeur doit être comprise entre 0 et 1
deviation=0.01 
doitLancerFichier=True # si True, il sera proposer de lancer le fichier stl
delaiMax=10 # si l'utilisateur entre un délai plus grand, il est réduit à delaiMax

# lancement automatique de programmes,
# Automatisation de la chaine de production
#------------------------------------------

#Pour lancer des programmes de votre choix
    #Typiquement par exemple un programme domotique, 
    #allumer votre imprimante ou/et une lumière.
#Utilisez la syntaxe suivante en ajoutant une ligne dans commandes=....:
#  [ ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',temps_attente ],
#
#  'commande à exécuter' : la commande que vous voulez exécuter. 
#  'parame 1, param 2,...' : si votre programme a besoin de paramètres. 
#  Ces paramètres ne serons pris en compte que si extention=''
#
#  'extention': si non vide alors le premier paramètre sera remplacé par le nom de 
#     votre fichier FreeCAD mais avec cette extension :
# 'question à poser' : la question à afficher dans la boite de dialogue
#  temps_attente : facuktatif : nb de seconde à attendre après exécution
#    permet par exemple d'attendre que l'imprimante ait démarré avant de lui 
#    demander de chauffer le plateau

# A noter les doubles \ en remplacement de \ (car les caractères spéciaux
# doivent être précédés d'un \ en python). idem pour par exemple l'apostrophe
# les lignes 
#[ ['commande à exécuter','param 1','param 2'  ] ,'.extension','Question à poser'  ],
# doivent être insérées plus bas après commande=[
#
# Exemples de lancements possibles :

"""
[ ['calc.exe'                                         ] ,''    ,'Lancer la calculatrice ?'],
[ ['C:\\WINDOWS\\system32\\mspaint.exe','dessin.jpg'  ] ,''    ,'Lancer Paint ?'          ],
[ ['C:\\Program Files\\Ultimaker Cura 4.8.0\\Cura.exe'] ,'.stl','Lancer Cura ?'           ],
[ ['curl','http://pidomotique/connecte.php'           ] , ''   ,'Connecter l\'imprimante ?'],
[ ['C:\\Program Files\\Mozilla Firefox\\firefox.exe','https://octopi.local'],'','Lancer Octoprint ?'],
 pour linux:
[ [   'firefox','https://octopi.local'                ] , ''   ,'Lancer Octoprint ?'      ], 

Exemple de pilotage de l'imprimante via octoprint (si vous l'avez...) :
   mise en température du plateau
- Remplacez les XXX se trouvant après X-Api-Key: par votre clef API
(que l'on trouve dans les paramètres d'octoprint)
- Remplacez http://octopi.local par l'url avec laquelle vous accédez à octoprint
- Modifiez la valeur 050 dans "M140 S050" : S050 pour chauffer à 50° si vous souhaitez 60° : S060
- 6 lignes à recopier :
  [
      [  'curl','-H', 'Content-Type: application/json','-H', 'X-Api-Key: XXXXXXXXXXXXXXXXXXXXX', '-X', 'POST',
            '-d {"command":"M140 S050"}','http://octopi.local/api/printer/command'
      ]
            , '','Chauffer le plateau de l\'imprimante ?'
  ],

"""


commandes=[
# insérez ici vos lignes.
  [ ['calc.exe'                                         ] ,''    ,'Lancer la calculatrice ?',2],
  [ ['C:\\Program Files\\Ultimaker Cura 4.8.0\\Cura.exe'] ,'.stl','Lancer Cura ?'           ],
  [  ['curl','http://pidomotique/connecte.php'              ] , ''   ,'Allumer et connecter l\'imprimante ?',4],
  [ ['C:\\Program Files\\Mozilla Firefox\\firefox.exe','https://octopi.local'],'','Lancer Octoprint ?'],
  [
      [  'curl','-H', 'Content-Type: application/json','-H', 'X-Api-Key: xxxxxxxxxxxxxx', '-X', 'POST',
            '-d {"command":"M140 S050"}','http://octopi.local/api/printer/command'
      ]
            , '','Chauffer le plateau de l\'imprimante ?'
  ],

         ]



#===============
#Initialisations
#===============
dictionnaireOrigineDeviation={}
mw = Gui.getMainWindow()
# Constant definitions
userCancelled= "Cancelled"
userOK= "OK"

indiceCommandeEtParam=0
indiceExtFileNameParamCommandeALancer=1
indiceTextAutreCommandeALancer=2
indiceDelai=3

# UI Class definitions



class BoiteDialogueSTLGuiClass(QtGui.QDialog):

    def  __init__(self, parent=None):
        super(BoiteDialogueSTLGuiClass, self).__init__(parent)
        self.initUI()

    def initUI(self):
        self.result = userCancelled
        self.tabCheckboxTextCommandes=[] # tab des checkbox commandes à lancer
        self.setGeometry( 250, 250, 400, 150)
        self.setWindowTitle("Titre de la fenêtre")
        lay = QtGui.QGridLayout(self) 

        # checkboxes
        self.checkboxGenererSTL = QtGui.QCheckBox("Générer le STL", self)
        self.checkboxGenererSTL.clicked.connect(self.onCheckboxGenererSTL)
        self.checkboxGenererSTL.setCheckState(QtCore.Qt.Checked)# mise à checked
        lay.addWidget(self.checkboxGenererSTL, 0, 0) 
        if doitLancerFichier :
            self.checkboxLancerSTL = QtGui.QCheckBox("Lancer le slicer", self)
            self.checkboxLancerSTL.clicked.connect(self.onCheckboxLancerSTL)
            self.checkboxLancerSTL.setCheckState(QtCore.Qt.Checked)
            lay.addWidget(self.checkboxLancerSTL, 2, 0) 

        posLigne=3
        for cmd in commandes :
           #decalPositionY=decalPositionY+20
           textCommande=cmd[indiceTextAutreCommandeALancer]
           self.checkboxTextCommande = QtGui.QCheckBox(textCommande, self)
           # mémorisation de cette checbox dans un tableau :
           self.tabCheckboxTextCommandes.append(self.checkboxTextCommande)
           self.checkboxTextCommande.clicked.connect(self.onCheckboxGenererSTL)
           self.checkboxTextCommande.setCheckState(QtCore.Qt.Checked)# mise à checked
           lay.addWidget(self.checkboxTextCommande, posLigne, 0) 
           posLigne=posLigne+1


        # numeric input field
        self.label2 = QtGui.QLabel('Précision (param deviation) \nentre 0.01 et 1\n'+
             '0.01 pour une grande qualité', self)
        lay.addWidget(self.label2, 0, 1) 

        self.precision = QtGui.QDoubleSpinBox(self)
        self.precision.setValue(deviation)
        self.precision.setRange(0.01,1)
        self.precision.setSingleStep(0.01)
        self.precision.setToolTip('Plus le chiffre est grand, plus la taille ' +
            'du fichier est grande \net meilleur est la précision.\n'+
            'Voir le paramètre deviation dans FreeCAD')
        lay.addWidget(self.precision, 0, 2) 

        cancelButton = QtGui.QDialogButtonBox (self)
        cancelButton.clicked.connect(self.onCancel)
       # cancelButton.setAutoDefault(True)
        cancelButton.setStandardButtons(QtGui.QDialogButtonBox.Cancel)
        lay.addWidget(cancelButton, posLigne+1, 1)
        # OK button
        okButton = QtGui.QDialogButtonBox (self)
        okButton.clicked.connect(self.onOk)
        okButton.setStandardButtons(QtGui.QDialogButtonBox.Ok)
        lay.addWidget(okButton, posLigne+1, 2)
        #okButton = QtGui.QPushButton('OK', self)
        #okButton.clicked.connect(self.onOk)
        #lay.addWidget(okButton, posLigne+1, 2)

    def onCheckboxGenererSTL(self):
        print('onCheckboxGenererSTL')
    def onCheckboxLancerSTL(self):
        print('onCheckboxLancerSTL')

    def onCancel(self):
        self.result         = False
        print('On cancel')
        self.close()
    def onOk(self):
        print('onOK')
        self.result         = True
        self.close()

    



#===============
#Fonctions
#===============
def octopi(api,commande,urlOctopi,apiKey) :
    """
    Fonction qui renvoie le paramètre à passer à subprocess.Popen si on veut lancer
    une AI octopi. Non utilisez ici, mais ça peut servir

    Arguments : 
        api l'api à exécuter  ex : '/api/printer/command'
        commande : la commande de l'API en question ex: '{"command":"M140 S050"}'
        urlOctoprint : l'url de votre octopi ex : 'https://octopi.local'
        apiKey : la clef API que vous trouvez dans setting/API d'octoprint
    Returns :

    Example
       API lanant un gcode. Ici la chauffe du plateau de l'imprimante
       subprocess.Popen(
                          octopi(
                                '/api/printer/command',
                                '{"command":"M140 S050"}',
                                'https://octopi.local',
                                'XXXXKEYXAPIXXXXXXXX'))
                 ,close_fds=True)
    """
    commandeCurl=[
    'curl','-H', 'Content-Type: application/json','-H',
         'X-Api-Key: '+apiKey,
         '-X', 'POST',
        '-d '+commande,urlOctopi+api
    ]
    return commandeCurl


def nameFileStl(dictionnaireOrigineDeviation):
    """
    Fonction qui renvoie le nom de fichier compris chemin
    où sera enregistré le stl
    Le document contenant les objets du dictionaire doit être ouvert et
    sauvé

    Arguments : 
        dictionnaireOrigineDeviation  (dictionary) : 
            un dictionnaire dont les clefs sont des solide. 
            le nom de fichier sera obtenu à partir du nom de fichier 
            du document contenant le premier objet (solide) de ce
            dictionnaire

    Returns :
        '' si le document n'existe pas,
        sinon le nom du fichier où sera enregistré le stl

    Example
        nomFichierStl=nameFileStl(dictionnaireOrigineDeviation)
    """

    doc=list(dictionnaireOrigineDeviation.keys())[0].Document
    if doc is None:
        QtWidgets.QMessageBox.information(mw,'Attention',\
            "Vous n'avez pas de document ouvert")
        return ''
    if doc.FileName=='':
        QtWidgets.QMessageBox.information(mw,
             'Attention','Sauvez votre document avant de relancer la commande') 
        return ''
    return(os.path.splitext(doc.FileName)[0]+'.stl')

def memoriseObjEtDeviation(selection,deviationImpose):
    """
    Mémorise les solide qui sont contenus dans selection et leur
    propriété deviation et remplace cette dernière par la valeur 
    de deviationImpose
    Elle impose un recompute ultérieur par un touch.
    Recompute() doit être éventuellement lancé après

    Arguments :
        selection : une sélection d'objets
        deviationImpose (float) : valeur que l'on veut imposer à deviation

    Returns: 
        un dictionnaire contenant l'objet en key et la déviation en value
        Renvoie {} s'il n'y a pas d'objet adéquate dans la sélection

    Example:
        memoriseObjEtDeviation(Gui.Selection.getSelection(),deviation)
    """
    dicoObjDeviation={}
    for objData in selection :
        if objData.isDerivedFrom('Part::Feature') \
          and not objData.isDerivedFrom('PartDesign::Feature')\
          and not objData.isDerivedFrom('Part::Part2DObject'):
            dicoObjDeviation[objData]=objData.ViewObject.Deviation
            objData.ViewObject.Deviation=deviationImpose 
           # pour imposer un recompute même si la déviation est plus petite
           # qu'avant :
            objData.touch()
            for o in objData.ViewObject.claimChildren():
                    o.touch()
    return dicoObjDeviation

def restitueDeviation(dicoOrigineDeviation):
    """
    Fonction qui remplace la propriété deviation dans les objets
    Elle impose un recompute par un touch. recompute() doit être
    lancé après

    Arguments:
        dicoOrigineDeviation un dictionnaire contenant l'objet en key 
        et la valeur à imposer dans la propriété déviation de l'objet
        en value

    Returns: Ne retourne rien

    Example:
        restitueDeviation(dicoOrigineDeviation)

    """
    for ob2 in dicoOrigineDeviation:
        ob2.ViewObject.Deviation=dicoOrigineDeviation[ob2]
        # pour imposer un recompute même si la déviation est plus petite
        # qu'avant :
        ob2.touch()
        for o in ob2.ViewObject.claimChildren():
            o.touch()

def openFile(fileName):
    """
    fonction qui lance un fichier en fonction du système d'exploitation
    vérifié pour l'instant uniquement sous windows

    Arguments:
        fileName (string) le nom du fichier à lancer

    Returns
        Ne retourne rien

    Example: 
        openFile(unfichier.stl)
        openFile(calc.exe)
    """
    if sys.platform == "win32":
        os.startfile(fileName)
    else:
        opener ="open" if sys.platform == "darwin" else "xdg-open"
        #subprocess.call([opener, fileName]) #le problème avec call est que freecad est bloqué en attendant que l'on sorte de l'application
        subprocess.Popen([opener, fileName],close_fds=True) 

def recalcul(dicoObjs):
    """
    lance un recompute pour chaque Document des objets du dico
    seulement si cela n'a pas déjà été fait
    Arguments:
        dicoObjs un dictionnaire contenant l'objet en key 

    Returns
        Ne retourne rien

    Example:
        recalcul(dicoMesObjets)
    """
    docDejaRecalcul=[]
    for obj in dicoObjs:
        if obj.Document not in docDejaRecalcul:
            #print('recompute {}'.format(obj.Document.Name))
            obj.Document.recompute()
            docDejaRecalcul.append(obj.Document)

def lanceCommmandes(commandesAlancer,fileStlName,formBoiteDialogueSTL):
    """
    lance les commmandes
    Arguments:
        commandesAlancer sous la forme d'un tableau de tableaux
        [ ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',délai ],
          ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',délai ],
          ...
        ]

         'commande à exécuter' : la commande que vous voulez exécuter. 
         Par exemple 'monProgramme.exe' si besoin mettre aussi son chemin. 
         'param 1, param2... ' : si votre programme a besoin de paramètres. 
            Ne sera pris en compte que si extention=''
         'extention': si non vide, le paramètre sera le nom de votre
              fichier FreeCAD mais avec votre extension :
         'question à poser' : la question à afficher dans la boite de dialogue
         délai : temps d'attente en secondes avant de passer à la commande suivante.

    Returns
        Ne retourne rien

    Example:
            lanceCommmandes(commandes,fileStlName) # lance les commandes que l'on a décrites dans commandes
    """


    #for i in range(len(formBoiteDialogueSTL.tabCheckboxTextCommandes)) :
    for tbktc in formBoiteDialogueSTL.tabCheckboxTextCommandes :
#        if formBoiteDialogueSTL.tabCheckboxTextCommandes[i].isChecked() :
        if tbktc.isChecked() :
            #commande=commandesAlancer[i]
            i= formBoiteDialogueSTL.tabCheckboxTextCommandes.index(tbktc)
            commande=commandesAlancer[i] ################################## PAS TOP : à reconsidérer
            #print('{}'.format(commande))
            commandeEtParam=commande[indiceCommandeEtParam]
            extFileNameParamCommandeALancer=commande[indiceExtFileNameParamCommandeALancer]
            textAutreCommandeALancer=commande[indiceTextAutreCommandeALancer]
            delai=0
            try :
               if commande[indiceDelai:indiceDelai+1]!=[] : #si l'utilisateur a rentré un délai
                  delai=commande[indiceDelai]
            except :
               print ('Délai non pris en compte car valeur numérique non valide'+
                      'pour la commande '+ textAutreCommandeALancer)
            if delai > delaiMax :
                delai=delaiMax

            if extFileNameParamCommandeALancer!='':     
                if commandeEtParam[2:3]==[]  : # Il n'y a pas de paramètre.
                   # On ajoute un élément pour y mettre notre nom de fichier
                   # avec notre extension.
                   commandeEtParam.append('')
                commandeEtParam[1]=   os.path.splitext(fileStlName)[0]+  extFileNameParamCommandeALancer
            subprocess.Popen(commandeEtParam,close_fds=True)
            time.sleep(delai)# Pause de durée delai secondes   


def run(objs=Gui.Selection.getSelection(), dev=deviation):
    """
    fonction principale
    Arguments:
        doc : le document actif
        objs : les objets que l'on a au préalable sélectionnés
        dev : la valeur de la propriété déviation qui sera 
              imposée avant la génération du stl

    Returns
        Ne retourne rien

    Example:
        run()
    """



    # Boite de dialogue de ce que l'on veut faire
    #formBoiteDialogueSTL = BoiteDialogueSTLGuiClass()
    formBoiteDialogueSTL =BoiteDialogueSTLGuiClass(mw)
    print(formBoiteDialogueSTL.exec_())
    if not formBoiteDialogueSTL.result : return
    dev=formBoiteDialogueSTL.precision.value()

    # on mémorise les  solide sélectionnés et leur déviation,
    # on fait un touch() de leur child pour prise en compte dans recompute :
    dictionnaireOrigineDeviation=memoriseObjEtDeviation(objs,dev)
    if len(dictionnaireOrigineDeviation)==0: # si on a trouvé aucun solide
        QtWidgets.QMessageBox.information(mw, 'Attention',\
            'Sélectionnez un ou plusieurs solide avant de lancer la macro')
        return
    # on récupère le nom du fichier où l'on devra enregistrer le stl
    fileStlName=nameFileStl(dictionnaireOrigineDeviation)
    if fileStlName == '' :
        return

    # l'export lui-même. On passe en paramètre les objets solide qui 
    # sont dans dictionnaireOrigineDeviation
    if formBoiteDialogueSTL.checkboxGenererSTL.isChecked():
       Mesh.export(list(dictionnaireOrigineDeviation.keys()), fileStlName) 
       print('Export fait')
    # restitution de la propriété deviation d'origine pour les solide :
    restitueDeviation(dictionnaireOrigineDeviation)
    recalcul(dictionnaireOrigineDeviation)

    if formBoiteDialogueSTL.checkboxLancerSTL.isChecked():
            #lance par exemple cura si cura a été associé aux fichiers *.stl
            openFile(fileStlName)

    lanceCommmandes(commandes,fileStlName,formBoiteDialogueSTL) # lance les commandes que l'on a décrites dans commandes


if __name__ == '__main__':
    run()



Macro Sketch Constraint From Spreadsheet :
https://wiki.freecad.org/Macro_Sketch_C ... adsheet/fr
openBrain
Veteran
Posts: 9041
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by openBrain »

Je supprime du post tout ce qui est pris en compte sinon ce sera illisible.
Mais déjà bravo pour le travail. Je sais ce que ça représente comme effort, et c'est des fois un peu ennuyeux d'avoir l'impression de faire plusieurs fois le boulot, ou de faire des choses qui ne changent rien pour l'utilisation. ;)
2cv001 wrote: Wed Dec 16, 2020 4:51 pm
Comme d'hab, je fais les remarques en vrac. Je vais essayer de mettre les numéros de ligne.
* L29-30 : tu ne dois pas utiliser PySide et PySide2 en même temps. C'est un peu compliqué à expliquer, mais en version simple, PySide permet d'être compatible avec les vieilles versions FreeCAD. Concrètement, dans PySide les widgets sont dans QtGui, alors que dans PySide2 ils sont dans un namespace séparé nommé QtWidgets. Il faut que tu choisisses entre les 2.
Bon ça, j'ai pas encore attaqué. J'ai cherché, pais pas vraiment trouvé ce qu'il faut faire. A suivre.
C'est un point un peu complexe. Dans un 1er temps, le plus simple AMHA est d'enlever l'import de PySide2, et de remplacer 'QtWidgets' par 'QtGui' dans toute la macro. Après on verra si tu veux repasser complètement en PySide2.
* L207-212 : pour les boutons OK/Cancel, on utilise plutôt une QDialogButtonBox qui permet d'afficher facilement les boutons standards de la manière traditionnelle de l'OS (et traduits), et on connecte simplement le signal 'accepted' au slot 'accept' du dialogue, et 'rejected' à 'reject'
* L458-459 : si tu utilises les boutons standard, tu peux utiliser le retour de 'exec_()' pour savoir ce qu'il s'est passé (documentation), ce qui fait que tu as juste à faire 'if not formBoiteDialogueSTL.exec_(): return' et tu n'as pas à gérer toi même la valeur de retour
J'ai fait cela :
Pas bon. :) Un QDialogButtonBox est un container qui peut déjà contenir tous les boutons.
Tu devrais avoir quelquechose du genre (je n'ai pas forcément pris les bon noms de variables) :

Code: Select all

butBox = QtGui.QDialogButtonBox(dialog)
butBox.setOrientation(QtCore.Qt.Horizontal)
butBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
layout.addWidget(butBox) #ligne à modifier en fonction du layout utilisé)
butBox.accepted.connect(self.accept)
butBox.rejected.connect(self.reject)
* L147+152+162 : les connexions signaux/slots ne servent pas à grand-chose à part polluer la vue rapport. Je pense que tu peux les supprimer.
mince, comme j'ai changé le code depuis ta version et je n'avais pas sauvé précisément la version en question, je ne sais pas à quoi ça correspond. Tu pourrais me donner les lignes ? soit les recopier, soit les num de ligne du code complet mies en bas de ce message ?
Maintenant ce sont les lignes 156+161+172
User avatar
2cv001
Posts: 484
Joined: Wed Jan 01, 2020 9:30 am

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by 2cv001 »

openBrain wrote: Wed Dec 16, 2020 5:15 pm
.........
Mais déjà bravo pour le travail. Je sais ce que ça représente comme effort, et c'est des fois un peu ennuyeux d'avoir l'impression de faire plusieurs fois le boulot, ou de faire des choses qui ne changent rien pour l'utilisation. ;)
Merci !
Effort, effectivement, ça m'oblige à aller fouiller sur internet afin de tenter de comprendre. Le problème des tutos ou exemples que l'on y trouve, est qu'ils donnent une solution, pas toujours la meilleur donc, mais ils n'expliquent pas vraiment. Ou alors je n'ai pas trouvé le bon (je pense à psyde2...=) Mais en même temps, c'est un jeu dans lequel le but du jeu est d'y arriver. Et apprendre, c'est du plaisir donc...
C'est un point un peu complexe. Dans un 1er temps, le plus simple AMHA est d'enlever l'import de PySide2, et de remplacer 'QtWidgets' par 'QtGui' dans toute la macro. Après on verra si tu veux repasser complètement en PySide2.
Fait et donc en restant en Pyside.
J'ai du coup ce message dont on avait déjà parlé comme étant "normal" :

07:33:13 C:/Datas/data/Imprimante 3D/Freecad/ExportStlV12.FCMacro:205: DeprecationWarning: an integer is required (got type PySide2.QtWidgets.QDialogButtonBox.StandardButton). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python.
butBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
Bizarre d’ailleurs que dans ce message il fasse référence à pyside2.
Tu devrais avoir quelque chose du genre (je n'ai pas forcément pris les bon noms de variables) :

Code: Select all

butBox = QtGui.QDialogButtonBox(dialog)
butBox.setOrientation(QtCore.Qt.Horizontal)
butBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
layout.addWidget(butBox) #ligne à modifier en fonction du layout utilisé)
butBox.accepted.connect(self.accept)
butBox.rejected.connect(self.reject)
Fait
IL a juste fallu que je remplace butBox = QtGui.QDialogButtonBox(dialog) par butBox = QtGui.QDialogButtonBox(self) car il ne connaissait pas 'dialog' mais ça marche maintenant. Je pense que ce qu'il manquait principalement, c'est le connect(self.accepted) et le butBox.rejected.connect(self.reject). Et le fait de "mixer" les deux boutons grâce au | dans s(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
* L147+152+162 : les connexions signaux/sl. Faitots ne servent pas à grand-chose à part polluer la vue rapport. Je pense que tu peux les supprimer.
Maintenant ce sont les lignes 156+161+172
Effectivement. Fait

* L458-459 : si tu utilises les boutons standard, tu peux utiliser le retour de 'exec_()' pour savoir ce qu'il s'est passé (documentation), ce qui fait que tu as juste à faire 'if not formBoiteDialogueSTL.exec_(): return' et tu n'as pas à gérer toi même la valeur de retour
Maintenant ça marche :D Fait

Un grand merci openBrain pour la mise à disposition de ton expertise !
Ci-dessous le nouvau source. Je me donne encore un temps pour relire etc. puis le le mettrai à la place du fichier joint dans le premier message de ce sujet.

Code: Select all

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Macro qui permet de créer un fichier "stl" avec des arrondis parfaits
= sans facettes visibles.
Elle permet aussi de lancer des programmes de votre choix
Par exemple pour automatiser la cheine FreeCAD -> Slicer -> impression

Principe du lissage : elle modifie la propriété deviation des solide avant
génération du stl puis replace les anciennes valeures
A la fin, elle propose de lancer le fichier stl qui s'ouvrira par exemple sous
cura si l'extension stl a été associé à cura dans votre système d'exploitation.

Lancement d'autres programmes ou commande :
Vous pouvez lui demander tout programme ou commande que vous pourriez taper 
dans un terminal. Pour cela, vous devez modifier ce qu'il y a dans la section
"paramètres pouvant être changés". 
Exemples d'applications : 
- Allumer l'imprimante et la lumière (nécessite par exemple une prise commandée)
- Connecter octoprint à l'imprimante
- Lancer la préchauffe du plateau
- Sauvegarder votre fichier FreeCAD et stl
....

Fonctionne sous windows. Testé ausi sous Linux 
"""
import FreeCAD as App
import Mesh
#from PySide2 import QtWidgets
from PySide import QtGui, QtCore
import os, sys, subprocess
import time

#================================
# Paramètres pouvant être changés
#================================

# Gestion du lissage
#--------------------

# deviation : valeur par défaut qui s'affichera dans la boite de dialogue. 
# 0.5 par défaut dans FreeCad, 0.05 permet un lissage plus fort.
# 0.01 est parfait du point de vue lissage. 
# plus la valeur est faible et plus la qualité est bonne, 
# mais plus la taille du fichier stl est grande
# la valeur doit être comprise entre 0 et 1
deviation=0.01 
doitLancerFichier=True # si True, il sera proposer de lancer le fichier stl
delaiMax=10 # si l'utilisateur entre un délai plus grand, il est réduit à delaiMax

# lancement automatique de programmes,
# Automatisation de la chaine de production
#------------------------------------------

#Pour lancer des programmes de votre choix
    #Typiquement par exemple un programme domotique, 
    #allumer votre imprimante ou/et une lumière.
#Utilisez la syntaxe suivante en ajoutant une ligne dans commandes=....:
#  [ ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',temps_attente ],
#
#  'commande à exécuter' : la commande que vous voulez exécuter. 
#  'parame 1, param 2,...' : si votre programme a besoin de paramètres. 
#  Ces paramètres ne serons pris en compte que si extention=''
#
#  'extention': si non vide alors le premier paramètre sera remplacé par le nom de 
#     votre fichier FreeCAD mais avec cette extension :
# 'question à poser' : la question à afficher dans la boite de dialogue
#  temps_attente : facuktatif : nb de seconde à attendre après exécution
#    permet par exemple d'attendre que l'imprimante ait démarré avant de lui 
#    demander de chauffer le plateau

# A noter les doubles \ en remplacement de \ (car les caractères spéciaux
# doivent être précédés d'un \ en python). idem pour par exemple l'apostrophe
# les lignes 
#[ ['commande à exécuter','param 1','param 2'  ] ,'.extension','Question à poser'  ],
# doivent être insérées plus bas après commande=[
#
# Exemples de lancements possibles :

"""
[ ['calc.exe'                                         ] ,''    ,'Lancer la calculatrice ?'],
[ ['C:\\WINDOWS\\system32\\mspaint.exe','dessin.jpg'  ] ,''    ,'Lancer Paint ?'          ],
[ ['C:\\Program Files\\Ultimaker Cura 4.8.0\\Cura.exe'] ,'.stl','Lancer Cura ?'           ],
[ ['curl','http://pidomotique/connecte.php'           ] , ''   ,'Connecter l\'imprimante ?'],
[ ['C:\\Program Files\\Mozilla Firefox\\firefox.exe','https://octopi.local'],'','Lancer Octoprint ?'],
 pour linux:
[ [   'firefox','https://octopi.local'                ] , ''   ,'Lancer Octoprint ?'      ], 

Exemple de pilotage de l'imprimante via octoprint (si vous l'avez...) :
   mise en température du plateau
- Remplacez les XXX se trouvant après X-Api-Key: par votre clef API
(que l'on trouve dans les paramètres d'octoprint)
- Remplacez http://octopi.local par l'url avec laquelle vous accédez à octoprint
- Modifiez la valeur 050 dans "M140 S050" : S050 pour chauffer à 50° si vous souhaitez 60° : S060
- 6 lignes à recopier :
  [
      [  'curl','-H', 'Content-Type: application/json','-H', 'X-Api-Key: XXXXXXXXXXXXXXXXXXXXX', '-X', 'POST',
            '-d {"command":"M140 S050"}','http://octopi.local/api/printer/command'
      ]
            , '','Chauffer le plateau de l\'imprimante ?'
  ],

"""


commandes=[
# insérez ici vos lignes.
  [ ['calc.exe'                                         ] ,''    ,'Lancer la calculatrice ?',2],
  [ ['C:\\Program Files\\Ultimaker Cura 4.8.0\\Cura.exe'] ,'.stl','Lancer Cura ?'           ],
  [  ['curl','http://pidomotique/connecte.php'              ] , ''   ,'Allumer et connecter l\'imprimante ?',4],
  [ ['C:\\Program Files\\Mozilla Firefox\\firefox.exe','https://octopi.local'],'','Lancer Octoprint ?'],
  [
      [  'curl','-H', 'Content-Type: application/json','-H', 'X-Api-Key: xxxxxxxxxxxxxxx', '-X', 'POST',
            '-d {"command":"M140 S050"}','http://octopi.local/api/printer/command'
      ]
            , '','Chauffer le plateau de l\'imprimante ?'
  ],

         ]



#===============
#Initialisations
#===============
dictionnaireOrigineDeviation={}
mw = Gui.getMainWindow()
# Constant definitions
userCancelled= "Cancelled"
userOK= "OK"

indiceCommandeEtParam=0
indiceExtFileNameParamCommandeALancer=1
indiceTextAutreCommandeALancer=2
indiceDelai=3

# UI Class definitions



class BoiteDialogueSTLGuiClass(QtGui.QDialog):

    def  __init__(self, parent=None):
        super(BoiteDialogueSTLGuiClass, self).__init__(parent)
        self.initUI()

    def initUI(self):
        self.result = userCancelled
        self.tabCheckboxTextCommandes=[] # tab des checkbox commandes à lancer
        self.setGeometry( 250, 250, 400, 150)
        self.setWindowTitle("Titre de la fenêtre")
        lay = QtGui.QGridLayout(self) 

        # checkboxes
        self.checkboxGenererSTL = QtGui.QCheckBox("Générer le STL", self)
        #self.checkboxGenererSTL.clicked.connect(self.onCheckboxGenererSTL)
        self.checkboxGenererSTL.setCheckState(QtCore.Qt.Checked)# mise à checked
        lay.addWidget(self.checkboxGenererSTL, 0, 0) 
        if doitLancerFichier :
            self.checkboxLancerSTL = QtGui.QCheckBox("Lancer le slicer", self)
       #     self.checkboxLancerSTL.clicked.connect(self.onCheckboxLancerSTL)
            self.checkboxLancerSTL.setCheckState(QtCore.Qt.Checked)
            lay.addWidget(self.checkboxLancerSTL, 2, 0) 

        posLigne=3
        for cmd in commandes :
           #decalPositionY=decalPositionY+20
           textCommande=cmd[indiceTextAutreCommandeALancer]
           self.checkboxTextCommande = QtGui.QCheckBox(textCommande, self)
           # mémorisation de cette checbox dans un tableau :
           self.tabCheckboxTextCommandes.append(self.checkboxTextCommande)
       #    self.checkboxTextCommande.clicked.connect(self.onCheckboxGenererSTL)
           self.checkboxTextCommande.setCheckState(QtCore.Qt.Checked)# mise à checked
           lay.addWidget(self.checkboxTextCommande, posLigne, 0) 
           posLigne=posLigne+1


        # numeric input field
        self.label2 = QtGui.QLabel('Précision (param deviation) \nentre 0.01 et 1\n'+
             '0.01 pour une grande qualité', self)
        lay.addWidget(self.label2, 0, 1) 

        self.precision = QtGui.QDoubleSpinBox(self)
        self.precision.setValue(deviation)
        self.precision.setRange(0.01,1)
        self.precision.setSingleStep(0.01)
        self.precision.setToolTip('Plus le chiffre est grand, plus la taille ' +
            'du fichier est grande \net meilleur est la précision.\n'+
            'Voir le paramètre deviation dans FreeCAD')
        lay.addWidget(self.precision, 0, 2) 
        """
        cancelButton = QtGui.QDialogButtonBox (self)
        cancelButton.clicked.connect(self.onCancel)
       # cancelButton.setAutoDefault(True)
        cancelButton.setStandardButtons(QtGui.QDialogButtonBox.Cancel)
        lay.addWidget(cancelButton, posLigne+1,1)
        # OK button
        okButton = QtGui.QDialogButtonBox (self)
        okButton.clicked.connect(self.onOk)
        okButton.setStandardButtons(QtGui.QDialogButtonBox.Ok)
        lay.addWidget(okButton, posLigne+1, 2)
        """
        butBox = QtGui.QDialogButtonBox(self)
        butBox.setOrientation(QtCore.Qt.Horizontal)
        butBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
        lay.addWidget(butBox) 
        butBox.accepted.connect(self.accept)
        butBox.rejected.connect(self.reject)
    
    def onCheckboxGenererSTL(self):
        print('onCheckboxGenererSTL')
    def onCheckboxLancerSTL(self):
        print('onCheckboxLancerSTL')
    """
    def onCancel(self):
        self.result         = False
        print('On cancel')
        self.close()
    def onOk(self):
        print('onOK')
        self.result         = True
        self.close()
    """
    



#===============
#Fonctions
#===============
def octopi(api,commande,urlOctopi,apiKey) :
    """
    Fonction qui renvoie le paramètre à passer à subprocess.Popen si on veut lancer
    une AI octopi. Non utilisez ici, mais ça peut servir

    Arguments : 
        api l'api à exécuter  ex : '/api/printer/command'
        commande : la commande de l'API en question ex: '{"command":"M140 S050"}'
        urlOctoprint : l'url de votre octopi ex : 'https://octopi.local'
        apiKey : la clef API que vous trouvez dans setting/API d'octoprint
    Returns :

    Example
       API lanant un gcode. Ici la chauffe du plateau de l'imprimante
       subprocess.Popen(
                          octopi(
                                '/api/printer/command',
                                '{"command":"M140 S050"}',
                                'https://octopi.local',
                                'XXXXKEYXAPIXXXXXXXX'))
                 ,close_fds=True)
    """
    commandeCurl=[
    'curl','-H', 'Content-Type: application/json','-H',
         'X-Api-Key: '+apiKey,
         '-X', 'POST',
        '-d '+commande,urlOctopi+api
    ]
    return commandeCurl


def nameFileStl(dictionnaireOrigineDeviation):
    """
    Fonction qui renvoie le nom de fichier compris chemin
    où sera enregistré le stl
    Le document contenant les objets du dictionaire doit être ouvert et
    sauvé

    Arguments : 
        dictionnaireOrigineDeviation  (dictionary) : 
            un dictionnaire dont les clefs sont des solide. 
            le nom de fichier sera obtenu à partir du nom de fichier 
            du document contenant le premier objet (solide) de ce
            dictionnaire

    Returns :
        '' si le document n'existe pas,
        sinon le nom du fichier où sera enregistré le stl

    Example
        nomFichierStl=nameFileStl(dictionnaireOrigineDeviation)
    """

    doc=list(dictionnaireOrigineDeviation.keys())[0].Document
    if doc is None:
        QtGui.QMessageBox.information(mw,'Attention',\
            "Vous n'avez pas de document ouvert")
        return ''
    if doc.FileName=='':
        QtGui.QMessageBox.information(mw,
             'Attention','Sauvez votre document avant de relancer la commande') 
        return ''
    return(os.path.splitext(doc.FileName)[0]+'.stl')

def memoriseObjEtDeviation(selection,deviationImpose):
    """
    Mémorise les solide qui sont contenus dans selection et leur
    propriété deviation et remplace cette dernière par la valeur 
    de deviationImpose
    Elle impose un recompute ultérieur par un touch.
    Recompute() doit être éventuellement lancé après

    Arguments :
        selection : une sélection d'objets
        deviationImpose (float) : valeur que l'on veut imposer à deviation

    Returns: 
        un dictionnaire contenant l'objet en key et la déviation en value
        Renvoie {} s'il n'y a pas d'objet adéquate dans la sélection

    Example:
        memoriseObjEtDeviation(Gui.Selection.getSelection(),deviation)
    """
    dicoObjDeviation={}
    for objData in selection :
        if objData.isDerivedFrom('Part::Feature') \
          and not objData.isDerivedFrom('PartDesign::Feature')\
          and not objData.isDerivedFrom('Part::Part2DObject'):
            dicoObjDeviation[objData]=objData.ViewObject.Deviation
            objData.ViewObject.Deviation=deviationImpose 
           # pour imposer un recompute même si la déviation est plus petite
           # qu'avant :
            objData.touch()
            for o in objData.ViewObject.claimChildren():
                    o.touch()
    return dicoObjDeviation

def restitueDeviation(dicoOrigineDeviation):
    """
    Fonction qui remplace la propriété deviation dans les objets
    Elle impose un recompute par un touch. recompute() doit être
    lancé après

    Arguments:
        dicoOrigineDeviation un dictionnaire contenant l'objet en key 
        et la valeur à imposer dans la propriété déviation de l'objet
        en value

    Returns: Ne retourne rien

    Example:
        restitueDeviation(dicoOrigineDeviation)

    """
    for ob2 in dicoOrigineDeviation:
        ob2.ViewObject.Deviation=dicoOrigineDeviation[ob2]
        # pour imposer un recompute même si la déviation est plus petite
        # qu'avant :
        ob2.touch()
        for o in ob2.ViewObject.claimChildren():
            o.touch()

def openFile(fileName):
    """
    fonction qui lance un fichier en fonction du système d'exploitation
    vérifié pour l'instant uniquement sous windows

    Arguments:
        fileName (string) le nom du fichier à lancer

    Returns
        Ne retourne rien

    Example: 
        openFile(unfichier.stl)
        openFile(calc.exe)
    """
    if sys.platform == "win32":
        os.startfile(fileName)
    else:
        opener ="open" if sys.platform == "darwin" else "xdg-open"
        subprocess.Popen([opener, fileName],close_fds=True) 

def recalcul(dicoObjs):
    """
    lance un recompute pour chaque Document des objets du dico
    seulement si cela n'a pas déjà été fait
    Arguments:
        dicoObjs un dictionnaire contenant l'objet en key 

    Returns
        Ne retourne rien

    Example:
        recalcul(dicoMesObjets)
    """
    docDejaRecalcul=[]
    for obj in dicoObjs:
        if obj.Document not in docDejaRecalcul:
            #print('recompute {}'.format(obj.Document.Name))
            obj.Document.recompute()
            docDejaRecalcul.append(obj.Document)

def lanceCommmandes(commandesAlancer,fileStlName,formBoiteDialogueSTL):
    """
    lance les commmandes
    Arguments:
        commandesAlancer sous la forme d'un tableau de tableaux
        [ ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',délai ],
          ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',délai ],
          ...
        ]

         'commande à exécuter' : la commande que vous voulez exécuter. 
         Par exemple 'monProgramme.exe' si besoin mettre aussi son chemin. 
         'param 1, param2... ' : si votre programme a besoin de paramètres. 
            Ne sera pris en compte que si extention=''
         'extention': si non vide, le paramètre sera le nom de votre
              fichier FreeCAD mais avec votre extension :
         'question à poser' : la question à afficher dans la boite de dialogue
         délai : temps d'attente en secondes avant de passer à la commande suivante.

    Returns
        Ne retourne rien

    Example:
            lanceCommmandes(commandes,fileStlName) # lance les commandes que l'on a décrites dans commandes
    """


    #for i in range(len(formBoiteDialogueSTL.tabCheckboxTextCommandes)) :
    for tbktc in formBoiteDialogueSTL.tabCheckboxTextCommandes :
#        if formBoiteDialogueSTL.tabCheckboxTextCommandes[i].isChecked() :
        if tbktc.isChecked() :
            #commande=commandesAlancer[i]
            i= formBoiteDialogueSTL.tabCheckboxTextCommandes.index(tbktc)
            commande=commandesAlancer[i] ################################## PAS TOP : à reconsidérer
            #print('{}'.format(commande))
            commandeEtParam=commande[indiceCommandeEtParam]
            extFileNameParamCommandeALancer=commande[indiceExtFileNameParamCommandeALancer]
            textAutreCommandeALancer=commande[indiceTextAutreCommandeALancer]
            delai=0
            try :
               if commande[indiceDelai:indiceDelai+1]!=[] : #si l'utilisateur a rentré un délai
                  delai=commande[indiceDelai]
            except :
               print ('Délai non pris en compte car valeur numérique non valide'+
                      'pour la commande '+ textAutreCommandeALancer)
            if delai > delaiMax :
                delai=delaiMax

            if extFileNameParamCommandeALancer!='':     
                if commandeEtParam[2:3]==[]  : # Il n'y a pas de paramètre.
                   # On ajoute un élément pour y mettre notre nom de fichier
                   # avec notre extension.
                   commandeEtParam.append('')
                commandeEtParam[1]=   os.path.splitext(fileStlName)[0]+  extFileNameParamCommandeALancer
            subprocess.Popen(commandeEtParam,close_fds=True)
            time.sleep(delai)# Pause de durée delai secondes   


def run(objs=Gui.Selection.getSelection(), dev=deviation):
    """
    fonction principale
    Arguments:
        doc : le document actif
        objs : les objets que l'on a au préalable sélectionnés
        dev : la valeur de la propriété déviation qui sera 
              imposée avant la génération du stl

    Returns
        Ne retourne rien

    Example:
        run()
    """



    # Boite de dialogue de ce que l'on veut faire
    formBoiteDialogueSTL =BoiteDialogueSTLGuiClass(mw)
    if not formBoiteDialogueSTL.exec_() : return
    dev=formBoiteDialogueSTL.precision.value()

    # on mémorise les  solide sélectionnés et leur déviation,
    # on fait un touch() de leur child pour prise en compte dans recompute :
    dictionnaireOrigineDeviation=memoriseObjEtDeviation(objs,dev)
    if len(dictionnaireOrigineDeviation)==0: # si on a trouvé aucun solide
        QtGui.QMessageBox.information(mw, 'Attention',\
            'Sélectionnez un ou plusieurs solide avant de lancer la macro')
        return
    # on récupère le nom du fichier où l'on devra enregistrer le stl
    fileStlName=nameFileStl(dictionnaireOrigineDeviation)
    if fileStlName == '' :
        return

    # l'export lui-même. On passe en paramètre les objets solide qui 
    # sont dans dictionnaireOrigineDeviation
    if formBoiteDialogueSTL.checkboxGenererSTL.isChecked():
       Mesh.export(list(dictionnaireOrigineDeviation.keys()), fileStlName) 
       print('Export fait')
    # restitution de la propriété deviation d'origine pour les solide :
    restitueDeviation(dictionnaireOrigineDeviation)
    recalcul(dictionnaireOrigineDeviation)

    if formBoiteDialogueSTL.checkboxLancerSTL.isChecked():
            #lance par exemple cura si cura a été associé aux fichiers *.stl
            openFile(fileStlName)

    lanceCommmandes(commandes,fileStlName,formBoiteDialogueSTL) # lance les commandes que l'on a décrites dans commandes


if __name__ == '__main__':
    run()


Macro Sketch Constraint From Spreadsheet :
https://wiki.freecad.org/Macro_Sketch_C ... adsheet/fr
openBrain
Veteran
Posts: 9041
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by openBrain »

2cv001 wrote: Thu Dec 17, 2020 7:03 am Ci-dessous le nouvau source. Je me donne encore un temps pour relire etc. puis le le mettrai à la place du fichier joint dans le premier message de ce sujet.
Les remarques : :)
* Manque toujours le 'import FreeCADGui as Gui'
* L129+130+148+... : normalement tu n'as plus besoin des variables userCancelled, userOK, self.result, ...
* L151 : tu devrais mettre comme titre du dialogue un truc plus parlant que "Titre de la fenêtre"
* L165+166+175 : plutôt que de gérer à la main un compteur, il vaut mieux utiliser la façon pythonic : 'for posLigne, cmd in enumerate(commandes, start=3):'
* Pas mal de code mort (commenté) entre les lignes 190 et 225
* Je suis surpris que ça marche en fait car il te manque une ligne à la fin du initUI : 'self.setLayout(lay)'
* L210-213 : fonctions inutiles maintenant je pense
* L472 : j'avais dû l'écrire comme ça pour faire court, mais tu devrais mettre le 'return' sur la ligne suivante avec une indentation (façon classique)
Fait et donc en restant en Pyside.
J'ai du coup ce message dont on avait déjà parlé comme étant "normal" :

07:33:13 C:/Datas/data/Imprimante 3D/Freecad/ExportStlV12.FCMacro:205: DeprecationWarning: an integer is required (got type PySide2.QtWidgets.QDialogButtonBox.StandardButton). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python.
butBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
Bizarre d’ailleurs que dans ce message il fasse référence à pyside2.
Du coup je vais essayer d'expliquer un peu plus.
'PySide' (nom générique) est le nom du binding Python pour Qt (c'est-à-dire la couche qui permet de se servir de Qt -- qui est en C++ -- depuis Python).
PySide est né du temps de Qt4. Le module Python 'PySide' (nom spécifique) est donc --normalement-- celui associé à Qt4, et PySide2 est celui associé à Qt5 -- qui est la version courante.
La différence principale quand on l'utilise est que PySide n'a que 2 namespaces principaux : QtGui (tout ce qui concerne les UI) et QtCore (toutes les fonctions de bases).
PySide2 a scindé QtGui en 2 et il a donc 3 principaux namespaces : QtWidgets (tout ce qui concerne les widgets), QtGui (tout ce qui concerne les UI mais pas les widgets) et QtCore (toutes les fonctions de bases). En gros ce qui concerne spécifiquement les widgets a été sorti de QtGui et mis dans un namespace spécifique QtWidgets.
FreeCAD 0.18 (c'est peut-être encore le cas en 0.19 mais pas sûr) était compilé à la fois en version Qt4 et en version Qt5. Et du coup, en théorie, il aurait fallu que les macros et workbenches en Python existent en 2 versions : PySide et PySide2. C'est ingérable. Les auteurs auraient aussi pu gérer chacun dans leur macro/workbench la détection de la version Qt et créer des wrappers -- des fonctions qui appellent soit PySide soit PySide2 -- en fonction. Mais ça aurait été le bazar.
Donc les développeurs FreeCAD ont fait quelque chose de très malin en gérant tout ça au niveau de l'import. En fait, de manière spécifique dans FreeCAD, quand tu fais 'from PySide import QtGui', il peut se passer 2 choses différentes qui sont transparentes pour toi :
* Si tu es sur une version Qt4, ça importe PySide.QtGui normalement
* Si tu es sur une version Qt5, ça importe PySide2.QtGui + PySide2.QtWidgets et ça remappe l'ensemble sur le namespace PySide.QtGui
Du coup les macros 'PySide' fonctionnent de la même manière en Qt4 ou Qt5.
Comme tu le vois, ça peut générer quelques warnings et ça empêche d'avoir accès aux fonctions les plus récentes de Qt, mais globalement ça marche très bien.
Je pense toutefois qu'à la sortie officielle de la 0.19, on arrêtera PySide et les nouvelles macros seront en PySide2. Dans ce cas, il faudra bien travailler avec QtWidgets pour tout ce qui concerne les widgets.
IL a juste fallu que je remplace butBox = QtGui.QDialogButtonBox(dialog) par butBox = QtGui.QDialogButtonBox(self) car il ne connaissait pas 'dialog' mais ça marche maintenant.
:oops: En fait j'ai copié le code d'un autre topic où le dialogue n'était pas une classe, et du coup j'ai oublié de corriger ça.
Un grand merci openBrain pour la mise à disposition de ton expertise !
Pas sûr qu'on soit au niveau de l'expertise là. :) Ceci dit c'est fait avec plaisir vu que tu fais aussi les efforts de ton côté et que tu es volontaire pour apprendre. ;)
User avatar
2cv001
Posts: 484
Joined: Wed Jan 01, 2020 9:30 am

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by 2cv001 »

openBrain wrote: Thu Dec 17, 2020 12:10 pm Les remarques : :)
* Manque toujours le 'import FreeCADGui as Gui'
Tiens, effectivement, sans doute un retour arrière trop fort à un moment. désolé..
Fait
* L129+130+148+... : normalement tu n'as plus besoin des variables userCancelled, userOK, self.result, ...
fait
* L151 : tu devrais mettre comme titre du dialogue un truc plus parlant que "Titre de la fenêtre"
Un mauvais couper coller d'un mini programme en parallèle sur lequel je travaillais pour comprendre comment ça fonctionnait et sur lequel je faisait plein d'essais.
Fait
* L165+166+175 : plutôt que de gérer à la main un compteur, il vaut mieux utiliser la façon pythonic : 'for posLigne, cmd in enumerate(commandes, start=3):'
C'est très puissant ! J'ai encore appris quelque chose.
Fait
* Pas mal de code mort (commenté) entre les lignes 190 et 225
J'ai supprimé un peu partout les codes morts dont tu parles, mais aussi des print() qui me servaient à débugger, ainsi que des commentaires devenus inutiles
* Je suis surpris que ça marche en fait car il te manque une ligne à la fin du initUI : 'self.setLayout(lay)'
Les mystères de l'informatique...
Je l'ai mis en dernière ligne mais je suppose que l'on peut le mettre où l'on veut après le ' lay = QtWidgets.QGridLayout(self) '

Code: Select all


    def initUI(self):
        self.tabCheckboxTextCommandes=[] # tab des checkbox commandes à lancer
        self.setGeometry( 250, 250, 400, 150)
        self.setWindowTitle("Export et lancement de programmes")
        lay = QtWidgets.QGridLayout(self) 

  .............................
  ......................
    
        butBox.accepted.connect(self.accept)
        butBox.rejected.connect(self.reject)

        self.setLayout(lay)
* L210-213 : fonctions inutiles maintenant je pense
J'ai fait du ménage
* L472 : j'avais dû l'écrire comme ça pour faire court, mais tu devrais mettre le 'return' sur la ligne suivante avec une indentation (façon classique)
Rectifié.
Du coup je vais essayer d'expliquer un peu plus.
'PySide' (nom générique) est .......
.....................Dans ce cas, il faudra bien travailler avec QtWidgets pour tout ce qui concerne les widgets.
Bravo pour l'explication ! tout compris ! (enfin je crois :D) J'ai donc fait la modif. Du coup d'ailleurs, j'ai pu retirer QtGui dans l'import :
from PySide2 import QtCore, QtWidgets car sauf erreur, il n'y a pas d'endroit où j'avais besoin de QtGui
Par contre, j'ai toujours les messages "warning"
" butBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
17:07:52 C:/Datas/data/Imprimante 3D/Freecad/ExportStlV14.FCMacro:183: DeprecationWarning: an integer is required (got type PySide2.QtWidgets.QDialogButtonBox.StandardButton). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python."
Encore merci openBrain pour ton temps passé !
Demain à tête reposée, je relie tout encore une fois

Michel
Le nouveau code :

Code: Select all

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Macro qui permet de créer un fichier "stl" avec des arrondis parfaits
= sans facettes visibles.
Elle permet aussi de lancer des programmes de votre choix
Par exemple pour automatiser la cheine FreeCAD -> Slicer -> impression

Principe du lissage : elle modifie la propriété deviation des solides avant
génération du stl puis replace les anciennes valeurs
A la fin, elle propose de lancer le fichier stl qui s'ouvrira par exemple sous
cura si l'extension stl a été associée à cura dans votre système d'exploitation.

Lancement d'autres programmes ou commandes :
Vous pouvez lui demander d'enchainer tout programme ou commande que vous pourriez  
taper dans un terminal. 
Pour cela, vous devez modifier ce qu'il y a dans la section
"paramètres pouvant être changés". 

Exemples d'applications : 
- Allumer l'imprimante et la lumière (nécessite par exemple une prise commandée)
- Connecter octoprint à l'imprimante
- Lancer la préchauffe du plateau
- Sauvegarder votre fichier FreeCAD et stl
....

Fonctionne sous windows. Testé ausi sous Linux 
"""
import FreeCAD as App
import Mesh
from PySide2 import QtCore, QtWidgets
import os, sys, subprocess
import time
import FreeCADGui as Gui

#================================
# Paramètres pouvant être changés
#================================

# Gestion du lissage
#--------------------

# deviation : valeur par défaut qui s'affichera dans la boite de dialogue. 
# 0.5 par défaut dans FreeCad, 0.05 permet un lissage plus fort.
# 0.01 est parfait du point de vue lissage. 
# plus la valeur est faible et plus la qualité est bonne, 
# mais plus la taille du fichier stl est grande
# la valeur doit être comprise entre 0 et 1
deviation=0.01 
doitLancerFichier=True # si True, il sera proposer de lancer le fichier stl.
                       # sinon mettre False

# lancement automatique de programmes,
# Automatisation de la chaine de production
#------------------------------------------

delaiMax=10 # si l'utilisateur entre un délaiplus grand que cette valeur,
            # il sera réduit à delaiMax
            # delai=nb de secondes entre les programmes

#Pour lancer des programmes de votre choix
    #Typiquement par exemple un programme domotique, 
    #allumer votre imprimante ou/et une lumière etc.
#Utilisez la syntaxe suivante en ajoutant une ligne plus bas, dans commandes=[....:
#  [ ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',temps_attente ],
#
#  'commande à exécuter' : la commande que vous voulez exécuter. 
#  'parame 1, param 2,...' : si votre programme a besoin de paramètres. 
#  Ces paramètres ne serons pris en compte que si extention=''
#
#  'extention': si non vide alors le premier paramètre sera remplacé par le nom de 
#     votre fichier FreeCAD mais avec cette extension :
# 'question à poser' : la question à afficher dans la boite de dialogue
#  temps_attente : facuktatif : nb de secondes à attendre après exécution
#    permet par exemple d'attendre que l'imprimante ait démarré avant de lui 
#    demander de chauffer le plateau

# A noter les doubles \ en remplacement de \ (car les caractères spéciaux
# doivent être précédés d'un \ en python). idem pour par exemple l'apostrophe
# les lignes en question sont à insérer plu sbas après commandes=[...
#
# Exemples de lancements possibles :

"""
[ ['calc.exe'                                         ] ,''    ,'Lancer la calculatrice ?'],
[ ['C:\\WINDOWS\\system32\\mspaint.exe','dessin.jpg'  ] ,''    ,'Lancer Paint ?'          ],
[ ['C:\\Program Files\\Ultimaker Cura 4.8.0\\Cura.exe'] ,'.stl','Lancer Cura ?'           ],
[ ['curl','http://pidomotique/connecte.php'           ] , ''   ,'Connecter l\'imprimante ?'],
[ ['C:\\Program Files\\Mozilla Firefox\\firefox.exe','https://octopi.local'],'','Lancer Octoprint ?'],
 pour linux:
[ [   'firefox','https://octopi.local'                ] , ''   ,'Lancer Octoprint ?'      ], 

Exemple de pilotage de l'imprimante via octoprint (si vous l'avez...) :
   mise en température du plateau
- Remplacez les XXX se trouvant après X-Api-Key: par votre clef API
(que l'on trouve dans les paramètres d'octoprint)
- Remplacez http://octopi.local par l'url avec laquelle vous accédez à octoprint
- Modifiez la valeur 050 dans "M140 S050". S050 indique qu'il faut chauffer à 50°
     si vous souhaitez 60°, remplacez S050 par S060
- 6 lignes à recopier après l'avoir adapté:
  [
      [  'curl','-H', 'Content-Type: application/json','-H', 'X-Api-Key: XXXXXXXXXXXXXXXXXXXXX', '-X', 'POST',
            '-d {"command":"M140 S050"}','http://octopi.local/api/printer/command'
      ]
            , '','Chauffer le plateau de l\'imprimante ?'
  ],

"""


commandes=[
# insérez ici vos lignes.
  [ ['calc.exe'                                         ] ,''    ,'Lancer la calculatrice ?',2],
  [ ['C:\\Program Files\\Ultimaker Cura 4.8.0\\Cura.exe'] ,'.stl','Lancer Cura ?'           ],
  [  ['curl','http://pidomotique/connecte.php'              ] , ''   ,'Allumer et connecter l\'imprimante ?',4],
  [ ['C:\\Program Files\\Mozilla Firefox\\firefox.exe','https://octopi.local'],'','Lancer Octoprint ?'],
  [
      [  'curl','-H', 'Content-Type: application/json','-H', 'X-Api-Key: xxxxxxxxxxxxxxx', '-X', 'POST',
            '-d {"command":"M140 S050"}','http://octopi.local/api/printer/command'
      ]
            , '','Chauffer le plateau de l\'imprimante ?'
  ],

         ]


#===============
#Initialisations
#===============
dictionnaireOrigineDeviation={}
mw = Gui.getMainWindow()
indiceCommandeEtParam=0
indiceExtFileNameParamCommandeALancer=1
indiceTextAutreCommandeALancer=2
indiceDelai=3

# UI Class definitions
class BoiteDialogueSTLGuiClass(QtWidgets.QDialog):

    def  __init__(self, parent=None):
        super(BoiteDialogueSTLGuiClass, self).__init__(parent)
        self.initUI()

    def initUI(self):
        self.tabCheckboxTextCommandes=[] # tab des checkbox commandes à lancer
        self.setGeometry( 250, 250, 400, 150)
        self.setWindowTitle("Export et lancement de programmes")
        lay = QtWidgets.QGridLayout(self) 

        # checkboxes
        self.checkboxGenererSTL = QtWidgets.QCheckBox("Générer le STL", self)
        self.checkboxGenererSTL.setCheckState(QtCore.Qt.Checked)
        lay.addWidget(self.checkboxGenererSTL, 0, 0) 
        if doitLancerFichier :
            self.checkboxLancerSTL = QtWidgets.QCheckBox("Lancer le slicer", self)
            self.checkboxLancerSTL.setCheckState(QtCore.Qt.Checked)
            lay.addWidget(self.checkboxLancerSTL, 2, 0) 

        for posLigne, cmd in enumerate(commandes, start=3):
           textCommande=cmd[indiceTextAutreCommandeALancer]
           self.checkboxTextCommande = QtWidgets.QCheckBox(textCommande, self)
           # mémorisation de cette checbox dans un tableau :
           self.tabCheckboxTextCommandes.append(self.checkboxTextCommande)
           self.checkboxTextCommande.setCheckState(QtCore.Qt.Checked)
           lay.addWidget(self.checkboxTextCommande, posLigne, 0) 

        # numeric input field
        self.label2 = QtWidgets.QLabel('Précision (param deviation) \nentre 0.01 et 1\n'+
             '0.01 pour une grande qualité', self)
        lay.addWidget(self.label2, 0, 1) 

        self.precision = QtWidgets.QDoubleSpinBox(self)
        self.precision.setValue(deviation)
        self.precision.setRange(0.01,1)
        self.precision.setSingleStep(0.01)
        self.precision.setToolTip('Plus le chiffre est grand, plus la taille ' +
            'du fichier est grande \net meilleur est la précision.\n'+
            'Voir le paramètre deviation dans FreeCAD')
        lay.addWidget(self.precision, 0, 2) 

        butBox = QtWidgets.QDialogButtonBox(self)
        butBox.setOrientation(QtCore.Qt.Horizontal)
        butBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
        lay.addWidget(butBox) 
        butBox.accepted.connect(self.accept)
        butBox.rejected.connect(self.reject)

        self.setLayout(lay)
    

#===============
#Fonctions
#===============
def octopi(api,commande,urlOctopi,apiKey) :
    """
    Fonction qui renvoie le paramètre à passer à subprocess.Popen si on veut lancer
    une AI octopi. Non utilisée ici. Mais pour mémoire et une prochaine évolution

    Arguments : 
        api l'api à exécuter  ex : '/api/printer/command'
        commande : la commande de l'API en question ex: '{"command":"M140 S050"}'
        urlOctoprint : l'url de votre octopi ex : 'https://octopi.local'
        apiKey : la clef API que vous trouvez dans setting/API d'octoprint
    Returns :

    Example
       API lanant un gcode. Ici la chauffe du plateau de l'imprimante
       subprocess.Popen(
                          octopi(
                                '/api/printer/command',
                                '{"command":"M140 S050"}',
                                'https://octopi.local',
                                'XXXXKEYXAPIXXXXXXXX'))
                 ,close_fds=True)
    """
    commandeCurl=[
    'curl','-H', 'Content-Type: application/json','-H',
         'X-Api-Key: '+apiKey,
         '-X', 'POST',
        '-d '+commande,urlOctopi+api
    ]
    return commandeCurl


def nameFileStl(dictionnaireOrigineDeviation,mw=Gui.getMainWindow()):
    """
    Fonction qui détermine et renvoie le nom de fichier compris chemin
    où sera enregistré le stl
    Le document FreeCAD contenant les objets du dictionaire doit être 
    ouvert etsauvé

    Arguments : 
        dictionnaireOrigineDeviation  (dictionary) : 
            un dictionnaire dont les clefs sont des solides FreeCAD. 
            le nom de fichier sera obtenu à partir du nom de fichier 
            du document contenant le premier objet (solide) de ce
            dictionnaire

    Returns :
        '' si le document n'existe pas,
        sinon le nom du fichier où sera enregistré le stl

    Example
        nomFichierStl=nameFileStl(dictionnaireOrigineDeviation)
    """

    doc=list(dictionnaireOrigineDeviation.keys())[0].Document
    if doc is None:
        QtWidgets.QMessageBox.information(mw,'Attention',\
            "Vous n'avez pas de document ouvert")
        return ''
    if doc.FileName=='':
        QtWidgets.QMessageBox.information(mw,
             'Attention','Sauvez votre document avant de relancer la commande') 
        return ''
    return(os.path.splitext(doc.FileName)[0]+'.stl')

def memoriseObjEtDeviation(selection,deviationImpose):
    """
    Mémorise les solides qui sont contenus dans selection et leur
    propriété deviation et remplace cette dernière par la valeur 
    de deviationImpose
    Elle impose un recompute ultérieur par un touch.
    Recompute() doit être éventuellement lancé après

    Arguments :
        selection : une sélection d'objets
        deviationImpose (float) : valeur que l'on veut imposer à deviation

    Returns: 
        un dictionnaire contenant l'objet en key et la déviation en value
        Renvoie {} s'il n'y a pas d'objet adéquate dans la sélection

    Example:
        memoriseObjEtDeviation(Gui.Selection.getSelection(),deviation)
    """
    dicoObjDeviation={}
    for objData in selection :
        if objData.isDerivedFrom('Part::Feature') \
          and not objData.isDerivedFrom('PartDesign::Feature')\
          and not objData.isDerivedFrom('Part::Part2DObject'):
            dicoObjDeviation[objData]=objData.ViewObject.Deviation
            objData.ViewObject.Deviation=deviationImpose 
           # pour imposer un recompute même si la déviation est plus petite
           # qu'avant :
            objData.touch()
            for o in objData.ViewObject.claimChildren():
                    o.touch()
    return dicoObjDeviation

def restitueDeviation(dicoOrigineDeviation):
    """
    Fonction qui remplace la propriété deviation dans les objets
    Elle impose un recompute par un touch. recompute() doit être
    lancé après

    Arguments:
        dicoOrigineDeviation un dictionnaire contenant l'objet en key 
        et la valeur à imposer dans la propriété déviation de l'objet
        en value

    Returns: Ne retourne rien

    Example:
        restitueDeviation(dicoOrigineDeviation)

    """
    for ob2 in dicoOrigineDeviation:
        ob2.ViewObject.Deviation=dicoOrigineDeviation[ob2]
        # pour imposer un recompute même si la déviation est plus petite
        # qu'avant :
        ob2.touch()
        for o in ob2.ViewObject.claimChildren():
            o.touch()

def openFile(fileName):
    """
    fonction qui lance un fichier en fonction du système d'exploitation
    vérifié pour l'instant uniquement sous windows

    Arguments:
        fileName (string) le nom du fichier à lancer

    Returns
        Ne retourne rien

    Example: 
        openFile(unfichier.stl)
        openFile(calc.exe)
    """
    if sys.platform == "win32":
        os.startfile(fileName)
    else:
        opener ="open" if sys.platform == "darwin" else "xdg-open"
        subprocess.Popen([opener, fileName],close_fds=True) 

def recalcul(dicoObjs):
    """
    lance un recompute pour chaque Document des objets du dico
    seulement si cela n'a pas déjà été fait
    Arguments:
        dicoObjs un dictionnaire contenant l'objet en key 

    Returns
        Ne retourne rien

    Example:
        recalcul(dicoMesObjets)
    """
    docDejaRecalcul=[]
    for obj in dicoObjs:
        if obj.Document not in docDejaRecalcul:
            obj.Document.recompute()
            docDejaRecalcul.append(obj.Document)

def lanceCommmandes(commandesAlancer,fileStlName,formBoiteDialogueSTL):
    """
    lance les commmandes
    Arguments:
        commandesAlancer sous la forme d'un tableau de tableaux
        [ ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',délai ],
          ['commande à exécuter', 'param 1', param 2, ] ,'.extention','question à poser',délai ],
          ...
        ]
         'commande à exécuter' : la commande que vous voulez exécuter. 
         Par exemple 'monProgramme.exe' si besoin mettre aussi son chemin. 
         'param 1, param2... ' : si votre programme a besoin de paramètres. 
            Ne sera pris en compte que si extention=''
         'extention': si non vide, le paramètre param 1 sera le nom de votre
              fichier FreeCAD mais avec votre extension :
         'question à poser' : la question à afficher dans la boite de dialogue
         délai : temps d'attente en secondes avant de passer à la commande suivante.

    Returns
        Ne retourne rien

    Example:
        lanceCommmandes(commandes,fileStlName,formBoiteDialogueSTL) # lance les commandes que l'on a décrites dans commandes
    """

    for tbktc in formBoiteDialogueSTL.tabCheckboxTextCommandes :
        if tbktc.isChecked() :
            i= formBoiteDialogueSTL.tabCheckboxTextCommandes.index(tbktc)
            commande=commandesAlancer[i]
            commandeEtParam=commande[indiceCommandeEtParam]
            extFileNameParamCommandeALancer=commande[indiceExtFileNameParamCommandeALancer]
            textAutreCommandeALancer=commande[indiceTextAutreCommandeALancer]
            delai=0
            try :
               if commande[indiceDelai:indiceDelai+1]!=[] : #si l'utilisateur a rentré un délai
                  delai=commande[indiceDelai]
            except :
               print ('Délai non pris en compte car valeur numérique non valide'+
                      'pour la commande '+ textAutreCommandeALancer)
            if delai > delaiMax :
                delai=delaiMax

            if extFileNameParamCommandeALancer!='':     
                if commandeEtParam[2:3]==[]  : # Il n'y a aucun paramètre.
                   # On ajoute un paramètre pour y mettre notre nom de fichier
                   # avec notre extension.
                   commandeEtParam.append('')
                commandeEtParam[1]=   os.path.splitext(fileStlName)[0]+  extFileNameParamCommandeALancer
            subprocess.Popen(commandeEtParam,close_fds=True)
            time.sleep(delai)# Pause de durée delai secondes   


def run(objs=Gui.Selection.getSelection(), dev=deviation):
    """
    fonction principale
    Arguments:
        doc : le document actif
        objs : les objets que l'on a au préalable sélectionnés
        dev : la valeur de la propriété déviation qui sera 
              imposée avant la génération du stl

    Returns
        Ne retourne rien

    Example:
        run()
    """

    # Boite de dialogue de ce que l'on veut faire
    formBoiteDialogueSTL=BoiteDialogueSTLGuiClass(mw)
    if not formBoiteDialogueSTL.exec_() : 
        return
    dev=formBoiteDialogueSTL.precision.value()
    # on mémorise les  solide sélectionnés et leur déviation,
    # on fait un touch() de leur child pour prise en compte dans recompute :
    dictionnaireOrigineDeviation=memoriseObjEtDeviation(objs,dev)
    if len(dictionnaireOrigineDeviation)==0: # si on a trouvé aucun solide
        QtWidgets.QMessageBox.information(mw, 'Attention',\
            '-Sélectionnez un ou plusieurs solide avant de lancer la macro')
        return
    # on récupère le nom du fichier où l'on devra enregistrer le stl
    fileStlName=nameFileStl(dictionnaireOrigineDeviation)
    if fileStlName == '' :
        return
    # l'export lui-même. On passe en paramètre les solides qui 
    # sont dans dictionnaireOrigineDeviation
    if formBoiteDialogueSTL.checkboxGenererSTL.isChecked():
       Mesh.export(list(dictionnaireOrigineDeviation.keys()), fileStlName) 
       print('Export fait')
    # restitution de la propriété deviation d'origine pour les solide :
    restitueDeviation(dictionnaireOrigineDeviation)
    recalcul(dictionnaireOrigineDeviation)

    if formBoiteDialogueSTL.checkboxLancerSTL.isChecked():
            #lance par exemple cura si cura a été associé aux fichiers *.stl
            openFile(fileStlName)

    lanceCommmandes(commandes,fileStlName,formBoiteDialogueSTL) # lance les commandes que l'on a décrites dans commandes

if __name__ == '__main__':
    run()

Macro Sketch Constraint From Spreadsheet :
https://wiki.freecad.org/Macro_Sketch_C ... adsheet/fr
openBrain
Veteran
Posts: 9041
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: Macro pour obtenir un stl avec des arrondis parfaits / Piloter octoprint

Post by openBrain »

Il y a dû y avoir un problème avec les 'quote' dans ton message. ;)
2cv001 wrote: Thu Dec 17, 2020 4:43 pm
* Je suis surpris que ça marche en fait car il te manque une ligne à la fin du initUI : 'self.setLayout(lay)'
Les mystères de l'informatique...
Je l'ai mis en dernière ligne mais je suppose que l'on peut le mettre où l'on veut après le ' lay = QtWidgets.QGridLayout(self) '
Exact
Bravo pour l'explication ! tout compris ! (enfin je crois :D) J'ai donc fait la modif. Du coup d'ailleurs, j'ai pu retirer QtGui dans l'import :
from PySide2 import QtCore, QtWidgets car sauf erreur, il n'y a pas d'endroit où j'avais besoin de QtGui
Par contre, j'ai toujours les messages "warning"
" butBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
17:07:52 C:/Datas/data/Imprimante 3D/Freecad/ExportStlV14.FCMacro:183: DeprecationWarning: an integer is required (got type PySide2.QtWidgets.QDialogButtonBox.StandardButton). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python."
Oui c'est "normal" que tu n'es pas de QtGui. En PySide2, il sert pour des choses graphiques mais pas liées au widget, genre manipulation d'images, du clavier, de la souris, ...

Après il ne te restera plus qu'à intégrer un mécanisme de traduction pour toucher un public international. :lol:
Post Reply