Files
FacereDataset/docs/sources/epro2_to_std_mapping.md
Knowit 3866e24189 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>
2026-04-29 01:41:12 +08:00

242 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.