"""
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_()