""" Macro réalisée pour le logiciel FreeCAD Réalisée par UAICF Nevers-Vauzelles - section Modélisme www.modelisme58.fr Janvier 2025 """ import FreeCAD as App import FreeCADGui as Gui import os import TechDraw import math import FreeCADGui from PySide import QtGui #********************************************************************************** # Export des fichiers FreeCAD #********************************************************************************** def ensure_assemblage_directory(base_dir): """ Crée le répertoire Assemblage s'il n'existe pas """ assemblage_dir = os.path.join(base_dir, "Assemblage") if not os.path.exists(assemblage_dir): os.makedirs(assemblage_dir) print(f"Répertoire Assemblage créé : {assemblage_dir}") else: print(f"Répertoire Assemblage existant : {assemblage_dir}") return assemblage_dir def set_object_visibility(obj, visible=True): """ Configure la visibilité d'un objet et de ses enfants """ if hasattr(obj, "ViewObject"): obj.ViewObject.Visibility = visible # Pour les Body, rend visible tous les Pads if obj.TypeId == "PartDesign::Body" and hasattr(obj, "Group"): for sub_obj in obj.Group: if sub_obj.TypeId == "PartDesign::Pad": if hasattr(sub_obj, "ViewObject"): sub_obj.ViewObject.Visibility = visible def export_objects_to_files(): """ Exporte chaque Body et Box du document actif dans un fichier FreeCAD séparé """ # Récupère le document actif doc = App.activeDocument() if not doc: print("Erreur : Aucun document actif") return # Obtient le chemin du document actif if doc.FileName: base_dir = os.path.dirname(doc.FileName) else: print("Erreur : Veuillez d'abord sauvegarder le document") return # Crée ou vérifie le répertoire Assemblage export_dir = ensure_assemblage_directory(base_dir) # Compte les exports export_count = 0 # Parcourt tous les objets du document for obj in doc.Objects: # Vérifie si l'objet est un Body ou une Box if obj.TypeId in ["PartDesign::Body", "Part::Box"]: try: # Crée un nouveau document new_doc = App.newDocument() # Copie l'objet dans le nouveau document new_obj = new_doc.copyObject(obj, True) # S'assure que l'objet est visible set_object_visibility(new_obj, True) # Recalcule le document new_doc.recompute() # Génère le nom du fichier dans le répertoire Assemblage filename = os.path.join(export_dir, f"{obj.Label}.FCStd") # Sauvegarde le nouveau document new_doc.saveAs(filename) print(f"{obj.TypeId.split('::')[-1]} '{obj.Label}' exporté vers : {filename}") export_count += 1 # Ajuste la vue pour voir tout le contenu if Gui: Gui.SendMsgToActiveView("ViewFit") # Ferme le document temporaire App.closeDocument(new_doc.Name) except Exception as e: print(f"Erreur lors de l'export de '{obj.Label}': {str(e)}") print(f"\nExport terminé : {export_count} objet(s) exporté(s) dans {export_dir}") #********************************************************************************** # Export d'un fichier FreeCAD #********************************************************************************** def export_active_body_to_file(): """ Exporte le Body actif dans un fichier FreeCAD séparé """ # Récupère le document actif doc = App.activeDocument() if not doc: print("Erreur : Aucun document actif") return # Obtient le chemin du document actif if doc.FileName: base_dir = os.path.dirname(doc.FileName) else: print("Erreur : Veuillez d'abord sauvegarder le document") return # Vérifie la sélection dans l'interface selected_obj = Gui.Selection.getSelection() if not selected_obj or selected_obj[0].TypeId != "PartDesign::Body": print("Erreur : Veuillez sélectionner un Body valide") return active_body = selected_obj[0] print(f"Body sélectionné pour l'export : {active_body.Label}") # Crée ou vérifie le répertoire Assemblage export_dir = ensure_assemblage_directory(base_dir) try: # Crée un nouveau document new_doc = App.newDocument() # Copie l'objet dans le nouveau document new_obj = new_doc.copyObject(active_body, True) # S'assure que l'objet est visible if hasattr(new_obj, "ViewObject"): new_obj.ViewObject.Visibility = True # Recalcule le document new_doc.recompute() # Génère le nom du fichier dans le répertoire Assemblage filename = os.path.join(export_dir, f"{active_body.Label}.FCStd") # Sauvegarde le nouveau document new_doc.saveAs(filename) print(f"Body '{active_body.Label}' exporté vers : {filename}") # Ajuste la vue pour voir tout le contenu if Gui: Gui.SendMsgToActiveView("ViewFit") # Ferme le document temporaire App.closeDocument(new_doc.Name) except Exception as e: print(f"Erreur lors de l'export de '{active_body.Label}': {str(e)}") #********************************************************************************** # Création des feuilles de dessin #********************************************************************************** def get_sketch_orientation(sketch): """ Détermine dans quel plan (XY, XZ, YZ) est orienté un sketch en fonction de sa normale (axe Z local). """ # Vérifie si l'objet possède un Placement if not hasattr(sketch, "Placement"): print(f"L'objet {sketch.Label} n'a pas de placement.") return None # Récupère la rotation (placement) du sketch placement = sketch.Placement rotation = placement.Rotation # Transforme l'axe Z local (0, 0, 1) pour obtenir la normale globale z_axis = rotation.multVec(App.Vector(0, 0, 1)) # Tolérance pour les comparaisons tolerance = 1e-6 # Compare la normale transformée pour identifier le plan if math.isclose(z_axis.x, 0, abs_tol=tolerance) and math.isclose(z_axis.y, 0, abs_tol=tolerance) and math.isclose(z_axis.z, 1, abs_tol=tolerance): return "XY" elif math.isclose(z_axis.x, 0, abs_tol=tolerance) and math.isclose(z_axis.y, -1, abs_tol=tolerance) and math.isclose(z_axis.z, 0, abs_tol=tolerance): return "XZ" elif math.isclose(z_axis.x, 1, abs_tol=tolerance) and math.isclose(z_axis.y, 0, abs_tol=tolerance) and math.isclose(z_axis.z, 0, abs_tol=tolerance): return "YZ" else: return "Incliné ou non aligné avec un plan standard" def create_dxf_group(): print("---------------------------------------------") print("Création des feuilles de dessin avec TechDraw") # Récupère le document actif doc = App.activeDocument() if doc is None: print("Erreur : Aucun document actif") return None # Vérifie si le groupe DXF existe déjà for obj in doc.Objects: if obj.Name == "DXF" and obj.TypeId == "App::DocumentObjectGroup": print("Le groupe DXF existe déjà") return obj # Crée le groupe uniquement s'il n'existe pas dxf_group = doc.addObject("App::DocumentObjectGroup", "DXF") print("Groupe DXF créé avec succès") # Rafraîchit la vue doc.recompute() Gui.SendMsgToActiveView("ViewFit") return dxf_group def createTechDrawViewsForBodies(): """ Crée une feuille TechDraw avec une vue pour chaque objet BODY, si elle n'existe pas déjà. Les pages sont ajoutées au groupe DXF. """ Gui.activateWorkbench("TechDrawWorkbench") # Récupère le document actif doc = App.ActiveDocument if not doc: print("Aucun document actif") return # Crée ou récupère le groupe DXF dxf_group = create_dxf_group() if not dxf_group: print("Impossible de créer ou trouver le groupe DXF") return body_count = 0 skipped_count = 0 # Parcourt tous les objets du document for obj in doc.Objects: # Vérifie si l'objet est un BODY if obj.TypeId == "PartDesign::Body": # Vérifie si une page pour ce BODY existe déjà page_name = f"{obj.Label}_" existing_pages = [page for page in doc.Objects if page.TypeId == "TechDraw::DrawPage" and page.Label == page_name] if existing_pages: #print(f"Une feuille existe déjà pour {obj.Label}. Ignorée.") skipped_count += 1 continue try: body_count += 1 #print(f"Création des vues pour {obj.Label}:") # Sélectionne le BODY Gui.Selection.clearSelection() Gui.Selection.addSelection(doc.Name, obj.Name) # Crée une nouvelle page Gui.runCommand('TechDraw_PageDefault', 0) doc.recompute() # Trouve la dernière page créée pages = [o for o in doc.Objects if o.TypeId == "TechDraw::DrawPage"] if pages: current_page = pages[-1] # Ajoute la page au groupe DXF dxf_group.addObject(current_page) # Crée la vue pour le BODY view = doc.addObject('TechDraw::DrawProjGroupItem', f'{obj.Label}') current_page.addView(view) # Détermine l'orientation à partir du Sketch principal du Body sketches = [child for child in obj.Group if child.TypeId == "Sketcher::SketchObject"] if sketches: # Utilise le premier sketch trouvé pour déterminer l'orientation orientation = get_sketch_orientation(sketches[0]) #print(f"Orientation déterminée à partir du sketch '{sketches[0].Label}' : {orientation}") else: print(f"Aucun sketch trouvé dans le Body '{obj.Label}'. Utilisation de l'orientation par défaut.") orientation = "XY" # Orientation par défaut # Applique l'orientation à la vue if orientation == "XY": view.Direction = App.Vector(0.0, 0.0, 1.0) view.XDirection = App.Vector(1.0, 0.0, 0.0) elif orientation == "XZ": view.Direction = App.Vector(0.0, -1.0, 0.0) view.XDirection = App.Vector(1.0, 0.0, 0.0) elif orientation == "YZ": view.Direction = App.Vector(1.0, 0.0, 0.0) view.XDirection = App.Vector(0.0, 1.0, 0.0) else: #print(f"{obj.Label} est orienté dans un plan non standard ou incliné.") view.Direction = App.Vector(0.0, 0.0, 1.0) view.XDirection = App.Vector(1.0, 0.0, 0.0) view.Source = [obj] # Renomme la page avec le nom du BODY current_page.Label = page_name #print(f"Vue créée pour {obj.Label}") doc.recompute() else: print("Impossible de créer une page TechDraw") except Exception as e: print(f"Erreur lors de la création de la vue pour {obj.Label}: {str(e)}") print("\nRésumé :") print(f"Vues créées pour {body_count} FEUILLE(s).") print(f"{skipped_count} FEUILLE(s) déjà existantes ont été ignorées.") doc.recompute() #********************************************************************************** # Tri des feuilles de dessin #********************************************************************************** def sort_techdraw_sheets(): """ Trie les feuilles TechDraw par ordre alphabétique dans le groupe DXF et ferme uniquement les onglets des feuilles TechDraw. """ doc = FreeCAD.ActiveDocument if not doc: FreeCAD.Console.PrintError("Aucun document actif!\n") return # Chercher le groupe DXF dxf_group = None for obj in doc.Objects: if obj.TypeId == 'App::DocumentObjectGroup' and obj.Label == 'DXF': dxf_group = obj break if not dxf_group: FreeCAD.Console.PrintError("Groupe DXF non trouvé!\n") return # Récupérer toutes les feuilles TechDraw dans le groupe DXF sheets = [obj for obj in dxf_group.Group if obj.TypeId == 'TechDraw::DrawPage'] if not sheets: FreeCAD.Console.PrintMessage("Aucune feuille TechDraw trouvée dans le groupe DXF.\n") return # Trier les feuilles par leur nom sorted_sheets = sorted(sheets, key=lambda x: x.Label.lower()) # Réorganiser les feuilles dans le groupe dxf_group.Group = sorted_sheets doc.recompute() FreeCAD.Console.PrintMessage(f"{len(sheets)} feuilles ont été triées alphabétiquement dans le groupe DXF.\n") FreeCAD.Console.PrintMessage("Les onglets des feuilles TechDraw ont été fermés.\n") def close_techdraw_sheets(): """ Ferme uniquement les onglets des feuilles TechDraw. """ mw = FreeCADGui.getMainWindow() count = 0 for window in mw.findChildren(QtGui.QMdiSubWindow): if window.windowTitle().endswith(': Page'): # Identifie les fenêtres TechDraw window.close() count += 1 FreeCAD.Console.PrintMessage(f"{count} feuilles TechDraw ont été fermées.\n") #********************************************************************************** # Création des fichiers DXF #********************************************************************************** def exportTechDrawToDXF(): """ Exporte toutes les feuilles TechDraw au format DXF. Le nom du fichier DXF correspond au label de la feuille. """ print("-----------------------------------------") print("Démarrage de l'export des fichiers DXF...") # Récupère le document actif doc = FreeCAD.ActiveDocument if not doc: print("Aucun document actif") return # Crée le nom du dossier avec la date et l'heure now = datetime.now() # export_dirname = f"DXF_export_{now.strftime('%Y%m%d_%H%M%S')}" export_dirname = f"DXF_export" # Crée le dossier d'export dans le même répertoire que le fichier FCStd doc_path = os.path.dirname(doc.FileName) export_dir = os.path.join(doc_path, export_dirname) if not os.path.exists(export_dir): os.makedirs(export_dir) print(f"Création du dossier : {export_dirname}") # Parcourt tous les objets du document for obj in doc.Objects: # Vérifie si l'objet est une feuille TechDraw if obj.TypeId == "TechDraw::DrawPage": try: # Utilise le label de la feuille comme nom de fichier filename = obj.Label # Nettoie le nom de fichier des caractères non valides filename = "".join(c for c in filename if c.isalnum() or c in (' ', '-', '_')) # Crée le chemin complet du fichier filepath = os.path.join(export_dir, filename + ".dxf") # Utilise la méthode writeDXFPage pour l'export TechDraw.writeDXFPage(obj, filepath) #print(f"Feuille '{obj.Label}' exportée vers : {filepath}") print(f"Feuille '{obj.Label}'") except Exception as e: print(f"Erreur lors de l'export de '{obj.Label}' : {str(e)}") print(f"Export terminé vers : {filepath}") #********************************************************************************** # Affichage ce la boite de dialogue #********************************************************************************** from PySide2 import QtWidgets, QtCore class MacroSelector(QtWidgets.QDialog): def __init__(self): super().__init__() self.setWindowTitle("Assistant automatisé") self.resize(500, 150) layout = QtWidgets.QVBoxLayout() # Texte label = QtWidgets.QLabel() label.setText('Des outils pour automatiser la création de fichiers,') layout.addWidget(label) label = QtWidgets.QLabel() label.setText('de feuilles de dessins et de fichiers DXF') layout.addWidget(label) layout.addWidget(QtWidgets.QLabel("")) # Ajoute un espace vide label = QtWidgets.QLabel() label.setText('Ces outils ont utilisés dans le cadre de dessins de pièces en 3D.') layout.addWidget(label) label = QtWidgets.QLabel() label.setText("On peut réaliser un assemblage des pièces avec l'atelier A2Plus pour ensuite les exporter au format DXF") layout.addWidget(label) label = QtWidgets.QLabel() label.setText("en vue de les découper au laser ou avec une machine à commande numérique (CNC).") layout.addWidget(label) layout.addWidget(QtWidgets.QLabel("")) # Ajoute un espace vide label = QtWidgets.QLabel('Choisir l\'application à utiliser ?') layout.addWidget(label) # Modification du texte du bouton pour refléter le nom de la macro self.btn1 = QtWidgets.QPushButton("Exporte l'objet actif CORPS dans un fichier \'FCStd\' dans le dossier Assemblage") self.btn2 = QtWidgets.QPushButton("Exporte TOUS les objets individuellement et tout regrouper dans le dossier Assemblage") self.btn3 = QtWidgets.QPushButton("Crée toutes les feuilles de dessin (Atelier \'TechDraw\')") self.btn4 = QtWidgets.QPushButton("Crée TOUS les fichiers dans un dossier DXF (Exporter la page au format \'DXF\')") self.btn1.clicked.connect(lambda: self.launch_macro(1)) self.btn2.clicked.connect(lambda: self.launch_macro(2)) self.btn3.clicked.connect(lambda: self.launch_macro(3)) self.btn4.clicked.connect(lambda: self.launch_macro(4)) layout.addWidget(QtWidgets.QLabel("")) # Ajoute un espace vide layout.addWidget(self.btn1) layout.addWidget(self.btn2) layout.addWidget(QtWidgets.QLabel("")) # Ajoute un espace vide layout.addWidget(self.btn3) layout.addWidget(QtWidgets.QLabel("")) # Ajoute un espace vide layout.addWidget(self.btn4) layout.addWidget(QtWidgets.QLabel("")) # Ajoute un espace vide # Bouton Quitter quit_button = QtWidgets.QPushButton("Quitter") quit_button.clicked.connect(self.reject) layout.addWidget(quit_button) # Texte avec lien URL layout.addWidget(QtWidgets.QLabel("")) # Ajoute un espace vide label = QtWidgets.QLabel() label.setText('Réalisé par UAICF Nevers-Vauzelles - www.modelisme58.fr - janvier 2025') label.setOpenExternalLinks(True) # Active les liens externes layout.addWidget(label) self.setLayout(layout) def launch_macro(self, macro_number): if macro_number == 1: # Exécute la fonction export_active_body_to_file() if macro_number == 2: # Exécute la fonction export_objects_to_files() if macro_number == 3: # Exécute la fonction createTechDrawViewsForBodies() sort_techdraw_sheets() close_techdraw_sheets() if macro_number == 4: # Exécute la fonction exportTechDrawToDXF() self.accept() # Créer et afficher la boîte de dialogue dialog = MacroSelector() dialog.exec_()