tools/epro2/std: rewrite to Option 2 (objects dump) per downstream spec
Downstream came back with concrete requirements: don't pre-compute Std
shape[] tilde strings, just dump the raw EPRO2 `objects: {id: payload}`
dict and they'll write a ~100-LoC adapter on their side. Pulling the
tilde-mapping work back saves us from second-guessing positional fields
without their parser to verify against, and shortens our pcb_writer
from ~500 lines to ~40.
Output shape (Std envelope intact, just no `shape[]`):
{
"success": true, "code": 0,
"result": {
"uuid", "puuid", "title",
"docType": 3 | 1,
"components": {},
"dataStr": {
"head": {
"docType": "3" | "1",
"editorVersion": "facere-epro2/0.1 (epro2 <X.Y.Z>)",
"units": "mil",
"epro2_doc_uuid": ...,
"epro2_editor_version": ...,
},
"BBox": {x, y, width, height}, # mil
"layers": [...], # Std layer-string array
"objects": dict(doc.objects), # raw EPRO2, 1:1
"preference": {}, "netColors": [], "DRCRULE": {},
}
}
}
Per-doc spec downstream gave us:
- shape[] dropped (empty placeholder misleads adapter)
- all units mil (no mm conversion — Std canvas already declares mil)
- head.units="mil" so adapter doesn't have to guess
- BBox min/max across known x/y/startX/endX/centerX fields; adapter
can refine by walking path arrays itself
- layers[] keeps Std's 17-line default + inner SIGNAL layers actually
used (21~Inner1.., 22~Inner2..)
- empty stubs preference/netColors/DRCRULE for grep-based triage
New: docs/sources/epro2_to_std_mapping.md with the full EPRO2 OPTYPE →
Std verb table that downstream's adapter authors will copy from. Tables
include the layer-id remapping (the 5↔7 paste/mask flip, 11→10 outline,
12→11 multi, SIGNAL 15+→21+), PCB op mappings, SCH op mappings (marked
best-effort: no Std SCH samples in our corpus), and the 5-Voltage
placeholder COMPONENT → extra net flag trick. Extracted from the
previous Option-3 writer (commit fe6971f) so adapter writers don't
have to reverse-engineer it from source.
ESP-VoCat smoke: 6 PCB + 9 SCH = 15 JSON files, head.units=mil
preserved, no shape[] field present. 82 → 84 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
241
docs/sources/epro2_to_std_mapping.md
Normal file
241
docs/sources/epro2_to_std_mapping.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# EPRO2 OPTYPE → EasyEDA Std shape verb mapping
|
||||
|
||||
For downstream adapters that consume `tools/epro2/std/`'s Option-2 output
|
||||
(raw `objects: {id: payload}` dict in the `dataStr` field) and need to
|
||||
produce real Std `shape[]` tilde strings.
|
||||
|
||||
This table is the same mapping our previous Phase-3 writer encoded
|
||||
inline (`fe6971f:tools/epro2/std/pcb_writer.py`); we extracted it here
|
||||
so adapter authors don't have to reverse-engineer it from the writer
|
||||
source.
|
||||
|
||||
All EPRO2 coordinate fields are in **mil**; Std `dataStr.canvas` declares
|
||||
`mil` as its unit, so the adapter copies coords through unchanged.
|
||||
|
||||
## Layer id remapping
|
||||
|
||||
EPRO2 and Std agree on most copper layer ids, but differ on the mask /
|
||||
paste layers (5↔7 swapped) and have different numbering for OUTLINE /
|
||||
MULTI / inner SIGNAL.
|
||||
|
||||
| EPRO2 id | EPRO2 type | Std id | Std name |
|
||||
|---------:|------------------------|-------:|-----------------------|
|
||||
| 1 | TOP | 1 | TopLayer |
|
||||
| 2 | BOTTOM | 2 | BottomLayer |
|
||||
| 3 | TOP_SILK | 3 | TopSilkLayer |
|
||||
| 4 | BOT_SILK | 4 | BottomSilkLayer |
|
||||
| 5 | TOP_SOLDER_MASK | **7** | TopSolderMaskLayer |
|
||||
| 6 | BOT_SOLDER_MASK | **8** | BottomSolderMaskLayer |
|
||||
| 7 | TOP_PASTE_MASK | **5** | TopPasteMaskLayer |
|
||||
| 8 | BOT_PASTE_MASK | **6** | BottomPasteMaskLayer |
|
||||
| 9 | TOP_ASSEMBLY | 13 | TopAssembly |
|
||||
| 10 | BOT_ASSEMBLY | 14 | BottomAssembly |
|
||||
| 11 | OUTLINE | 10 | BoardOutLine |
|
||||
| 12 | MULTI (THT pads) | 11 | Multi-Layer |
|
||||
| 13 | DOCUMENT | 12 | Document |
|
||||
| 14 | MECHANICAL | 15 | Mechanical |
|
||||
| 15..46 | SIGNAL inner (in use) | 21..50 | Inner1..InnerN |
|
||||
|
||||
The 21..50 inner mapping is dense — assign Std `21` to the lowest-numbered
|
||||
EPRO2 SIGNAL id actually carrying geometry on this board, `22` to the
|
||||
next, etc. EPRO2 SIGNAL layers declared in LAYER ops but unused don't
|
||||
need a Std slot.
|
||||
|
||||
## PCB OPTYPE → Std shape verb (docType=3)
|
||||
|
||||
### LINE (copper trace, silk line, ...) → `TRACK`
|
||||
|
||||
```
|
||||
TRACK~width~layer~net~points~uuid~locked
|
||||
```
|
||||
- `width` ← `LINE.width` (mil)
|
||||
- `layer` ← `_layer(LINE.layerId)` via the table above
|
||||
- `net` ← `LINE.netName` (string, may be empty for non-net graphics)
|
||||
- `points` ← `"<startX> <startY> <endX> <endY>"` (mil, space-separated)
|
||||
- `uuid` ← any unique `gge<8 hex>` id; downstream usually mints fresh
|
||||
- `locked` ← `0`
|
||||
|
||||
EPRO2 doesn't distinguish copper trace from silk line at the op level —
|
||||
both are LINE with a different `layerId`. Std uses `TRACK` for both;
|
||||
the layer id is what disambiguates.
|
||||
|
||||
### VIA → `VIA`
|
||||
|
||||
```
|
||||
VIA~x~y~outerD~net~innerD~uuid~locked
|
||||
```
|
||||
- `x` `y` ← `VIA.centerX/centerY`
|
||||
- `outerD` ← `VIA.viaDiameter`
|
||||
- `innerD` ← `VIA.holeDiameter`
|
||||
- `net` ← `VIA.netName`
|
||||
|
||||
### POUR → `COPPERAREA`
|
||||
|
||||
```
|
||||
COPPERAREA~1~layer~net~svgPath~strokeWidth~~~~~~~uuid~locked
|
||||
```
|
||||
- `1` is the `id` slot Std uses; any int works
|
||||
- `svgPath` ← convert `POUR.path` to SVG `M..L..Z` string. Three
|
||||
EPRO2 path encodings:
|
||||
- rectangle `[['R', x, y, w, h, ...]]` → 4-corner closed polygon
|
||||
- circle `[['CIRCLE', cx, cy, r]]` → 24-segment polygon approximation
|
||||
- polyline `[[x1, y1, 'L', x2, y2, ..., 'ARC', radius, endX, endY, ...]]`
|
||||
→ walk numeric pairs as `M x y` (first) / `L x y` (rest); ARC verbs
|
||||
chord-approximate to `L endX endY` (good enough for fill connectivity,
|
||||
Phase-2 sticks with this; precise arc chord recovery is a follow-up)
|
||||
- `strokeWidth` ← `POUR.width`
|
||||
|
||||
### FILL (manual filled region) → `SOLIDREGION`
|
||||
|
||||
```
|
||||
SOLIDREGION~99~~svgPath~solid~uuid~~~~locked
|
||||
```
|
||||
- Same SVG-path encoding as COPPERAREA
|
||||
- `99` is the `id` slot; the `~~` after it is an empty layer field
|
||||
(FILL on EPRO2 carries `layerId` but Std SOLIDREGION leaves it blank
|
||||
for "uses the path's natural color"; this is fine for downstream)
|
||||
|
||||
### POLY with `path[0] == 'CIRCLE'` → `CIRCLE`
|
||||
|
||||
```
|
||||
CIRCLE~cx~cy~radius~strokeWidth~layer~uuid~locked~~
|
||||
```
|
||||
|
||||
### POLY with polyline path → `SOLIDREGION` (graphic polygon)
|
||||
|
||||
Same as FILL.
|
||||
|
||||
### COMPONENT (+ its FOOTPRINT.PADs) → `LIB...#@$PAD...#@$TEXT...`
|
||||
|
||||
The Std `LIB` shape is one outer string plus N inner shapes joined by
|
||||
the literal three-byte separator `#@$`. The outer carries placement; each
|
||||
inner is a real PAD / TEXT shape with the **PCB-absolute coords** that
|
||||
result from rotating + translating the FOOTPRINT-local pad positions.
|
||||
|
||||
Outer:
|
||||
```
|
||||
LIB~x~y~package_name`~rotation~~uuid~display~~~locked~~yes~~
|
||||
```
|
||||
- `x` `y` ← `COMPONENT.x/y` (mil)
|
||||
- `package_name` ← FOOTPRINT META.title (then a literal trailing backtick)
|
||||
- `rotation` ← `COMPONENT.angle` (degrees)
|
||||
- `display` `1`, `locked` `0`
|
||||
|
||||
Inner PAD (one per FOOTPRINT.PAD owned by this COMPONENT):
|
||||
```
|
||||
PAD~shape~x~y~width~height~layer~net~num~drillSize~~rotation~uuid~0~~Y~0~0~0.2~x,y
|
||||
```
|
||||
- `shape` ← `defaultPad.padType` ∈ {`RECT`, `ELLIPSE`, `OVAL`, `POLYGON`}
|
||||
- `x` `y` ← absolute coords:
|
||||
```
|
||||
abs_x = comp.x + pad.centerX * cos(comp.angle) − pad.centerY * sin(comp.angle)
|
||||
abs_y = comp.y + pad.centerX * sin(comp.angle) + pad.centerY * cos(comp.angle)
|
||||
```
|
||||
- `width` `height` ← `defaultPad.width/height`
|
||||
- `layer` ← `_layer(pad.layerId)` (typically 1=TOP, 2=BOTTOM, 11=Multi for THT)
|
||||
- `net` ← resolve via PCB-level `PAD_NET` op:
|
||||
the PCB doc has ops with composite ids
|
||||
`["PAD_NET", <component_id>, <pin_num>, <pad_id>]` → `padNet` payload
|
||||
is the net name. Cross-doc lookup; the FOOTPRINT itself doesn't know
|
||||
the net of any specific instance.
|
||||
- `num` ← `pad.num` (pin number, string)
|
||||
- `drillSize` ← `pad.hole.width` if hole present, else `0`
|
||||
- `rotation` ← `(pad.padAngle + comp.angle) % 360`
|
||||
|
||||
Inner TEXT (designator + value, one each if attrs present):
|
||||
```
|
||||
TEXT~P~x~y~strokeWidth~rotation~mirror~layer~font~size~content~svgPath~visible
|
||||
```
|
||||
- `P` flag = property text (vs `L` for label)
|
||||
- `content` ← attrs.Designator / attrs.Value pulled from ATTR ops with
|
||||
`parentId = component_id`
|
||||
|
||||
The downstream adapter doesn't need a separate ATTR walk — by the time
|
||||
it has the COMPONENT's ATTR-derived attrs (Designator, Value, Footprint,
|
||||
...), those are typically already collapsed into a `attrs_dict` map
|
||||
(`tools.epro2.relations.Relations.attrs_dict(parent_id)` does this).
|
||||
|
||||
## Schematic OPTYPE → Std verb (docType=1, **best-effort**)
|
||||
|
||||
We have zero Std schematic samples in `data/raw/oshwhub/*/source/` (all
|
||||
the projects we crawled are PCB-only Std exports), so the field orders
|
||||
below follow the **EasyEDA Std public schematic spec**, not direct
|
||||
observation. Adapter authors should expect to tweak field positions if
|
||||
their parser rejects a verb.
|
||||
|
||||
### LINE → `W` (wire segment)
|
||||
|
||||
```
|
||||
W~strokeColor~strokeWidth~strokeStyle~points~uuid~locked
|
||||
```
|
||||
- `points` ← same `<x1> <y1> <x2> <y2>` form as TRACK
|
||||
|
||||
### LINE.lineGroup with parent WIRE.NET attr → also emit `N` (net flag)
|
||||
|
||||
```
|
||||
N~x~y~rotation~text~uuid~locked
|
||||
```
|
||||
EPRO2 binds wire segments by NET name, not just geometry. Place one N
|
||||
flag at each LINE's start endpoint, with the `text` set to the parent
|
||||
WIRE op's `NET` ATTR value. Same-named flags on physically distinct
|
||||
wire segments is how Std unifies a multi-segment named net.
|
||||
|
||||
### COMPONENT (+ its SYMBOL primitives) → `LIB...#@$P...`
|
||||
|
||||
Outer:
|
||||
```
|
||||
LIB~x~y~package`<symbol_title>`~rotation~~uuid~display~~~locked~~yes~~
|
||||
```
|
||||
|
||||
Inner per SYMBOL.PIN:
|
||||
```
|
||||
P~show~0~~x~y~rotation~uuid^^pin_number^^pin_name^^length
|
||||
```
|
||||
|
||||
(Note: PIN field separator inside the inner string uses `^^` not `~`,
|
||||
per spec — but this varies by editor version. If downstream's parser
|
||||
rejects PIN, this is the most likely culprit.)
|
||||
|
||||
### Power-port placeholder → `LIB` + extra `N`
|
||||
|
||||
EPRO2 represents power rails (VBUS / GND / VCC / VBAT_IN / ...) as a
|
||||
generic placeholder COMPONENT with `partId = "pid8a0e77bacb214e"` whose
|
||||
**Global Net Name** ATTR carries the rail name. For each such instance,
|
||||
emit the regular `LIB` placement *plus* an `N` flag at the placement
|
||||
coords with the Global Net Name as `text` — that's how the symbol's pin
|
||||
binds to the global rail. (This mirrors the same fix our KiCad path uses
|
||||
to emit a `(global_label)` for these.)
|
||||
|
||||
### TEXT → `T`
|
||||
|
||||
```
|
||||
T~x~y~rotation~text~uuid~locked
|
||||
```
|
||||
|
||||
## Skipped / "not yet supported"
|
||||
|
||||
These exist in EPRO2 but our writer doesn't address them — adapters can
|
||||
choose to skip silently or emit best-effort placeholders:
|
||||
|
||||
| EPRO2 op | Std target | Notes |
|
||||
|------------|------------|--------------------------------------------------------|
|
||||
| TEARDROP | (drop) | Cosmetic fillets at via/pad-trace junctions |
|
||||
| ARC (PCB) | `ARC` | Std verb exists; we emit only chord-approximated ones |
|
||||
| IMAGE | `SVGNODE` | Bitmap logos; Std stores as embedded SVG JSON |
|
||||
| STRING (PCB) | `TEXT` | Board-level text; field order distinct from PCB TEXT-in-LIB |
|
||||
| BUS / BE (SCH) | `BUS` / `BE` | Bus + bus entry — no EPRO2 sample in our corpus |
|
||||
|
||||
## Provenance fields the adapter can rely on
|
||||
|
||||
In addition to `objects`, our writer always emits:
|
||||
- `result.dataStr.head.docType` `"3"` (PCB) or `"1"` (SCH) — same string
|
||||
encoding Std uses
|
||||
- `result.dataStr.head.units` `"mil"` — explicit unit hint so the
|
||||
adapter doesn't have to guess
|
||||
- `result.dataStr.head.editorVersion` `"facere-epro2/0.1 (epro2 X.Y.Z)"`
|
||||
where X.Y.Z is the EPRO2 doc's `editVersion`. Useful for triage when
|
||||
a board exhibits version-specific quirks.
|
||||
- `result.dataStr.BBox` `{x, y, width, height}` — gross outer rectangle
|
||||
from min/max of every numeric `x/y/startX/startY/endX/endY/centerX/centerY`
|
||||
field across `objects`. Adapters that want a tighter BBox can refine
|
||||
by walking `path` arrays themselves.
|
||||
Reference in New Issue
Block a user