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:
@@ -1,15 +1,19 @@
|
||||
"""CLI: convert EPRO2 docs to EasyEDA Std-format JSON files.
|
||||
"""CLI: dump EPRO2 docs to Std-shaped JSON files for downstream consumers.
|
||||
|
||||
Mirrors the layout of Std project sources: one ``<doc_uuid>.json`` per
|
||||
document, flat in ``--out``. Use this for downstream consumers that
|
||||
already speak Std (Wokwi-based pipelines, dataStr parsers, etc.) — the
|
||||
KiCad writer at ``tools.epro2.kicad`` is the alternate target for
|
||||
downstream that wants kicad_sch / kicad_pcb instead.
|
||||
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
|
||||
@@ -19,85 +23,81 @@ import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ..project_relations import ProjectRelations
|
||||
from ..replay import Project, replay_project
|
||||
from .pcb_writer import write_pcb_std
|
||||
from .sch_writer import write_sch_std
|
||||
|
||||
|
||||
def _convert_pcbs(proj: Project, out_dir: Path, pr: ProjectRelations) -> int:
|
||||
pcb_uuids = [u for u, d in proj.documents.items() if d.doc_type == "PCB"]
|
||||
if not pcb_uuids:
|
||||
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: converting {len(pcb_uuids)} doc(s) → {out_dir}")
|
||||
for u in pcb_uuids:
|
||||
print(f"PCB: dumping {len(uuids)} doc(s) → {out_dir}")
|
||||
for u in uuids:
|
||||
try:
|
||||
payload = write_pcb_std(proj.documents[u], project_relations=pr)
|
||||
payload = write_pcb_std(proj.documents[u])
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" FAIL {u[:12]}: {e}", file=sys.stderr)
|
||||
continue
|
||||
# Stamp puuid so downstream can wire docs back to a project
|
||||
payload["result"]["puuid"] = proj.project_uuid or ""
|
||||
(out_dir / f"{u}.json").write_text(
|
||||
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
_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: tracks={s.tracks} vias={s.vias} "
|
||||
f"copperareas={s.copperareas} libs={s.libs} pads={s.pads} "
|
||||
f"libs_unresolved={s.libs_unresolved}"
|
||||
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(pcb_uuids)
|
||||
return len(uuids)
|
||||
|
||||
|
||||
def _convert_schs(proj: Project, out_dir: Path, pr: ProjectRelations) -> int:
|
||||
sch_uuids = [u for u, d in proj.documents.items() if d.doc_type == "SCH_PAGE"]
|
||||
if not sch_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: converting {len(sch_uuids)} doc(s) → {out_dir}")
|
||||
for u in sch_uuids:
|
||||
print(f"SCH: dumping {len(uuids)} doc(s) → {out_dir}")
|
||||
for u in uuids:
|
||||
try:
|
||||
payload = write_sch_std(proj.documents[u], project_relations=pr)
|
||||
payload = write_sch_std(proj.documents[u])
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" FAIL {u[:12]}: {e}", file=sys.stderr)
|
||||
continue
|
||||
payload["result"]["puuid"] = proj.project_uuid or ""
|
||||
(out_dir / f"{u}.json").write_text(
|
||||
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
_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: wires={s.wires} libs={s.libs} "
|
||||
f"netflags={s.netflags} texts={s.texts} libs_unresolved={s.libs_unresolved}"
|
||||
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(sch_uuids)
|
||||
return len(uuids)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description="EPRO2 → EasyEDA Std JSON exporter")
|
||||
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="convert every PCB doc to Std JSON")
|
||||
g.add_argument("--all-sch", action="store_true", help="convert every SCH_PAGE doc to Std JSON")
|
||||
g.add_argument("--all", action="store_true", help="convert both PCB and SCH_PAGE docs")
|
||||
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)
|
||||
pr = ProjectRelations.build(proj)
|
||||
|
||||
n = 0
|
||||
if args.all_pcb or args.all:
|
||||
n += _convert_pcbs(proj, args.out, pr)
|
||||
n += _convert_pcbs(proj, args.out)
|
||||
if args.all_sch or args.all:
|
||||
n += _convert_schs(proj, args.out, pr)
|
||||
n += _convert_schs(proj, args.out)
|
||||
if n == 0:
|
||||
print("nothing to convert (no PCB / SCH_PAGE docs found)", file=sys.stderr)
|
||||
print("nothing to dump (no PCB / SCH_PAGE docs found)", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user