Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Batch convert multiple CAD/BIM files (Revit, IFC, DWG, DGN) with progress tracking, error handling, and consolidated reporting.
.claude/skills/datadrivenconstruction-batch-cad-converter/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 98% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 127% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 244% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 164% | 0% |
Large projects and archives contain hundreds or thousands of CAD/BIM files:
Unified batch converter handling all supported formats with progress tracking, error recovery, and consolidated reporting.
pythonimport subprocess from pathlib import Path from typing import List, Optional, Dict, Any, Callable from dataclasses import dataclass, field from datetime import datetime import time import json from enum import Enum from concurrent.futures import ThreadPoolExecutor, as_completed class CADFormat(Enum): """Supported CAD/BIM formats.""" REVIT = (".rvt", ".rfa") IFC = (".ifc",) DWG = (".dwg",) DGN = (".dgn",) class ConversionStatus(Enum): """Status of conversion operation.""" PENDING = "pending" CONVERTING = "converting" SUCCESS = "success" FAILED = "failed" SKIPPED = "skipped" @dataclass class ConversionResult: """Result of single file conversion.""" input_file: str output_file: Optional[str] format: str status: ConversionStatus start_time: datetime end_time: Optional[datetime] duration_seconds: float error_message: Optional[str] = None file_size_kb: float = 0 @dataclass class BatchResult: """Result of batch conversion.""" total_files: int successful: int failed: int skipped: int total_duration: float results: List[ConversionResult] start_time: datetime end_time: datetime class BatchCADConverter: """Batch convert multiple CAD/BIM files.""" # Default converter paths DEFAULT_CONVERTERS = { 'revit': 'RvtExporter.exe', 'ifc': 'IfcExporter.exe', 'dwg': 'DwgExporter.exe', 'dgn': 'DgnExporter.exe' } def __init__(self, converter_dir: str = ".", converters: Dict[str, str] = None): self.converter_dir = Path(converter_dir) self.converters = converters or self.DEFAULT_CONVERTERS self.results: List[ConversionResult] = [] self.progress_callback: Optional[Callable] = None def set_progress_callback(self, callback: Callable[[int, int, str], None]): """Set callback for progress updates.""" self.progress_callback = callback def _get_format(self, file_path: Path) -> Optional[str]: """Detect CAD format from extension.""" ext = file_path.suffix.lower() for format_name, extensions in [ ('revit', ('.rvt', '.rfa')), ('ifc', ('.ifc',)), ('dwg', ('.dwg',)), ('dgn', ('.dgn',)) ]: if ext in extensions: return format_name return None def _get_converter(self, format_name: str) -> Optional[Path]: """Get converter path for format.""" if format_name not in self.converters: return None converter = self.converter_dir / self.converters[format_name] if converter.exists(): return converter # Try in system PATH return Path(self.converters[format_name]) def convert_file(self, input_file: str, output_dir: Optional[str] = None, options: List[str] = None) -> ConversionResult: """Convert single file.""" input_path = Path(input_file) start_time = datetime.now() # Detect format format_name = self._get_format(input_path) if not format_name: return ConversionResult( input_file=input_file, output_file=None, format='unknown', status=ConversionStatus.SKIPPED, start_time=start_time, end_time=datetime.now(), duration_seconds=0, error_message="Unsupported format" ) # Get converter converter = self._get_converter(format_name) if not converter: return ConversionResult( input_file=input_file, output_file=None, format=format_name, status=ConversionStatus.FAILED, start_time=start_time, end_time=datetime.now(), duration_seconds=0, error_message=f"Converter not found for {format_name}" ) # Build command cmd = [str(converter), str(input_path)] if options: cmd.extend(options) # Execute try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) end_time = datetime.now() duration = (end_time - start_time).total_seconds() # Determine output file output_file = input_path.with_suffix('.xlsx') if output_dir: output_file = Path(output_dir) / output_file.name if result.returncode == 0 and output_file.exists(): return ConversionResult( input_file=input_file, output_file=str(output_file), format=format_name, status=ConversionStatus.SUCCESS, start_time=start_time, end_time=end_time, duration_seconds=duration, file_size_kb=output_file.stat().st_size / 1024 ) else: return ConversionResult( input_file=input_file, output_file=None, format=format_name, status=ConversionStatus.FAILED, start_time=start_time, end_time=end_time, duration_seconds=duration, error_message=result.stderr or "Conversion failed" ) except subprocess.TimeoutExpired: return ConversionResult( input_file=input_file, output_file=None, format=format_name, status=ConversionStatus.FAILED, start_time=start_time, end_time=datetime.now(), duration_seconds=3600, error_message="Timeout exceeded (1 hour)" ) except Exception as e: return ConversionResult( input_file=input_file, output_file=None, format=format_name, status=ConversionStatus.FAILED, start_time=start_time, end_time=datetime.now(), duration_seconds=0, error_message=str(e) ) def batch_convert(self, input_folder: str, output_folder: Optional[str] = None, include_subfolders: bool = True, formats: List[str] = None, options: Dict[str, List[str]] = None, parallel: bool = False, max_workers: int = 4) -> BatchResult: """Convert all files in folder.""" start_time = datetime.now() input_path = Path(input_folder) # Find all supported files files = [] pattern = "**/*" if include_subfolders else "*" for ext in ['.rvt', '.rfa', '.ifc', '.dwg', '.dgn']: files.extend(input_path.glob(f"{pattern}{ext}")) # Filter by format if specified if formats: files = [f for f in files if self._get_format(f) in formats] total_files = len(files) self.results = [] # Create output directory if output_folder: Path(output_folder).mkdir(parents=True, exist_ok=True) # Process files if parallel and total_files > 1: self._convert_parallel(files, output_folder, options, max_workers) else: self._convert_sequential(files, output_folder, options) end_time = datetime.now() # Calculate statistics successful = sum(1 for r in self.results if r.status == ConversionStatus.SUCCESS) failed = sum(1 for r in self.results if r.status == ConversionStatus.FAILED) skipped = sum(1 for r in self.results if r.status == ConversionStatus.SKIPPED) return BatchResult( total_files=total_files, successful=successful, failed=failed, skipped=skipped, total_duration=(end_time - start_time).total_seconds(), results=self.results, start_time=start_time, end_time=end_time ) def _convert_sequential(self, files: List[Path], output_folder: Optional[str], options: Dict[str, List[str]]): """Convert files sequentially.""" total = len(files) for i, file_path in enumerate(files, 1): if self.progress_callback: self.progress_callback(i, total, str(file_path)) format_name = self._get_format(file_path) format_options = options.get(format_name, []) if options else [] result = self.convert_file(str(file_path), output_folder, format_options) self.results.append(result) status_symbol = "✓" if result.status == ConversionStatus.SUCCESS else "✗" print(f"[{i}/{total}] {status_symbol} {file_path.name}") def _convert_parallel(self, files: List[Path], output_folder: Optional[str], options: Dict[str, List[str]], max_workers: int): """Convert files in parallel.""" total = len(files) with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = {} for file_path in files: format_name = self._get_format(file_path) format_options = options.get(format_name, []) if options else [] future = executor.submit(self.convert_file, str(file_path), output_folder, format_options) futures[future] = file_path completed = 0 for future in as_completed(futures): completed += 1 result = future.result() self.results.append(result) if self.progress_callback: self.progress_callback(completed, total, str(futures[future])) def generate_report(self, batch_result: BatchResult, output_path: str = None) -> str: """Generate conversion report.""" report = { 'summary': { 'total_files': batch_result.total_files, 'successful': batch_result.successful, 'failed': batch_result.failed, 'skipped': batch_result.skipped, 'success_rate': round(batch_result.successful / batch_result.total_files * 100, 1) if batch_result.total_files > 0 else 0, 'total_duration_seconds': round(batch_result.total_duration, 2), 'start_time': batch_result.start_time.isoformat(), 'end_time': batch_result.end_time.isoformat() }, 'results': [ { 'input': r.input_file, 'output': r.output_file, 'format': r.format, 'status': r.status.value, 'duration': round(r.duration_seconds, 2), 'error': r.error_message } for r in batch_result.results ] } report_json = json.dumps(report, indent=2) if output_path: with open(output_path, 'w') as f: f.write(report_json) return report_json # Progress callback example def print_progress(current: int, total: int, file_name: str): """Print progress to console.""" percent = current / total * 100 print(f"Progress: {current}/{total} ({percent:.1f}%) - {file_name}")
python# Initialize batch converter converter = BatchCADConverter(converter_dir="C:/DDC/") # Set progress callback converter.set_progress_callback(print_progress) # Convert all files result = converter.batch_convert( input_folder="C:/Projects", output_folder="C:/Converted", include_subfolders=True ) print(f"Success: {result.successful}/{result.total_files}")
pythonresult = converter.batch_convert( input_folder="C:/Archive", formats=['revit', 'ifc'], # Only Revit and IFC parallel=True, max_workers=4 )
pythonoptions = { 'revit': ['complete', 'bbox', 'rooms'], 'ifc': ['bbox'], 'dwg': [] } result = converter.batch_convert( input_folder="C:/Projects", options=options )
pythonresult = converter.batch_convert("C:/Projects") report = converter.generate_report(result, "conversion_report.json")
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 22,338 | 24,760 | +11% | 1 | 1 | 0% | 4,315 | 8,715 | +102% | 0 | 0 | — |
case-02 | fail→pass | 18,879 | 17,120 | -9% | 1 | 1 | 0% | 3,682 | 7,281 | +98% | 0 | 0 | — |
case-03 | fail→pass | 15,160 | 7,826 | -48% | 1 | 1 | 0% | 2,709 | 4,928 | +82% | 0 | 0 | — |
case-04 | fail→pass | 22,672 | 3,201 | -86% | 1 | 1 | 0% | 1,797 | 4,071 | +127% | 0 | 0 | — |
case-22 | pass→pass | 13,063 | 12,723 | -3% | 1 | 1 | 0% | 2,508 | 5,947 | +137% | 0 | 0 | — |
case-05 | fail→pass | 7,734 | 6,744 | -13% | 1 | 1 | 0% | 1,383 | 4,761 | +244% | 0 | 0 | — |
case-06 | fail→pass | 7,906 | 3,796 | -52% | 1 | 1 | 0% | 1,562 | 4,119 | +164% | 0 | 0 | — |
case-07 | fail→pass | 11,752 | 4,190 | -64% | 1 | 1 | 0% | 1,990 | 4,252 | +114% | 0 | 0 | — |
case-08 | pass→pass | 5,841 | 2,183 | -63% | 1 | 1 | 0% | 872 | 3,810 | +337% | 0 | 0 | — |
case-09 | fail→pass | 18,069 | 2,771 | -85% | 1 | 1 | 0% | 771 | 3,880 | +403% | 0 | 0 | — |
case-10 | pass→pass | 13,521 | 3,559 | -74% | 1 | 1 | 0% | 2,010 | 4,167 | +107% | 0 | 0 | — |
case-11 | fail→pass | 11,676 | 4,192 | -64% | 1 | 1 | 0% | 1,916 | 4,249 | +122% | 0 | 0 | — |
case-12 | fail→pass | 8,961 | 2,365 | -74% | 1 | 1 | 0% | 1,458 | 3,900 | +167% | 0 | 0 | — |
case-13 | pass→pass | 15,036 | 2,672 | -82% | 1 | 1 | 0% | 2,505 | 3,897 | +56% | 0 | 0 | — |
case-14 | fail→pass | 9,114 | 3,786 | -58% | 1 | 1 | 0% | 1,611 | 4,161 | +158% | 0 | 0 | — |
case-15 | pass→pass | 11,987 | 1,559 | -87% | 1 | 1 | 0% | 2,058 | 3,702 | +80% | 0 | 0 | — |
case-16 | fail→pass | 5,740 | 4,586 | -20% | 1 | 1 | 0% | 865 | 4,411 | +410% | 0 | 0 | — |
case-17 | fail→pass | 11,456 | 6,929 | -40% | 1 | 1 | 0% | 1,816 | 4,885 | +169% | 0 | 0 | — |
case-18 | fail→pass | 9,552 | 5,814 | -39% | 1 | 1 | 0% | 1,500 | 4,476 | +198% | 0 | 0 | — |
case-19 | pass→pass | 8,877 | 2,687 | -70% | 1 | 1 | 0% | 1,478 | 3,897 | +164% | 0 | 0 | — |
case-20 | pass→pass | 17,527 | 14,543 | -17% | 1 | 1 | 0% | 3,301 | 6,270 | +90% | 0 | 0 | — |
case-21 | pass→pass | 13,567 | 16,050 | +18% | 1 | 1 | 0% | 2,782 | 6,757 | +143% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +59 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.