ADR 012: Intermediate Representation (IR) Architecture Refactoring¶
Status¶
Decided (2026-07-17), pending implementation
Context¶
The current export pipeline is:
The data model layer (src/models/) is nominally an intermediate layer, but in practice it is heavily contaminated with EasyEDA-specific data:
- Metadata pollution:
SymbolInfo/FootprintInfocontain EasyEDA platform fields (docType,datastrid,jlcOnSale,lcscId,supplierPart, etc.) irrelevant to export logic - Unparsed geometry: Point coordinates and paths stored as raw EasyEDA delimited strings (
QString points = "x1,y1 x2,y2") instead of numeric types - Hardcoded layer IDs: Uses EasyEDA numeric layer IDs (e.g.
layerId == 99for KeepOut) with no abstraction - Unsafe pin type casting:
static_cast<PinType>(settings[2].toInt())directly maps from EasyEDA integers without standardized mapping - Raw pad shape strings:
"RECT","ELLIPSE"etc. stored as strings instead of enums
This prevents: - Integrating new data sources (e.g. Altium library import) since the intermediate layer is bound to EasyEDA's format - Testing exporters independently, since EasyEDA-specific data must be constructed - Clean geometry handling, as string parsing is duplicated across exporters
Decision¶
Introduce a generic Intermediate Representation (IR) layer to fully decouple data sources from exporters¶
Target Architecture¶
flowchart TD
EasyEDA[EasyEDA API JSON] --> EasyImporter[EasyEDA Importer]
AltiumSource[Altium Library] --> AltiumImporter[Altium Importer]
Future[Future Data Source] --> FutureImporter[Corresponding Importer]
EasyImporter --> IR[Generic IR Layer<br/>ComponentIR / SymbolIR / FootprintIR / Model3DIR]
AltiumImporter --> IR
FutureImporter --> IR
IR --> KiCad[KiCad Exporter]
IR --> Altium[Altium Exporter]
Core Principles¶
- Zero external dependency in IR: No EasyEDA, Altium, or KiCad-specific constants, string formats, or platform fields
- Parsed geometry: All coordinates and paths stored as numeric types in IR
- Type safety: Enums replace raw strings and integers
- Importers own parsing: Each data source's format is fully parsed and mapped in its own Importer
- Exporters only consume IR: Exporters have no knowledge of data source format
Directory Structure Change¶
src/core/
├── ir/ # [NEW] Generic intermediate representation
│ ├── ComponentIR.h
│ ├── SymbolIR.h / .cpp
│ ├── FootprintIR.h / .cpp
│ ├── Model3DIR.h
│ ├── IRTypes.h # Shared enums and type definitions
│ └── CMakeLists.txt
│
├── importers/ # [NEW] Data source importers
│ ├── easyeda/ # Migrated from existing models/ + EasyedaXxxImporter
│ │ ├── EasyedaSymbolImporter.h / .cpp
│ │ ├── EasyedaFootprintImporter.h / .cpp
│ │ ├── EasyedaMetadata.h # EasyEDA-specific metadata struct
│ │ └── CMakeLists.txt
│ └── CMakeLists.txt
│
├── exporters/ # [REORGANIZED] Exporters consume IR
│ ├── kicad/
│ └── altium/
│
└── easyeda/ # [KEPT] EasyEDA API client (unchanged)
Deleted files: src/models/SymbolData.*, FootprintData.*, ComponentData.*, Model3DData.*, and all serializers. Logic split into IR types + importer mappings.
IR Type Definitions (IRTypes.h)¶
enum class PadShape { Rect, Ellipse, RoundedRectangle, Polygon, Oblong };
enum class PinElectricalType { Input, Output, Bidirectional, Passive, Power, OpenCollector, OpenEmitter, Unspecified };
enum class PinDirection { Right, Left, Up, Down };
enum class LayerType {
TopCopper, BottomCopper, TopSilk, BottomSilk, TopPaste, BottomPaste,
TopMask, BottomMask, TopOverlay, BottomOverlay, MultiLayer, KeepOut,
Mechanical1, Mechanical2, /* ... */ UserDefined
};
Implementation Phases¶
| Phase | Duration | Risk | Scope |
|---|---|---|---|
| 1: IR types + geometry parsing | 1 week | Low | src/core/ir/, enum definitions, mapping tables |
| 2: Importer migration | 1 week | Medium | src/core/importers/easyeda/, remove old models, golden file validation |
| 3: Exporter adaptation | 1 week | Medium | Exporters consume IR, ViewModel/Service/CLI/BOM adaptation, cache migration |
Exporter Interface Change¶
// Before (consumes EasyEDA model)
class ISymbolExporter {
virtual bool exportSymbol(const SymbolData& data) = 0;
};
// After (consumes IR)
class ISymbolExporter {
virtual bool exportSymbol(const SymbolComponentIR& ir) = 0;
};
Layer ID Mapping Strategy¶
Each source maintains its own mapping in its Importer:
// importers/easyeda/EasyedaLayerMap.h
LayerType toLayerType(int easyedaLayerId); // 99 -> KeepOut, 1 -> TopCopper
// importers/altium/AltiumLayerMap.h (future)
LayerType toLayerType(int altiumLayerId);
Consequences¶
Benefits¶
- New data source integration cost reduced to writing one Importer class
- Exporters fully decoupled from source format; testable with direct IR construction
- Geometry parsing consolidated in one place instead of duplicated across exporters
- Type-safe enums prevent runtime string comparison errors
- Clear separation of concerns across the codebase
Costs¶
- ~20-25 files affected (new + deleted + modified)
- Cache serialization format changes; old cache auto-invalidated
- 3 weeks estimated total implementation time
- Regression risk at Phase 2 (model switchover) requires golden file comparison
No External Impact¶
- CLI arguments unchanged
- GUI operation unchanged
- Exported file format unchanged
- No new external dependencies
Follow-up Opportunities¶
With IR in place, these become straightforward:
| Data Source | Work Required | Estimate |
|---|---|---|
| Altium library import | OLECompoundReader + AltiumSchLibReader + AltiumPcbLibReader + AltiumImporter |
2-3 weeks |
| KiCad library import | S-expression parser + KicadImporter |
1-2 weeks |
| Other EDA formats | Only Importer needed; exporters untouched | Per importer complexity |
References¶
- [[001-mvvm-architecture]] - Project MVVM architecture
- [[002-pipeline-parallelism-for-export]] - Export pipeline architecture
- [[010-component-cache-architecture]] - Cache architecture (needs adaptation)
- AltiumSharp project - Altium format reference implementation
- Validation tool:
/tmp/validate_altium_lib.py- OLE structure validator