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:
Augustin
2026-02-20 14:12:11 +01:00
commit a57721216b
12 changed files with 1476 additions and 0 deletions

26
.gitignore vendored Normal file
View 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
View 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
View 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: []

View 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
View 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())

View File

@@ -0,0 +1 @@
"""Configuration module for bed profiles."""

View 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

View File

@@ -0,0 +1 @@
"""Core module for XYZ correction."""

View 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)

View 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

View 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
View 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*"]