Initial commit: IMP3D Corrector - XYZ correction for 3D printers
Features: - G-code parser for movement commands - XYZ corrector with skew, scale, offset, and rotation - Simple calibration (1 square) and advanced calibration (5 squares) - Multi-point measurement analysis - Profile management (create, edit, delete, list) - CLI interface with interactive calibration Generated with Crush Assisted-by: GLM-5 via Crush <crush@charm.land>
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Hidden folders
|
||||||
|
.crush/
|
||||||
|
.git/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
*.gcode
|
||||||
|
!example/*.gcode
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
125
README.md
Normal file
125
README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# IMP3D Corrector
|
||||||
|
|
||||||
|
Correction de plan XYZ pour imprimantes 3D avec problèmes de calibration.
|
||||||
|
|
||||||
|
## Problèmes résolus
|
||||||
|
|
||||||
|
- **Skew X-Y**: Axes X et Y non perpendiculaires
|
||||||
|
- **Erreurs d'échelle**: Dimensions incorrectes sur les bords
|
||||||
|
- **Décalage d'origine**: Position zéro mal alignée
|
||||||
|
- **Rotation du plateau**: Léger angle du plateau
|
||||||
|
- **Asymétries**: Différences de dimensions selon la position
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
.venv/bin/pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
### Calibration simple (1 carré)
|
||||||
|
|
||||||
|
Pour une calibration rapide:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Générer le G-code de calibration
|
||||||
|
imp3d-corrector calibrate -w 220 -d 220 -o calibration.gcode
|
||||||
|
|
||||||
|
# Imprimer, mesurer, puis créer le profil
|
||||||
|
imp3d-corrector create ma_printer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calibration avancée (5 carrés) - RECOMMANDÉE
|
||||||
|
|
||||||
|
Pour une calibration précise avec détection des problèmes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Générer le G-code avec 5 carrés (coins + centre)
|
||||||
|
imp3d-corrector calibrate-advanced -w 220 -d 220 -o calibration_advanced.gcode
|
||||||
|
|
||||||
|
# 2. Imprimer le fichier
|
||||||
|
|
||||||
|
# 3. Mesurer chaque carré et créer le profil
|
||||||
|
imp3d-corrector create-advanced ma_printer
|
||||||
|
```
|
||||||
|
|
||||||
|
Le système analysera:
|
||||||
|
- **Échelle moyenne** (compensation globale)
|
||||||
|
- **Skew** (angle entre X et Y)
|
||||||
|
- **Asymétrie** (différence gauche/droite, haut/bas)
|
||||||
|
- **Variance** (uniformité des dimensions sur le plateau)
|
||||||
|
|
||||||
|
### Analyser sans créer de profil
|
||||||
|
|
||||||
|
```bash
|
||||||
|
imp3d-corrector analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
### Appliquer les corrections
|
||||||
|
|
||||||
|
```bash
|
||||||
|
imp3d-corrector correct mon_fichier.gcode -p ma_printer -o fichier_corrigé.gcode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestion des profils
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lister les profils
|
||||||
|
imp3d-corrector list
|
||||||
|
|
||||||
|
# Afficher un profil
|
||||||
|
imp3d-corrector show ma_printer
|
||||||
|
|
||||||
|
# Modifier un profil
|
||||||
|
imp3d-corrector edit ma_printer
|
||||||
|
|
||||||
|
# Supprimer un profil
|
||||||
|
imp3d-corrector delete ma_printer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
imp3d_corrector/
|
||||||
|
├── cli.py # Interface en ligne de commande
|
||||||
|
├── config/
|
||||||
|
│ └── profile_manager.py # Gestion des profils
|
||||||
|
└── core/
|
||||||
|
├── corrector.py # Logique de correction XYZ
|
||||||
|
├── gcode_parser.py # Parser G-code
|
||||||
|
└── advanced_calibration.py # Calibration multi-points
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow recommandé
|
||||||
|
|
||||||
|
1. **Première calibration**: Utilisez `calibrate-advanced` pour avoir une vue complète
|
||||||
|
2. **Analysez** les résultats - si des problèmes d'asymétrie sont détectés, vérifiez votre matériel
|
||||||
|
3. **Créez le profil** avec les corrections calculées
|
||||||
|
4. **Testez** avec un carré simple pour valider
|
||||||
|
|
||||||
|
## Exemple de configuration YAML
|
||||||
|
|
||||||
|
Les profils sont stockés dans `~/.config/imp3d_corrector/profiles/`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: ma_printer
|
||||||
|
skew_xy: 0.5
|
||||||
|
offset_x: 0.0
|
||||||
|
offset_y: 0.0
|
||||||
|
scale_x: 1.002
|
||||||
|
scale_y: 0.998
|
||||||
|
rotation: 0.0
|
||||||
|
bed_width: 220.0
|
||||||
|
bed_depth: 220.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interprétation des résultats
|
||||||
|
|
||||||
|
| Problème détecté | Cause probable | Solution |
|
||||||
|
|-----------------|----------------|----------|
|
||||||
|
| Skew élevé | Axes non perpendiculaires | Ajustement mécanique ou compensation logicielle |
|
||||||
|
| Asymétrie X | Courroie X détendue d'un côté | Vérifier la tension de courroie |
|
||||||
|
| Asymétrie Y | Courroie Y détendue d'un côté | Vérifier la tension de courroie |
|
||||||
|
| Variance élevée | Problème de linéarité | Vérifier les rails/roulements |
|
||||||
10
config.example.yaml
Normal file
10
config.example.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: example_printer
|
||||||
|
skew_xy: 0.5
|
||||||
|
offset_x: 0.0
|
||||||
|
offset_y: 0.0
|
||||||
|
scale_x: 1.002
|
||||||
|
scale_y: 0.998
|
||||||
|
rotation: 0.0
|
||||||
|
bed_width: 220.0
|
||||||
|
bed_depth: 220.0
|
||||||
|
calibration_points: []
|
||||||
3
imp3d_corrector/__init__.py
Normal file
3
imp3d_corrector/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""IMP3D Corrector - Correction de plan XYZ pour imprimantes 3D."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
412
imp3d_corrector/cli.py
Normal file
412
imp3d_corrector/cli.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""CLI pour IMP3D Corrector - Correction de plan XYZ pour imprimantes 3D."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from imp3d_corrector.config.profile_manager import ProfileManager
|
||||||
|
from imp3d_corrector.core.corrector import BedCalibration, XYZCorrector
|
||||||
|
from imp3d_corrector.core.advanced_calibration import (
|
||||||
|
AdvancedCalibrationGCode, MultiPointCalibration,
|
||||||
|
SquareMeasurement, CalibrationPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_create_profile(args):
|
||||||
|
"""Crée un nouveau profil de calibration."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
|
||||||
|
if manager.profile_exists(args.name):
|
||||||
|
overwrite = input(f"Le profil '{args.name}' existe déjà. Écraser? (o/N): ")
|
||||||
|
if not overwrite.lower().startswith('o'):
|
||||||
|
print("Annulé.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
calibration = manager.create_interactive(args.name)
|
||||||
|
path = manager.save_profile(calibration)
|
||||||
|
print(f"\n✓ Profil sauvegardé: {path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_edit_profile(args):
|
||||||
|
"""Modifie un profil existant."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
|
||||||
|
if not manager.profile_exists(args.name):
|
||||||
|
print(f"Erreur: Le profil '{args.name}' n'existe pas.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
calibration = manager.edit_profile(args.name)
|
||||||
|
if calibration:
|
||||||
|
path = manager.save_profile(calibration)
|
||||||
|
print(f"\n✓ Profil mis à jour: {path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list_profiles(args):
|
||||||
|
"""Liste tous les profils disponibles."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
profiles = manager.list_profiles()
|
||||||
|
|
||||||
|
if not profiles:
|
||||||
|
print("Aucun profil trouvé.")
|
||||||
|
print("Créez-en un avec: imp3d-corrector create <nom>")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("Profils disponibles:")
|
||||||
|
for name in profiles:
|
||||||
|
cal = manager.load_profile(name)
|
||||||
|
print(f" • {name}")
|
||||||
|
print(f" Plateau: {cal.bed_width}x{cal.bed_depth}mm")
|
||||||
|
print(f" Skew: {cal.skew_xy:.2f}°, Scale: {cal.scale_x:.4f}/{cal.scale_y:.4f}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_delete_profile(args):
|
||||||
|
"""Supprime un profil."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
|
||||||
|
if not manager.profile_exists(args.name):
|
||||||
|
print(f"Erreur: Le profil '{args.name}' n'existe pas.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
confirm = input(f"Supprimer le profil '{args.name}'? (o/N): ")
|
||||||
|
if confirm.lower().startswith('o'):
|
||||||
|
manager.delete_profile(args.name)
|
||||||
|
print(f"✓ Profil '{args.name}' supprimé.")
|
||||||
|
else:
|
||||||
|
print("Annulé.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_show_profile(args):
|
||||||
|
"""Affiche les détails d'un profil."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
|
||||||
|
if not manager.profile_exists(args.name):
|
||||||
|
print(f"Erreur: Le profil '{args.name}' n'existe pas.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
cal = manager.load_profile(args.name)
|
||||||
|
print(f"\n=== Profil: {cal.name} ===\n")
|
||||||
|
print(f"Dimensions du plateau: {cal.bed_width} x {cal.bed_depth} mm")
|
||||||
|
print(f"\nCorrections:")
|
||||||
|
print(f" Offset X: {cal.offset_x:.3f} mm")
|
||||||
|
print(f" Offset Y: {cal.offset_y:.3f} mm")
|
||||||
|
print(f" Scale X: {cal.scale_x:.6f}")
|
||||||
|
print(f" Scale Y: {cal.scale_y:.6f}")
|
||||||
|
print(f" Skew: {cal.skew_xy:.3f}°")
|
||||||
|
print(f" Rotation: {cal.rotation:.3f}°")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_correct(args):
|
||||||
|
"""Applique la correction à un fichier G-code."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
|
||||||
|
if not manager.profile_exists(args.profile):
|
||||||
|
print(f"Erreur: Le profil '{args.profile}' n'existe pas.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
input_path = Path(args.input)
|
||||||
|
if not input_path.exists():
|
||||||
|
print(f"Erreur: Le fichier '{args.input}' n'existe pas.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Déterminer le fichier de sortie
|
||||||
|
if args.output:
|
||||||
|
output_path = Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = input_path.with_name(f'{input_path.stem}_corrected{input_path.suffix}')
|
||||||
|
|
||||||
|
calibration = manager.load_profile(args.profile)
|
||||||
|
corrector = XYZCorrector(calibration)
|
||||||
|
|
||||||
|
print(f"Application de la correction avec le profil '{args.profile}'...")
|
||||||
|
print(f" Entrée: {input_path}")
|
||||||
|
print(f" Sortie: {output_path}")
|
||||||
|
|
||||||
|
corrector.correct_gcode_file(str(input_path), str(output_path))
|
||||||
|
|
||||||
|
print(f"\n✓ Correction appliquée: {output_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_generate_calibration(args):
|
||||||
|
"""Génère un G-code de calibration."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
|
||||||
|
if args.profile and manager.profile_exists(args.profile):
|
||||||
|
calibration = manager.load_profile(args.profile)
|
||||||
|
else:
|
||||||
|
calibration = BedCalibration(
|
||||||
|
name="calibration",
|
||||||
|
bed_width=args.width or 220,
|
||||||
|
bed_depth=args.depth or 220
|
||||||
|
)
|
||||||
|
|
||||||
|
corrector = XYZCorrector(calibration)
|
||||||
|
gcode = corrector.generate_correction_gcode()
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
output_path = Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = Path('calibration_square.gcode')
|
||||||
|
|
||||||
|
with open(output_path, 'w') as f:
|
||||||
|
f.write(gcode)
|
||||||
|
|
||||||
|
print(f"✓ G-code de calibration généré: {output_path}")
|
||||||
|
print(" 1. Imprimez ce fichier")
|
||||||
|
print(" 2. Mesurez le carré (côtés X, Y et diagonale)")
|
||||||
|
print(" 3. Créez un profil avec ces mesures: imp3d-corrector create <nom>")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_generate_advanced_calibration(args):
|
||||||
|
"""Génère un G-code de calibration avancé (5 carrés)."""
|
||||||
|
generator = AdvancedCalibrationGCode(
|
||||||
|
bed_width=args.width or 220,
|
||||||
|
bed_depth=args.depth or 220,
|
||||||
|
square_size=args.size or 30,
|
||||||
|
margin=args.margin or 15
|
||||||
|
)
|
||||||
|
|
||||||
|
gcode = generator.generate()
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
output_path = Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = Path('calibration_advanced.gcode')
|
||||||
|
|
||||||
|
with open(output_path, 'w') as f:
|
||||||
|
f.write(gcode)
|
||||||
|
|
||||||
|
print(f"✓ G-code de calibration avancé généré: {output_path}")
|
||||||
|
print(f" 5 carrés de {args.size or 30}mm seront imprimés")
|
||||||
|
print(" Positions: 4 coins + centre")
|
||||||
|
print("\n 1. Imprimez ce fichier")
|
||||||
|
print(" 2. Mesurez CHAQUE carré (X, Y, diagonale)")
|
||||||
|
print(" 3. Créez un profil: imp3d-corrector create-advanced <nom>")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_create_advanced_profile(args):
|
||||||
|
"""Crée un profil à partir de mesures multi-points."""
|
||||||
|
manager = ProfileManager()
|
||||||
|
|
||||||
|
if manager.profile_exists(args.name):
|
||||||
|
overwrite = input(f"Le profil '{args.name}' existe déjà. Écraser? (o/N): ")
|
||||||
|
if not overwrite.lower().startswith('o'):
|
||||||
|
print("Annulé.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"CRÉATION DE PROFIL AVANCÉ: {args.name}")
|
||||||
|
print(f"{'='*50}\n")
|
||||||
|
|
||||||
|
# Paramètres du plateau
|
||||||
|
print("Dimensions du plateau:")
|
||||||
|
bed_width = float(input(" Largeur X (mm) [220]: ") or "220")
|
||||||
|
bed_depth = float(input(" Profondeur Y (mm) [220]: ") or "220")
|
||||||
|
square_size = float(input(" Taille des carrés de test (mm) [30]: ") or "30")
|
||||||
|
|
||||||
|
calibration = MultiPointCalibration(
|
||||||
|
bed_width=bed_width,
|
||||||
|
bed_depth=bed_depth,
|
||||||
|
square_size=square_size
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print("MESURES DES CARRÉS")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
print(f"\nPour chaque carré, entrez les mesures.")
|
||||||
|
print(f"Taille attendue: {square_size} mm")
|
||||||
|
print(f"Diagonale attendue: {square_size * 1.414:.2f} mm\n")
|
||||||
|
|
||||||
|
positions_order = [
|
||||||
|
(CalibrationPosition.BOTTOM_LEFT, "Bas-Gauche (BL)"),
|
||||||
|
(CalibrationPosition.BOTTOM_RIGHT, "Bas-Droite (BR)"),
|
||||||
|
(CalibrationPosition.CENTER, "Centre (C)"),
|
||||||
|
(CalibrationPosition.TOP_LEFT, "Haut-Gauche (TL)"),
|
||||||
|
(CalibrationPosition.TOP_RIGHT, "Haut-Droite (TR)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pos, label in positions_order:
|
||||||
|
print(f"\n--- {label} ---")
|
||||||
|
measured_x = float(input(f" Côté X (mm): "))
|
||||||
|
measured_y = float(input(f" Côté Y (mm): "))
|
||||||
|
measured_diag = float(input(f" Diagonale (mm): "))
|
||||||
|
|
||||||
|
measurement = SquareMeasurement(
|
||||||
|
position=pos,
|
||||||
|
expected_size=square_size,
|
||||||
|
measured_x=measured_x,
|
||||||
|
measured_y=measured_y,
|
||||||
|
measured_diagonal=measured_diag
|
||||||
|
)
|
||||||
|
calibration.add_measurement(measurement)
|
||||||
|
|
||||||
|
# Afficher le rapport
|
||||||
|
print(calibration.generate_report())
|
||||||
|
|
||||||
|
# Obtenir les corrections
|
||||||
|
corrections = calibration.get_corrections()
|
||||||
|
|
||||||
|
if corrections['issues']:
|
||||||
|
print("\n⚠ Des problèmes ont été détectés.")
|
||||||
|
print(" Les corrections globales peuvent ne pas être optimales.")
|
||||||
|
print(" Envisagez des corrections manuelles si nécessaire.\n")
|
||||||
|
|
||||||
|
# Créer le profil
|
||||||
|
bed_cal = BedCalibration(
|
||||||
|
name=args.name,
|
||||||
|
skew_xy=corrections['skew_xy'],
|
||||||
|
scale_x=corrections['scale_x'],
|
||||||
|
scale_y=corrections['scale_y'],
|
||||||
|
bed_width=bed_width,
|
||||||
|
bed_depth=bed_depth
|
||||||
|
)
|
||||||
|
|
||||||
|
# Option pour ajustements manuels
|
||||||
|
manual = input("\nAjustements manuels supplémentaires? (o/N): ").lower().startswith('o')
|
||||||
|
|
||||||
|
if manual:
|
||||||
|
bed_cal.offset_x = float(input(f" Offset X (mm) [{bed_cal.offset_x}]: ") or bed_cal.offset_x)
|
||||||
|
bed_cal.offset_y = float(input(f" Offset Y (mm) [{bed_cal.offset_y}]: ") or bed_cal.offset_y)
|
||||||
|
bed_cal.rotation = float(input(f" Rotation (°) [{bed_cal.rotation}]: ") or bed_cal.rotation)
|
||||||
|
|
||||||
|
path = manager.save_profile(bed_cal)
|
||||||
|
print(f"\n✓ Profil sauvegardé: {path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_analyze_calibration(args):
|
||||||
|
"""Analyse les mesures de calibration sans créer de profil."""
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print("ANALYSE DE CALIBRATION")
|
||||||
|
print(f"{'='*50}\n")
|
||||||
|
|
||||||
|
bed_width = float(input("Largeur plateau X (mm) [220]: ") or "220")
|
||||||
|
bed_depth = float(input("Profondeur plateau Y (mm) [220]: ") or "220")
|
||||||
|
square_size = float(input("Taille des carrés (mm) [30]: ") or "30")
|
||||||
|
|
||||||
|
calibration = MultiPointCalibration(
|
||||||
|
bed_width=bed_width,
|
||||||
|
bed_depth=bed_depth,
|
||||||
|
square_size=square_size
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\nEntrez les mesures (ou 'skip' pour ignorer une position):\n")
|
||||||
|
|
||||||
|
positions_order = [
|
||||||
|
(CalibrationPosition.BOTTOM_LEFT, "Bas-Gauche (BL)"),
|
||||||
|
(CalibrationPosition.BOTTOM_RIGHT, "Bas-Droite (BR)"),
|
||||||
|
(CalibrationPosition.CENTER, "Centre (C)"),
|
||||||
|
(CalibrationPosition.TOP_LEFT, "Haut-Gauche (TL)"),
|
||||||
|
(CalibrationPosition.TOP_RIGHT, "Haut-Droite (TR)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pos, label in positions_order:
|
||||||
|
print(f"\n--- {label} ---")
|
||||||
|
x_input = input(f" Côté X (mm) [skip]: ")
|
||||||
|
if x_input.lower() == 'skip':
|
||||||
|
continue
|
||||||
|
y_input = input(f" Côté Y (mm): ")
|
||||||
|
diag_input = input(f" Diagonale (mm): ")
|
||||||
|
|
||||||
|
try:
|
||||||
|
measurement = SquareMeasurement(
|
||||||
|
position=pos,
|
||||||
|
expected_size=square_size,
|
||||||
|
measured_x=float(x_input),
|
||||||
|
measured_y=float(y_input),
|
||||||
|
measured_diagonal=float(diag_input)
|
||||||
|
)
|
||||||
|
calibration.add_measurement(measurement)
|
||||||
|
except ValueError:
|
||||||
|
print(" → Ignoré (valeurs invalides)")
|
||||||
|
|
||||||
|
print(calibration.generate_report())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='imp3d-corrector',
|
||||||
|
description='Correction de plan XYZ pour imprimantes 3D'
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest='command', help='Commandes disponibles')
|
||||||
|
|
||||||
|
# Commande: create
|
||||||
|
create_parser = subparsers.add_parser('create', help='Créer un nouveau profil')
|
||||||
|
create_parser.add_argument('name', help='Nom du profil')
|
||||||
|
create_parser.set_defaults(func=cmd_create_profile)
|
||||||
|
|
||||||
|
# Commande: edit
|
||||||
|
edit_parser = subparsers.add_parser('edit', help='Modifier un profil existant')
|
||||||
|
edit_parser.add_argument('name', help='Nom du profil')
|
||||||
|
edit_parser.set_defaults(func=cmd_edit_profile)
|
||||||
|
|
||||||
|
# Commande: list
|
||||||
|
list_parser = subparsers.add_parser('list', help='Lister les profils')
|
||||||
|
list_parser.set_defaults(func=cmd_list_profiles)
|
||||||
|
|
||||||
|
# Commande: delete
|
||||||
|
delete_parser = subparsers.add_parser('delete', help='Supprimer un profil')
|
||||||
|
delete_parser.add_argument('name', help='Nom du profil')
|
||||||
|
delete_parser.set_defaults(func=cmd_delete_profile)
|
||||||
|
|
||||||
|
# Commande: show
|
||||||
|
show_parser = subparsers.add_parser('show', help='Afficher un profil')
|
||||||
|
show_parser.add_argument('name', help='Nom du profil')
|
||||||
|
show_parser.set_defaults(func=cmd_show_profile)
|
||||||
|
|
||||||
|
# Commande: correct
|
||||||
|
correct_parser = subparsers.add_parser('correct', help='Appliquer la correction à un G-code')
|
||||||
|
correct_parser.add_argument('input', help='Fichier G-code à corriger')
|
||||||
|
correct_parser.add_argument('-p', '--profile', required=True, help='Profil à utiliser')
|
||||||
|
correct_parser.add_argument('-o', '--output', help='Fichier de sortie (défaut: *_corrected.gcode)')
|
||||||
|
correct_parser.set_defaults(func=cmd_correct)
|
||||||
|
|
||||||
|
# Commande: calibrate
|
||||||
|
calibrate_parser = subparsers.add_parser('calibrate', help='Générer un G-code de calibration simple (1 carré)')
|
||||||
|
calibrate_parser.add_argument('-p', '--profile', help='Profil existant à utiliser')
|
||||||
|
calibrate_parser.add_argument('-w', '--width', type=float, help='Largeur du plateau (mm)')
|
||||||
|
calibrate_parser.add_argument('-d', '--depth', type=float, help='Profondeur du plateau (mm)')
|
||||||
|
calibrate_parser.add_argument('-o', '--output', help='Fichier de sortie')
|
||||||
|
calibrate_parser.set_defaults(func=cmd_generate_calibration)
|
||||||
|
|
||||||
|
# Commande: calibrate-advanced
|
||||||
|
cal_adv_parser = subparsers.add_parser('calibrate-advanced',
|
||||||
|
help='Générer un G-code de calibration avancé (5 carrés)')
|
||||||
|
cal_adv_parser.add_argument('-w', '--width', type=float, help='Largeur du plateau (mm)')
|
||||||
|
cal_adv_parser.add_argument('-d', '--depth', type=float, help='Profondeur du plateau (mm)')
|
||||||
|
cal_adv_parser.add_argument('-s', '--size', type=float, help='Taille des carrés (mm) [30]')
|
||||||
|
cal_adv_parser.add_argument('-m', '--margin', type=float, help='Marge depuis les bords (mm) [15]')
|
||||||
|
cal_adv_parser.add_argument('-o', '--output', help='Fichier de sortie')
|
||||||
|
cal_adv_parser.set_defaults(func=cmd_generate_advanced_calibration)
|
||||||
|
|
||||||
|
# Commande: create-advanced
|
||||||
|
create_adv_parser = subparsers.add_parser('create-advanced',
|
||||||
|
help='Créer un profil depuis mesures multi-points')
|
||||||
|
create_adv_parser.add_argument('name', help='Nom du profil')
|
||||||
|
create_adv_parser.set_defaults(func=cmd_create_advanced_profile)
|
||||||
|
|
||||||
|
# Commande: analyze
|
||||||
|
analyze_parser = subparsers.add_parser('analyze',
|
||||||
|
help='Analyser des mesures de calibration (sans créer de profil)')
|
||||||
|
analyze_parser.set_defaults(func=cmd_analyze_calibration)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command is None:
|
||||||
|
parser.print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
1
imp3d_corrector/config/__init__.py
Normal file
1
imp3d_corrector/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Configuration module for bed profiles."""
|
||||||
127
imp3d_corrector/config/profile_manager.py
Normal file
127
imp3d_corrector/config/profile_manager.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"""Gestion des profils de plateau pour imprimantes 3D."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from ..core.corrector import BedCalibration
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileManager:
|
||||||
|
"""Gère les profils de calibration des plateaux."""
|
||||||
|
|
||||||
|
def __init__(self, config_dir: Optional[str] = None):
|
||||||
|
"""Initialise le gestionnaire de profils.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_dir: Répertoire de stockage des profils.
|
||||||
|
Par défaut: ~/.config/imp3d_corrector/profiles/
|
||||||
|
"""
|
||||||
|
if config_dir:
|
||||||
|
self.config_dir = Path(config_dir)
|
||||||
|
else:
|
||||||
|
self.config_dir = Path.home() / '.config' / 'imp3d_corrector' / 'profiles'
|
||||||
|
|
||||||
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def list_profiles(self) -> List[str]:
|
||||||
|
"""Retourne la liste des profils disponibles."""
|
||||||
|
profiles = []
|
||||||
|
for f in self.config_dir.glob('*.yaml'):
|
||||||
|
profiles.append(f.stem)
|
||||||
|
return sorted(profiles)
|
||||||
|
|
||||||
|
def load_profile(self, name: str) -> BedCalibration:
|
||||||
|
"""Charge un profil par son nom."""
|
||||||
|
filepath = self.config_dir / f'{name}.yaml'
|
||||||
|
if not filepath.exists():
|
||||||
|
raise FileNotFoundError(f"Profil '{name}' non trouvé")
|
||||||
|
return BedCalibration.from_yaml(str(filepath))
|
||||||
|
|
||||||
|
def save_profile(self, calibration: BedCalibration) -> str:
|
||||||
|
"""Sauvegarde un profil."""
|
||||||
|
filepath = self.config_dir / f'{calibration.name}.yaml'
|
||||||
|
calibration.to_yaml(str(filepath))
|
||||||
|
return str(filepath)
|
||||||
|
|
||||||
|
def delete_profile(self, name: str) -> bool:
|
||||||
|
"""Supprime un profil."""
|
||||||
|
filepath = self.config_dir / f'{name}.yaml'
|
||||||
|
if filepath.exists():
|
||||||
|
filepath.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def profile_exists(self, name: str) -> bool:
|
||||||
|
"""Vérifie si un profil existe."""
|
||||||
|
return (self.config_dir / f'{name}.yaml').exists()
|
||||||
|
|
||||||
|
def get_profile_path(self, name: str) -> Path:
|
||||||
|
"""Retourne le chemin du fichier de profil."""
|
||||||
|
return self.config_dir / f'{name}.yaml'
|
||||||
|
|
||||||
|
def create_interactive(self, name: str) -> BedCalibration:
|
||||||
|
"""Crée un profil de manière interactive (pour utilisation CLI)."""
|
||||||
|
print(f"\n=== Création du profil: {name} ===\n")
|
||||||
|
|
||||||
|
print("Dimensions du plateau:")
|
||||||
|
bed_width = float(input(" Largeur X (mm) [220]: ") or "220")
|
||||||
|
bed_depth = float(input(" Profondeur Y (mm) [220]: ") or "220")
|
||||||
|
|
||||||
|
print("\n--- Mesures de calibration ---")
|
||||||
|
print("Imprimez d'abord un carré de test et mesurez-le.")
|
||||||
|
print("(Laissez vide pour utiliser les valeurs par défaut)")
|
||||||
|
|
||||||
|
expected = float(input(" Taille attendue du carré (mm) [50]: ") or "50")
|
||||||
|
measured_x = float(input(" Taille mesurée sur X (mm) [50]: ") or "50")
|
||||||
|
measured_y = float(input(" Taille mesurée sur Y (mm) [50]: ") or "50")
|
||||||
|
measured_diag = float(input(" Diagonale mesurée (mm) [70.71]: ") or "70.71")
|
||||||
|
|
||||||
|
calibration = BedCalibration.from_measurements(
|
||||||
|
name=name,
|
||||||
|
expected_square_size=expected,
|
||||||
|
measured_x=measured_x,
|
||||||
|
measured_y=measured_y,
|
||||||
|
measured_diagonal=measured_diag,
|
||||||
|
bed_width=bed_width,
|
||||||
|
bed_depth=bed_depth
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n--- Corrections calculées ---")
|
||||||
|
print(f" Scale X: {calibration.scale_x:.4f}")
|
||||||
|
print(f" Scale Y: {calibration.scale_y:.4f}")
|
||||||
|
print(f" Skew X-Y: {calibration.skew_xy:.2f}°")
|
||||||
|
|
||||||
|
# Option pour ajustements manuels
|
||||||
|
manual = input("\nAjustements manuels? (o/N): ").lower().startswith('o')
|
||||||
|
|
||||||
|
if manual:
|
||||||
|
calibration.offset_x = float(input(f" Offset X (mm) [{calibration.offset_x}]: ") or calibration.offset_x)
|
||||||
|
calibration.offset_y = float(input(f" Offset Y (mm) [{calibration.offset_y}]: ") or calibration.offset_y)
|
||||||
|
calibration.rotation = float(input(f" Rotation (°) [{calibration.rotation}]: ") or calibration.rotation)
|
||||||
|
calibration.skew_xy = float(input(f" Skew X-Y (°) [{calibration.skew_xy}]: ") or calibration.skew_xy)
|
||||||
|
calibration._skew_rad = __import__('math').radians(calibration.skew_xy)
|
||||||
|
|
||||||
|
return calibration
|
||||||
|
|
||||||
|
def edit_profile(self, name: str) -> Optional[BedCalibration]:
|
||||||
|
"""Modifie un profil existant."""
|
||||||
|
calibration = self.load_profile(name)
|
||||||
|
|
||||||
|
print(f"\n=== Modification du profil: {name} ===\n")
|
||||||
|
print("Laissez vide pour garder la valeur actuelle\n")
|
||||||
|
|
||||||
|
calibration.bed_width = float(input(f"Largeur X (mm) [{calibration.bed_width}]: ") or calibration.bed_width)
|
||||||
|
calibration.bed_depth = float(input(f"Profondeur Y (mm) [{calibration.bed_depth}]: ") or calibration.bed_depth)
|
||||||
|
calibration.offset_x = float(input(f"Offset X (mm) [{calibration.offset_x}]: ") or calibration.offset_x)
|
||||||
|
calibration.offset_y = float(input(f"Offset Y (mm) [{calibration.offset_y}]: ") or calibration.offset_y)
|
||||||
|
calibration.scale_x = float(input(f"Scale X [{calibration.scale_x}]: ") or calibration.scale_x)
|
||||||
|
calibration.scale_y = float(input(f"Scale Y [{calibration.scale_y}]: ") or calibration.scale_y)
|
||||||
|
calibration.skew_xy = float(input(f"Skew X-Y (°) [{calibration.skew_xy}]: ") or calibration.skew_xy)
|
||||||
|
calibration.rotation = float(input(f"Rotation (°) [{calibration.rotation}]: ") or calibration.rotation)
|
||||||
|
|
||||||
|
calibration._skew_rad = __import__('math').radians(calibration.skew_xy)
|
||||||
|
calibration._rotation_rad = __import__('math').radians(calibration.rotation)
|
||||||
|
|
||||||
|
return calibration
|
||||||
1
imp3d_corrector/core/__init__.py
Normal file
1
imp3d_corrector/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Core module for XYZ correction."""
|
||||||
355
imp3d_corrector/core/advanced_calibration.py
Normal file
355
imp3d_corrector/core/advanced_calibration.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
"""Calibration avancée avec multi-points pour imprimantes 3D."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Dict, Tuple, Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationPosition(Enum):
|
||||||
|
"""Positions des carrés de calibration sur le plateau."""
|
||||||
|
CENTER = "center"
|
||||||
|
TOP_LEFT = "top_left"
|
||||||
|
TOP_RIGHT = "top_right"
|
||||||
|
BOTTOM_LEFT = "bottom_left"
|
||||||
|
BOTTOM_RIGHT = "bottom_right"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SquareMeasurement:
|
||||||
|
"""Mesures d'un carré de calibration."""
|
||||||
|
position: CalibrationPosition
|
||||||
|
expected_size: float # mm
|
||||||
|
measured_x: float # mm
|
||||||
|
measured_y: float # mm
|
||||||
|
measured_diagonal: float # mm
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale_x(self) -> float:
|
||||||
|
"""Facteur d'échelle X calculé."""
|
||||||
|
return self.expected_size / self.measured_x if self.measured_x > 0 else 1.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale_y(self) -> float:
|
||||||
|
"""Facteur d'échelle Y calculé."""
|
||||||
|
return self.expected_size / self.measured_y if self.measured_y > 0 else 1.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skew_angle(self) -> float:
|
||||||
|
"""Angle de skew calculé en degrés."""
|
||||||
|
avg_side = (self.measured_x + self.measured_y) / 2
|
||||||
|
if avg_side > 0 and self.measured_diagonal > 0:
|
||||||
|
try:
|
||||||
|
return math.degrees(math.acos(self.measured_diagonal / (2 * avg_side))) - 45
|
||||||
|
except ValueError:
|
||||||
|
return 0.0
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def diagonal_error(self) -> float:
|
||||||
|
"""Erreur de diagonale en mm."""
|
||||||
|
expected_diag = self.expected_size * math.sqrt(2)
|
||||||
|
return self.measured_diagonal - expected_diag
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MultiPointCalibration:
|
||||||
|
"""Calibration multi-points complète."""
|
||||||
|
bed_width: float = 220.0
|
||||||
|
bed_depth: float = 220.0
|
||||||
|
square_size: float = 30.0
|
||||||
|
measurements: Dict[CalibrationPosition, SquareMeasurement] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def add_measurement(self, measurement: SquareMeasurement):
|
||||||
|
"""Ajoute une mesure à la calibration."""
|
||||||
|
self.measurements[measurement.position] = measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_center(self) -> bool:
|
||||||
|
return CalibrationPosition.CENTER in self.measurements
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_corners(self) -> bool:
|
||||||
|
corners = {CalibrationPosition.TOP_LEFT, CalibrationPosition.TOP_RIGHT,
|
||||||
|
CalibrationPosition.BOTTOM_LEFT, CalibrationPosition.BOTTOM_RIGHT}
|
||||||
|
return corners.issubset(self.measurements.keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_complete(self) -> bool:
|
||||||
|
"""Vérifie si toutes les mesures sont présentes."""
|
||||||
|
return len(self.measurements) == 5
|
||||||
|
|
||||||
|
def get_global_scale(self) -> Tuple[float, float]:
|
||||||
|
"""Calcule l'échelle globale moyenne."""
|
||||||
|
if not self.measurements:
|
||||||
|
return 1.0, 1.0
|
||||||
|
|
||||||
|
avg_scale_x = sum(m.scale_x for m in self.measurements.values()) / len(self.measurements)
|
||||||
|
avg_scale_y = sum(m.scale_y for m in self.measurements.values()) / len(self.measurements)
|
||||||
|
return avg_scale_x, avg_scale_y
|
||||||
|
|
||||||
|
def get_global_skew(self) -> float:
|
||||||
|
"""Calcule le skew global moyen."""
|
||||||
|
if not self.measurements:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return sum(m.skew_angle for m in self.measurements.values()) / len(self.measurements)
|
||||||
|
|
||||||
|
def get_scale_variation(self) -> Dict[str, Tuple[float, float]]:
|
||||||
|
"""Analyse les variations d'échelle selon la position.
|
||||||
|
|
||||||
|
Retourne les écarts d'échelle par zone.
|
||||||
|
"""
|
||||||
|
if not self.has_corners:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
variations = {}
|
||||||
|
center = self.measurements.get(CalibrationPosition.CENTER)
|
||||||
|
|
||||||
|
for pos in [CalibrationPosition.TOP_LEFT, CalibrationPosition.TOP_RIGHT,
|
||||||
|
CalibrationPosition.BOTTOM_LEFT, CalibrationPosition.BOTTOM_RIGHT]:
|
||||||
|
corner = self.measurements[pos]
|
||||||
|
if center:
|
||||||
|
diff_x = corner.scale_x - center.scale_x
|
||||||
|
diff_y = corner.scale_y - center.scale_y
|
||||||
|
variations[pos.value] = (diff_x, diff_y)
|
||||||
|
|
||||||
|
return variations
|
||||||
|
|
||||||
|
def detect_non_linearity(self) -> Dict[str, float]:
|
||||||
|
"""Détecte les problèmes de linéarité sur les axes.
|
||||||
|
|
||||||
|
Compare les échelles gauche/droite et haut/bas.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
'x_asymmetry': 0.0, # Différence gauche/droite
|
||||||
|
'y_asymmetry': 0.0, # Différence haut/bas
|
||||||
|
'x_variance': 0.0, # Variance sur X
|
||||||
|
'y_variance': 0.0, # Variance sur Y
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self.has_corners:
|
||||||
|
return result
|
||||||
|
|
||||||
|
tl = self.measurements[CalibrationPosition.TOP_LEFT]
|
||||||
|
tr = self.measurements[CalibrationPosition.TOP_RIGHT]
|
||||||
|
bl = self.measurements[CalibrationPosition.BOTTOM_LEFT]
|
||||||
|
br = self.measurements[CalibrationPosition.BOTTOM_RIGHT]
|
||||||
|
|
||||||
|
# Asymétrie X: différence entre côté gauche et droit
|
||||||
|
left_avg_x = (tl.scale_x + bl.scale_x) / 2
|
||||||
|
right_avg_x = (tr.scale_x + br.scale_x) / 2
|
||||||
|
result['x_asymmetry'] = right_avg_x - left_avg_x
|
||||||
|
|
||||||
|
# Asymétrie Y: différence entre haut et bas
|
||||||
|
top_avg_y = (tl.scale_y + tr.scale_y) / 2
|
||||||
|
bottom_avg_y = (bl.scale_y + br.scale_y) / 2
|
||||||
|
result['y_asymmetry'] = bottom_avg_y - top_avg_y
|
||||||
|
|
||||||
|
# Variance sur X
|
||||||
|
all_x = [tl.scale_x, tr.scale_x, bl.scale_x, br.scale_x]
|
||||||
|
mean_x = sum(all_x) / len(all_x)
|
||||||
|
result['x_variance'] = sum((x - mean_x) ** 2 for x in all_x) / len(all_x)
|
||||||
|
|
||||||
|
# Variance sur Y
|
||||||
|
all_y = [tl.scale_y, tr.scale_y, bl.scale_y, br.scale_y]
|
||||||
|
mean_y = sum(all_y) / len(all_y)
|
||||||
|
result['y_variance'] = sum((y - mean_y) ** 2 for y in all_y) / len(all_y)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_corrections(self) -> Dict:
|
||||||
|
"""Génère les corrections recommandées."""
|
||||||
|
scale_x, scale_y = self.get_global_scale()
|
||||||
|
skew = self.get_global_skew()
|
||||||
|
nonlin = self.detect_non_linearity()
|
||||||
|
|
||||||
|
corrections = {
|
||||||
|
'scale_x': scale_x,
|
||||||
|
'scale_y': scale_y,
|
||||||
|
'skew_xy': skew,
|
||||||
|
'offset_x': 0.0,
|
||||||
|
'offset_y': 0.0,
|
||||||
|
'rotation': 0.0,
|
||||||
|
'issues': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Détecter les problèmes
|
||||||
|
if abs(nonlin['x_asymmetry']) > 0.005:
|
||||||
|
side = "droit plus long" if nonlin['x_asymmetry'] > 0 else "gauche plus long"
|
||||||
|
corrections['issues'].append(
|
||||||
|
f"Asymétrie X détectée ({side}): {abs(nonlin['x_asymmetry']*100):.2f}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
if abs(nonlin['y_asymmetry']) > 0.005:
|
||||||
|
side = "bas plus long" if nonlin['y_asymmetry'] > 0 else "haut plus long"
|
||||||
|
corrections['issues'].append(
|
||||||
|
f"Asymétrie Y détectée ({side}): {abs(nonlin['y_asymmetry']*100):.2f}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
if nonlin['x_variance'] > 0.0001:
|
||||||
|
corrections['issues'].append(
|
||||||
|
f"Variance X élevée: mesures non uniformes sur le plateau"
|
||||||
|
)
|
||||||
|
|
||||||
|
if nonlin['y_variance'] > 0.0001:
|
||||||
|
corrections['issues'].append(
|
||||||
|
f"Variance Y élevée: mesures non uniformes sur le plateau"
|
||||||
|
)
|
||||||
|
|
||||||
|
if abs(skew) > 0.3:
|
||||||
|
corrections['issues'].append(
|
||||||
|
f"Skew important: {skew:.2f}° (axes non perpendiculaires)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return corrections
|
||||||
|
|
||||||
|
def generate_report(self) -> str:
|
||||||
|
"""Génère un rapport de calibration lisible."""
|
||||||
|
lines = ["=" * 50]
|
||||||
|
lines.append("RAPPORT DE CALIBRATION")
|
||||||
|
lines.append("=" * 50)
|
||||||
|
lines.append(f"\nPlateau: {self.bed_width} x {self.bed_depth} mm")
|
||||||
|
lines.append(f"Taille des carrés: {self.square_size} mm")
|
||||||
|
lines.append(f"Points mesurés: {len(self.measurements)}/5")
|
||||||
|
|
||||||
|
if self.measurements:
|
||||||
|
lines.append("\n--- Mesures par position ---")
|
||||||
|
for pos, m in self.measurements.items():
|
||||||
|
lines.append(f"\n{pos.value.upper().replace('_', ' ')}:")
|
||||||
|
lines.append(f" X: {m.measured_x:.2f} mm (scale: {m.scale_x:.4f})")
|
||||||
|
lines.append(f" Y: {m.measured_y:.2f} mm (scale: {m.scale_y:.4f})")
|
||||||
|
lines.append(f" Diag: {m.measured_diagonal:.2f} mm (skew: {m.skew_angle:.2f}°)")
|
||||||
|
|
||||||
|
corrections = self.get_corrections()
|
||||||
|
lines.append("\n--- Corrections calculées ---")
|
||||||
|
lines.append(f" Scale X: {corrections['scale_x']:.6f}")
|
||||||
|
lines.append(f" Scale Y: {corrections['scale_y']:.6f}")
|
||||||
|
lines.append(f" Skew: {corrections['skew_xy']:.3f}°")
|
||||||
|
|
||||||
|
if corrections['issues']:
|
||||||
|
lines.append("\n--- Problèmes détectés ---")
|
||||||
|
for issue in corrections['issues']:
|
||||||
|
lines.append(f" ⚠ {issue}")
|
||||||
|
else:
|
||||||
|
lines.append("\n✓ Aucun problème majeur détecté")
|
||||||
|
|
||||||
|
lines.append("\n" + "=" * 50)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
class AdvancedCalibrationGCode:
|
||||||
|
"""Générateur de G-code de calibration avancé."""
|
||||||
|
|
||||||
|
def __init__(self, bed_width: float = 220.0, bed_depth: float = 220.0,
|
||||||
|
square_size: float = 30.0, margin: float = 15.0):
|
||||||
|
self.bed_width = bed_width
|
||||||
|
self.bed_depth = bed_depth
|
||||||
|
self.square_size = square_size
|
||||||
|
self.margin = margin
|
||||||
|
|
||||||
|
def get_positions(self) -> Dict[CalibrationPosition, Tuple[float, float]]:
|
||||||
|
"""Retourne les positions centrales de chaque carré."""
|
||||||
|
half = self.square_size / 2
|
||||||
|
|
||||||
|
positions = {
|
||||||
|
CalibrationPosition.CENTER: (self.bed_width / 2, self.bed_depth / 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Coins avec marge
|
||||||
|
positions[CalibrationPosition.BOTTOM_LEFT] = (self.margin + half, self.margin + half)
|
||||||
|
positions[CalibrationPosition.BOTTOM_RIGHT] = (self.bed_width - self.margin - half, self.margin + half)
|
||||||
|
positions[CalibrationPosition.TOP_LEFT] = (self.margin + half, self.bed_depth - self.margin - half)
|
||||||
|
positions[CalibrationPosition.TOP_RIGHT] = (self.bed_width - self.margin - half, self.bed_depth - self.margin - half)
|
||||||
|
|
||||||
|
return positions
|
||||||
|
|
||||||
|
def generate(self, include_labels: bool = True) -> str:
|
||||||
|
"""Génère le G-code de calibration complet."""
|
||||||
|
positions = self.get_positions()
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"; " + "=" * 50,
|
||||||
|
"; G-CODE DE CALIBRATION AVANCÉ",
|
||||||
|
"; " + "=" * 50,
|
||||||
|
f"; Plateau: {self.bed_width} x {self.bed_depth} mm",
|
||||||
|
f"; Carrés de test: {self.square_size} x {self.square_size} mm",
|
||||||
|
";",
|
||||||
|
"; Instructions:",
|
||||||
|
"; 1. Imprimez ce fichier",
|
||||||
|
"; 2. Mesurez CHAQUE carré (X, Y, diagonale)",
|
||||||
|
"; 3. Notez les valeurs pour chaque position",
|
||||||
|
"; 4. Utilisez 'imp3d-corrector calibrate-advanced' pour entrer les mesures",
|
||||||
|
";",
|
||||||
|
"; Positions:",
|
||||||
|
"; TL = Top Left (haut gauche)",
|
||||||
|
"; TR = Top Right (haut droit)",
|
||||||
|
"; BL = Bottom Left (bas gauche)",
|
||||||
|
"; BR = Bottom Right (bas droit)",
|
||||||
|
"; C = Center (centre)",
|
||||||
|
"; " + "=" * 50,
|
||||||
|
"",
|
||||||
|
"G21 ; Unités en millimètres",
|
||||||
|
"G90 ; Positionnement absolu",
|
||||||
|
"M82 ; Extrusion absolue",
|
||||||
|
"G28 ; Home",
|
||||||
|
"G1 Z10 F3000 ; Monter la buse",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
extrusion = 0
|
||||||
|
order = [
|
||||||
|
CalibrationPosition.BOTTOM_LEFT,
|
||||||
|
CalibrationPosition.BOTTOM_RIGHT,
|
||||||
|
CalibrationPosition.CENTER,
|
||||||
|
CalibrationPosition.TOP_LEFT,
|
||||||
|
CalibrationPosition.TOP_RIGHT,
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, pos in enumerate(order):
|
||||||
|
cx, cy = positions[pos]
|
||||||
|
half = self.square_size / 2
|
||||||
|
|
||||||
|
label = {
|
||||||
|
CalibrationPosition.BOTTOM_LEFT: "BL",
|
||||||
|
CalibrationPosition.BOTTOM_RIGHT: "BR",
|
||||||
|
CalibrationPosition.CENTER: "C",
|
||||||
|
CalibrationPosition.TOP_LEFT: "TL",
|
||||||
|
CalibrationPosition.TOP_RIGHT: "TR",
|
||||||
|
}[pos]
|
||||||
|
|
||||||
|
lines.append(f"; --- Carré {label} ({pos.value}) ---")
|
||||||
|
lines.append(f"; Position centre: X={cx:.1f} Y={cy:.1f}")
|
||||||
|
|
||||||
|
# Aller au point de départ
|
||||||
|
lines.append(f"G1 X{cx - half:.3f} Y{cy - half:.3f} F6000")
|
||||||
|
lines.append("G1 Z0.3 F3000")
|
||||||
|
|
||||||
|
# Dessiner le carré
|
||||||
|
extrusion += 5
|
||||||
|
lines.append(f"G1 X{cx + half:.3f} Y{cy - half:.3f} E{extrusion:.1f} F1500")
|
||||||
|
extrusion += 5
|
||||||
|
lines.append(f"G1 X{cx + half:.3f} Y{cy + half:.3f} E{extrusion:.1f} F1500")
|
||||||
|
extrusion += 5
|
||||||
|
lines.append(f"G1 X{cx - half:.3f} Y{cy + half:.3f} E{extrusion:.1f} F1500")
|
||||||
|
extrusion += 5
|
||||||
|
lines.append(f"G1 X{cx - half:.3f} Y{cy - half:.3f} E{extrusion:.1f} F1500")
|
||||||
|
|
||||||
|
lines.append("G1 Z2 F3000")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Terminer
|
||||||
|
lines.append("; --- Fin ---")
|
||||||
|
lines.append("G1 Z10 F3000")
|
||||||
|
lines.append(f"G1 X0 Y{self.bed_depth} F6000")
|
||||||
|
lines.append("M84 ; Désactiver les moteurs")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("; " + "=" * 50)
|
||||||
|
lines.append("; MESURES À PRENDRE:")
|
||||||
|
lines.append("; Pour chaque carré, mesurez:")
|
||||||
|
lines.append(f"; - Côté X (attendu: {self.square_size} mm)")
|
||||||
|
lines.append(f"; - Côté Y (attendu: {self.square_size} mm)")
|
||||||
|
lines.append(f"; - Diagonale (attendu: {self.square_size * 1.414:.2f} mm)")
|
||||||
|
lines.append("; " + "=" * 50)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
273
imp3d_corrector/core/corrector.py
Normal file
273
imp3d_corrector/core/corrector.py
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
"""Correcteur de plan XYZ pour imprimantes 3D.
|
||||||
|
|
||||||
|
Ce module applique les corrections de:
|
||||||
|
- Décalage des axes X/Y (skew)
|
||||||
|
- Erreurs de longueur sur les bords
|
||||||
|
- Rotation du plateau
|
||||||
|
- Mise à l'échelle
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BedCalibration:
|
||||||
|
"""Configuration de calibration du plateau.
|
||||||
|
|
||||||
|
Mesures pour détecter les défauts:
|
||||||
|
- skew_xy: Angle de désalignement X-Y en degrés (positif = Y tourne vers X)
|
||||||
|
- offset_x: Décalage de l'origine sur X (mm)
|
||||||
|
- offset_y: Décalage de l'origine sur Y (mm)
|
||||||
|
- scale_x: Facteur d'échelle X (1.0 = pas de changement)
|
||||||
|
- scale_y: Facteur d'échelle Y (1.0 = pas de changement)
|
||||||
|
- rotation: Rotation du plateau en degrés
|
||||||
|
"""
|
||||||
|
name: str = "default"
|
||||||
|
skew_xy: float = 0.0 # degrés
|
||||||
|
offset_x: float = 0.0 # mm
|
||||||
|
offset_y: float = 0.0 # mm
|
||||||
|
scale_x: float = 1.0
|
||||||
|
scale_y: float = 1.0
|
||||||
|
rotation: float = 0.0 # degrés
|
||||||
|
|
||||||
|
# Dimensions du plateau
|
||||||
|
bed_width: float = 220.0 # mm (X)
|
||||||
|
bed_depth: float = 220.0 # mm (Y)
|
||||||
|
|
||||||
|
# Points de calibration (optionnel, pour calibration avancée)
|
||||||
|
calibration_points: list = field(default_factory=list)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Convertit les angles en radians pour les calculs."""
|
||||||
|
self._skew_rad = math.radians(self.skew_xy)
|
||||||
|
self._rotation_rad = math.radians(self.rotation)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_measurements(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
expected_square_size: float,
|
||||||
|
measured_x: float,
|
||||||
|
measured_y: float,
|
||||||
|
measured_diagonal: float,
|
||||||
|
bed_width: float = 220.0,
|
||||||
|
bed_depth: float = 220.0
|
||||||
|
) -> 'BedCalibration':
|
||||||
|
"""Crée une calibration à partir de mesures d'un carré de test.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Nom du profil
|
||||||
|
expected_square_size: Taille attendue du carré (mm)
|
||||||
|
measured_x: Taille mesurée sur X (mm)
|
||||||
|
measured_y: Taille mesurée sur Y (mm)
|
||||||
|
measured_diagonal: Diagonale mesurée (mm)
|
||||||
|
bed_width: Largeur du plateau (mm)
|
||||||
|
bed_depth: Profondeur du plateau (mm)
|
||||||
|
"""
|
||||||
|
# Calcul du facteur d'échelle
|
||||||
|
scale_x = expected_square_size / measured_x if measured_x > 0 else 1.0
|
||||||
|
scale_y = expected_square_size / measured_y if measured_y > 0 else 1.0
|
||||||
|
|
||||||
|
# Calcul du skew à partir de la diagonale
|
||||||
|
# Pour un carré parfait: diag = sqrt(2) * côté
|
||||||
|
expected_diagonal = expected_square_size * math.sqrt(2)
|
||||||
|
|
||||||
|
# Si la diagonale ne correspond pas, il y a du skew
|
||||||
|
# Formule simplifiée: skew ≈ acos(diag_mesurée / (2 * côté_moyen)) - 45°
|
||||||
|
avg_side = (measured_x + measured_y) / 2
|
||||||
|
if avg_side > 0 and measured_diagonal > 0:
|
||||||
|
try:
|
||||||
|
skew_angle = math.degrees(math.acos(measured_diagonal / (2 * avg_side))) - 45
|
||||||
|
except ValueError:
|
||||||
|
skew_angle = 0.0
|
||||||
|
else:
|
||||||
|
skew_angle = 0.0
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
skew_xy=skew_angle,
|
||||||
|
scale_x=scale_x,
|
||||||
|
scale_y=scale_y,
|
||||||
|
bed_width=bed_width,
|
||||||
|
bed_depth=bed_depth
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_yaml(cls, filepath: str) -> 'BedCalibration':
|
||||||
|
"""Charge une calibration depuis un fichier YAML."""
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
return cls(**data)
|
||||||
|
|
||||||
|
def to_yaml(self, filepath: str):
|
||||||
|
"""Sauvegarde la calibration dans un fichier YAML."""
|
||||||
|
data = {
|
||||||
|
'name': self.name,
|
||||||
|
'skew_xy': self.skew_xy,
|
||||||
|
'offset_x': self.offset_x,
|
||||||
|
'offset_y': self.offset_y,
|
||||||
|
'scale_x': self.scale_x,
|
||||||
|
'scale_y': self.scale_y,
|
||||||
|
'rotation': self.rotation,
|
||||||
|
'bed_width': self.bed_width,
|
||||||
|
'bed_depth': self.bed_depth,
|
||||||
|
'calibration_points': self.calibration_points
|
||||||
|
}
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
yaml.dump(data, f, default_flow_style=False)
|
||||||
|
|
||||||
|
|
||||||
|
class XYZCorrector:
|
||||||
|
"""Applique les corrections XYZ à un fichier G-code."""
|
||||||
|
|
||||||
|
def __init__(self, calibration: BedCalibration):
|
||||||
|
self.calibration = calibration
|
||||||
|
self._current_pos = {'X': 0.0, 'Y': 0.0, 'Z': 0.0}
|
||||||
|
|
||||||
|
def correct_point(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
||||||
|
"""Applique les corrections à un point 3D.
|
||||||
|
|
||||||
|
Ordre des transformations:
|
||||||
|
1. Appliquer le décalage d'origine
|
||||||
|
2. Appliquer la mise à l'échelle
|
||||||
|
3. Appliquer le skew
|
||||||
|
4. Appliquer la rotation
|
||||||
|
"""
|
||||||
|
# 1. Décalage d'origine
|
||||||
|
x -= self.calibration.offset_x
|
||||||
|
y -= self.calibration.offset_y
|
||||||
|
|
||||||
|
# 2. Mise à l'échelle
|
||||||
|
x *= self.calibration.scale_x
|
||||||
|
y *= self.calibration.scale_y
|
||||||
|
|
||||||
|
# 3. Correction du skew (cisaillement X-Y)
|
||||||
|
# Le skew fait que l'axe Y n'est pas perpendiculaire à X
|
||||||
|
skew = self.calibration._skew_rad
|
||||||
|
x_new = x - y * math.tan(skew)
|
||||||
|
y_new = y
|
||||||
|
|
||||||
|
# 4. Rotation
|
||||||
|
rot = self.calibration._rotation_rad
|
||||||
|
cos_r = math.cos(rot)
|
||||||
|
sin_r = math.sin(rot)
|
||||||
|
x_final = x_new * cos_r - y_new * sin_r
|
||||||
|
y_final = x_new * sin_r + y_new * cos_r
|
||||||
|
|
||||||
|
return x_final, y_final, z
|
||||||
|
|
||||||
|
def correct_gcode_line(self, line: str) -> str:
|
||||||
|
"""Corrige une ligne G-code si elle contient des coordonnées."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# Préserver les commentaires et lignes vides
|
||||||
|
if not stripped or stripped.startswith(';'):
|
||||||
|
return line
|
||||||
|
|
||||||
|
# Commandes de mouvement
|
||||||
|
if not any(stripped.startswith(cmd) for cmd in ['G0', 'G1', 'G2', 'G3']):
|
||||||
|
return line
|
||||||
|
|
||||||
|
# Extraire les coordonnées
|
||||||
|
def replace_coord(match):
|
||||||
|
axis = match.group(1)
|
||||||
|
value = float(match.group(2))
|
||||||
|
|
||||||
|
# Stocker la valeur actuelle
|
||||||
|
if axis in 'XYZ':
|
||||||
|
self._current_pos[axis] = value
|
||||||
|
|
||||||
|
return match.group(0) # Temporaire, on va recalculer
|
||||||
|
|
||||||
|
# Calculer la position corrigée
|
||||||
|
x_match = re.search(r'X([-+]?[0-9]*\.?[0-9]+)', stripped, re.IGNORECASE)
|
||||||
|
y_match = re.search(r'Y([-+]?[0-9]*\.?[0-9]+)', stripped, re.IGNORECASE)
|
||||||
|
z_match = re.search(r'Z([-+]?[0-9]*\.?[0-9]+)', stripped, re.IGNORECASE)
|
||||||
|
|
||||||
|
# Mettre à jour la position courante
|
||||||
|
if x_match:
|
||||||
|
self._current_pos['X'] = float(x_match.group(1))
|
||||||
|
if y_match:
|
||||||
|
self._current_pos['Y'] = float(y_match.group(1))
|
||||||
|
if z_match:
|
||||||
|
self._current_pos['Z'] = float(z_match.group(1))
|
||||||
|
|
||||||
|
# Appliquer la correction
|
||||||
|
new_x, new_y, new_z = self.correct_point(
|
||||||
|
self._current_pos['X'],
|
||||||
|
self._current_pos['Y'],
|
||||||
|
self._current_pos['Z']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reconstruire la ligne
|
||||||
|
result = stripped
|
||||||
|
|
||||||
|
if x_match:
|
||||||
|
result = re.sub(r'X[-+]?[0-9]*\.?[0-9]+', f'X{new_x:.3f}', result, flags=re.IGNORECASE)
|
||||||
|
if y_match:
|
||||||
|
result = re.sub(r'Y[-+]?[0-9]*\.?[0-9]+', f'Y{new_y:.3f}', result, flags=re.IGNORECASE)
|
||||||
|
if z_match:
|
||||||
|
result = re.sub(r'Z[-+]?[0-9]*\.?[0-9]+', f'Z{new_z:.3f}', result, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
# Préserver le formatage original (indentation, etc.)
|
||||||
|
if line.startswith(' ') or line.startswith('\t'):
|
||||||
|
return line[:len(line) - len(line.lstrip())] + result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def correct_gcode_file(self, input_path: str, output_path: str):
|
||||||
|
"""Corrige un fichier G-code complet."""
|
||||||
|
with open(input_path, 'r') as infile, open(output_path, 'w') as outfile:
|
||||||
|
for line in infile:
|
||||||
|
corrected = self.correct_gcode_line(line)
|
||||||
|
outfile.write(corrected if corrected.endswith('\n') else corrected + '\n')
|
||||||
|
|
||||||
|
def generate_correction_gcode(self) -> str:
|
||||||
|
"""Génère un G-code de calibration pour vérifier les corrections.
|
||||||
|
|
||||||
|
Ce G-code dessine un carré de test aux dimensions spécifiées.
|
||||||
|
"""
|
||||||
|
bed_w = self.calibration.bed_width
|
||||||
|
bed_d = self.calibration.bed_depth
|
||||||
|
size = min(bed_w, bed_d) * 0.5 # Carré à 50% du plateau
|
||||||
|
|
||||||
|
# Centrer le carré
|
||||||
|
cx, cy = bed_w / 2, bed_d / 2
|
||||||
|
half = size / 2
|
||||||
|
|
||||||
|
gcode = f"""; G-code de calibration généré pour: {self.calibration.name}
|
||||||
|
; Dimensions du plateau: {bed_w} x {bed_d} mm
|
||||||
|
; Carré de test: {size} x {size} mm
|
||||||
|
|
||||||
|
G21 ; Unités en millimètres
|
||||||
|
G90 ; Positionnement absolu
|
||||||
|
M82 ; Extrusion absolue
|
||||||
|
G28 ; Home
|
||||||
|
|
||||||
|
; Monter la buse
|
||||||
|
G1 Z10 F3000
|
||||||
|
|
||||||
|
; Aller au point de départ
|
||||||
|
G1 X{cx - half:.3f} Y{cy - half:.3f} F6000
|
||||||
|
|
||||||
|
; Dessiner le carré
|
||||||
|
G1 Z0.3 F3000
|
||||||
|
G1 X{cx + half:.3f} Y{cy - half:.3f} E10 F1500
|
||||||
|
G1 X{cx + half:.3f} Y{cy + half:.3f} E20 F1500
|
||||||
|
G1 X{cx - half:.3f} Y{cy + half:.3f} E30 F1500
|
||||||
|
G1 X{cx - half:.3f} Y{cy - half:.3f} E40 F1500
|
||||||
|
|
||||||
|
; Terminer
|
||||||
|
G1 Z10 F3000
|
||||||
|
G1 X0 Y{bed_d} F6000
|
||||||
|
M84 ; Désactiver les moteurs
|
||||||
|
|
||||||
|
; Mesurez les côtés et la diagonale de ce carré
|
||||||
|
; Entrez les valeurs dans la configuration
|
||||||
|
"""
|
||||||
|
return gcode
|
||||||
101
imp3d_corrector/core/gcode_parser.py
Normal file
101
imp3d_corrector/core/gcode_parser.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""Parser G-code pour extraire les commandes de mouvement."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, List, Generator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GCodeMove:
|
||||||
|
"""Représente une commande de mouvement G-code."""
|
||||||
|
command: str
|
||||||
|
x: Optional[float] = None
|
||||||
|
y: Optional[float] = None
|
||||||
|
z: Optional[float] = None
|
||||||
|
e: Optional[float] = None
|
||||||
|
f: Optional[float] = None
|
||||||
|
original_line: str = ""
|
||||||
|
line_number: int = 0
|
||||||
|
is_relative: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class GCodeParser:
|
||||||
|
"""Parse les fichiers G-code et extrait les commandes de mouvement."""
|
||||||
|
|
||||||
|
MOVE_COMMANDS = {'G0', 'G1', 'G2', 'G3'}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.absolute_mode = True # G90 (absolu) vs G91 (relatif)
|
||||||
|
self.absolute_extrusion = True # M82 vs M83
|
||||||
|
self.current_pos = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'E': 0.0}
|
||||||
|
|
||||||
|
def parse_line(self, line: str, line_number: int = 0) -> Optional[GCodeMove]:
|
||||||
|
"""Parse une ligne G-code et retourne un GCodeMove si c'est un mouvement."""
|
||||||
|
original = line
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Ignorer les commentaires et lignes vides
|
||||||
|
if not line or line.startswith(';'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Retirer les commentaires en fin de ligne
|
||||||
|
if ';' in line:
|
||||||
|
line = line.split(';')[0].strip()
|
||||||
|
|
||||||
|
# Détecter le mode de positionnement
|
||||||
|
if line == 'G90':
|
||||||
|
self.absolute_mode = True
|
||||||
|
return None
|
||||||
|
elif line == 'G91':
|
||||||
|
self.absolute_mode = False
|
||||||
|
return None
|
||||||
|
elif line == 'M82':
|
||||||
|
self.absolute_extrusion = True
|
||||||
|
return None
|
||||||
|
elif line == 'M83':
|
||||||
|
self.absolute_extrusion = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extraire la commande
|
||||||
|
parts = line.split()
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cmd = parts[0].upper()
|
||||||
|
|
||||||
|
# Vérifier si c'est une commande de mouvement
|
||||||
|
if cmd not in self.MOVE_COMMANDS:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Parser les paramètres
|
||||||
|
move = GCodeMove(
|
||||||
|
command=cmd,
|
||||||
|
original_line=original,
|
||||||
|
line_number=line_number,
|
||||||
|
is_relative=not self.absolute_mode
|
||||||
|
)
|
||||||
|
|
||||||
|
param_pattern = re.compile(r'([XYZEF])([-+]?[0-9]*\.?[0-9]+)')
|
||||||
|
|
||||||
|
for part in parts[1:]:
|
||||||
|
match = param_pattern.match(part.upper())
|
||||||
|
if match:
|
||||||
|
param, value = match.groups()
|
||||||
|
setattr(move, param.lower(), float(value))
|
||||||
|
|
||||||
|
return move
|
||||||
|
|
||||||
|
def parse_file(self, filepath: str) -> Generator[GCodeMove, None, None]:
|
||||||
|
"""Parse un fichier G-code et génère les mouvements."""
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
for line_num, line in enumerate(f, 1):
|
||||||
|
move = self.parse_line(line, line_num)
|
||||||
|
if move:
|
||||||
|
yield move
|
||||||
|
|
||||||
|
def parse_lines(self, lines: List[str]) -> Generator[GCodeMove, None, None]:
|
||||||
|
"""Parse une liste de lignes G-code."""
|
||||||
|
for line_num, line in enumerate(lines, 1):
|
||||||
|
move = self.parse_line(line, line_num)
|
||||||
|
if move:
|
||||||
|
yield move
|
||||||
42
pyproject.toml
Normal file
42
pyproject.toml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "imp3d-corrector"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Correction de plan XYZ pour imprimantes 3D"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [
|
||||||
|
{name = "muyue"}
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"PyYAML>=6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
imp3d-corrector = "imp3d_corrector.cli:main"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"black>=23.0",
|
||||||
|
"flake8>=6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["imp3d_corrector*"]
|
||||||
Reference in New Issue
Block a user