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>
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
"""CLI: dump EPRO2 docs to Std-shaped JSON files for downstream consumers.
|
|
|
|
The output is "Option 2" per the downstream colleague's spec: Std envelope
|
|
with a raw EPRO2 ``objects: {id: payload}`` dict in place of the usual
|
|
``shape[]`` tilde-string array. Their ~100-LoC adapter walks ``objects``
|
|
and dispatches by ``_type`` to build real Std shapes — see
|
|
``docs/sources/epro2_to_std_mapping.md`` for the OPTYPE → Std verb table.
|
|
|
|
Usage:
|
|
uv run python -m tools.epro2.std <project_dir> --all-pcb --out <dir>
|
|
uv run python -m tools.epro2.std <project_dir> --all-sch --out <dir>
|
|
uv run python -m tools.epro2.std <project_dir> --all --out <dir>
|
|
|
|
Output: flat ``<doc_uuid>.json`` per doc — mirrors Std's own data layout
|
|
so a downstream pipeline that already iterates ``source/*.json`` works
|
|
unchanged.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from ..replay import Project, replay_project
|
|
from .pcb_writer import write_pcb_std
|
|
from .sch_writer import write_sch_std
|
|
|
|
|
|
def _dump(payload: dict, out_path: Path, project_uuid: str) -> None:
|
|
payload["result"]["puuid"] = project_uuid or ""
|
|
out_path.write_text(
|
|
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _convert_pcbs(proj: Project, out_dir: Path) -> int:
|
|
uuids = [u for u, d in proj.documents.items() if d.doc_type == "PCB"]
|
|
if not uuids:
|
|
return 0
|
|
print(f"PCB: dumping {len(uuids)} doc(s) → {out_dir}")
|
|
for u in uuids:
|
|
try:
|
|
payload = write_pcb_std(proj.documents[u])
|
|
except Exception as e: # noqa: BLE001
|
|
print(f" FAIL {u[:12]}: {e}", file=sys.stderr)
|
|
continue
|
|
_dump(payload, out_dir / f"{u}.json", proj.project_uuid or "")
|
|
s = getattr(write_pcb_std, "last_stats", None)
|
|
if s:
|
|
print(
|
|
f" {u[:12]}.json: objects={s.objects} layers={s.layers_emitted} "
|
|
f"BBox=({s.bbox_x:g},{s.bbox_y:g},{s.bbox_w:g},{s.bbox_h:g})"
|
|
)
|
|
return len(uuids)
|
|
|
|
|
|
def _convert_schs(proj: Project, out_dir: Path) -> int:
|
|
uuids = [u for u, d in proj.documents.items() if d.doc_type == "SCH_PAGE"]
|
|
if not uuids:
|
|
return 0
|
|
print(f"SCH: dumping {len(uuids)} doc(s) → {out_dir}")
|
|
for u in uuids:
|
|
try:
|
|
payload = write_sch_std(proj.documents[u])
|
|
except Exception as e: # noqa: BLE001
|
|
print(f" FAIL {u[:12]}: {e}", file=sys.stderr)
|
|
continue
|
|
_dump(payload, out_dir / f"{u}.json", proj.project_uuid or "")
|
|
s = getattr(write_sch_std, "last_stats", None)
|
|
if s:
|
|
print(
|
|
f" {u[:12]}.json: objects={s.objects} "
|
|
f"BBox=({s.bbox_x:g},{s.bbox_y:g},{s.bbox_w:g},{s.bbox_h:g})"
|
|
)
|
|
return len(uuids)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ap = argparse.ArgumentParser(description="EPRO2 → EasyEDA Std-shaped JSON dump")
|
|
ap.add_argument("project_dir", type=Path)
|
|
g = ap.add_mutually_exclusive_group(required=True)
|
|
g.add_argument("--all-pcb", action="store_true", help="dump every PCB doc")
|
|
g.add_argument("--all-sch", action="store_true", help="dump every SCH_PAGE doc")
|
|
g.add_argument("--all", action="store_true", help="dump both PCB and SCH_PAGE docs")
|
|
ap.add_argument("--out", type=Path, default=Path("data/processed/std_json"))
|
|
args = ap.parse_args(argv)
|
|
|
|
proj = replay_project(args.project_dir)
|
|
args.out.mkdir(parents=True, exist_ok=True)
|
|
|
|
n = 0
|
|
if args.all_pcb or args.all:
|
|
n += _convert_pcbs(proj, args.out)
|
|
if args.all_sch or args.all:
|
|
n += _convert_schs(proj, args.out)
|
|
if n == 0:
|
|
print("nothing to dump (no PCB / SCH_PAGE docs found)", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|