← Commit history

Initial commit: skill/altium-pcblib

Kyle Bergstedt ·cc7390431e ·4mo ago
2 files changed +261
README.mdadded+196
@@ -0,0 +1,196 @@+## What this is++`altium-pcblib` is a private Adom Rust crate that reads Altium Designer's+binary footprint libraries (`.PcbLib`, and the PCB stream embedded inside+`.IntLib`) and emits per-pad JSON shaped exactly like KiCad's+`parse_kicad_mod` output. One source of truth instead of every Adom tool+re-implementing OLE2 / CFBF walking + Altium's reverse-engineered binary+record format.++Already wired into:+- **adom-chipsmith** Tab 4 cross-source agreement matrix — fourth column+  alongside KiCad / Fusion / Datasheet.+- **concur** consensus check — auto-picks the right multi-variant+  footprint (IPC vs MFG) by pad-count agreement with other parsed sources.++Source lives in the private GitHub repo `adom-inc/altium-pcblib`. Skills+on the wiki cover discoverability + integration; the source itself is+not published here.++## Why a skill page, not a library page++In Adom-wiki terms a **library** is an EDA part bundle (symbol ++footprint + 3D + variants + pad↔pin mapping). `altium-pcblib` is a Rust+crate — so its wiki page is a **skill**: it teaches Claude/the team how+to use the crate, while the source stays private.++## Depending on it++In the workspace (chipsmith, concur, anything else under+`/home/adom/project/`):++```toml+[dependencies]+altium-pcblib = { path = "../altium-pcblib" }+```++On a build container without the workspace checkout:++```toml+[dependencies]+altium-pcblib = { git = "https://github.com/adom-inc/altium-pcblib", branch = "main" }+```++Single direct dep (`cfb`, the OLE2 reader) plus `anyhow` and `serde_json`.++## Public Rust API++```rust+// Parse from a file on disk.+pub fn parse_altium_pcblib_path(path: &Path) -> anyhow::Result<serde_json::Value>;++// Parse from raw bytes (handy for include_bytes! tests and HTTP bodies).+pub fn parse_altium_pcblib_bytes(bytes: &[u8]) -> anyhow::Result<serde_json::Value>;++// Render the parser's view of the footprint as standalone SVG. THIS IS THE+// AUTHORITATIVE GROUND TRUTH — if a downstream tool draws something different,+// the downstream tool is the bug.+pub fn render_footprint_svg(footprint: &serde_json::Value) -> String;++// Render primary + every alternate variant, stacked vertically in one SVG.+pub fn render_all_footprints_svg(parsed: &serde_json::Value) -> String;+```++The returned `Value` is shaped exactly like+`adom-chipsmith::view_library::parse_kicad_mod_text` output:++```json+{+  "module_name": "D8",+  "pad_count": 8,+  "pads": [{+    "number": "1", "type": "smd", "shape": "rect",+    "at": [-2.4638, -1.905, 0.0],+    "size": [1.9812, 0.5588],+    "layers": ["F.Cu", "F.Paste", "F.Mask"],+    "drill": null+  }],+  "silk_markers": [],+  "pin1_silk": { "found": false },+  "_source": "altium_pcblib",+  "_alternates": [ /* other footprints, sorted by pad_count desc */ ]+}+```++## CLI bin: `altium-pcblib-svg`++Single binary that ships with the crate. Use it whenever a downstream+renderer looks suspect:++```bash+# Primary footprint, SVG to stdout.+altium-pcblib-svg /path/to/Foo.PcbLib > foo.svg++# Primary + every alternate variant stacked vertically.+altium-pcblib-svg /path/to/Foo.PcbLib --all -o foo-all.svg+```++SVG conventions:+- 1 SVG unit = 1 mm. No scaling.+- Y-down (matches both KiCad and our emitted JSON).+- Rotation applied via `transform="rotate(deg cx cy)"` clockwise on screen.+- Pad numbers stay un-rotated for readability.+- F.Cu pads orange, B.Cu blue, thru-hole green, drill holes white on top.+- Origin crosshair at (0, 0) with 1 mm arms.++## Format quirks worth knowing++These bit us during reverse-engineering — they're encoded in the parser+already, but if you extend it or debug a weird chip, here's what's going+on.++### 1. FAT overshoot++INA226AIDGSR's PcbLib declares 256 FAT entries but only has 213 sectors.+The `cfb` crate refuses with `Malformed FAT … FAT has N entries, but+file has only M sectors`. Fix: pad the input bytes to 1 MiB with `0xFF`+(CFBF "free sector" sentinel) before opening. The unreferenced trailing+sectors don't hold real data.++### 2. Per-record-type sub-block count++Altium records aren't uniform `[type][len][payload]`. Different record+types carry different numbers of `[u32 len][bytes]` sub-blocks:++- Pad (0x02): 6 sub-blocks (designator, layer-name pstring, four metadata blocks).+- Text (0x05): 2 sub-blocks (main + style trailer).+- Track / Arc / Via / Fill / Region / ComponentBody: 1 sub-block.++If you hit an unknown type byte mid-stream, the parser stops walking+gracefully — pads always come first, so by then we already have what we+need.++### 3. Pad block-5 layout (the 194-byte v3 / 202-byte v4 pad data)++```+[0]      u8 layer (1 = TOP_CU, 32 = BOTTOM_CU, ...)+[1]      u8 net/flag (0x08 in v3, 0x0c in v4 — version probe)+[2]      u8 reserved+[3..13]  10 bytes of 0xFF (component / net id sentinels)+[13..17] i32 LE x position (units = 1 / 10000 mil = 2.54e-6 mm)+[17..21] i32 LE y position (Altium Y-up — flipped to KiCad Y-down at parse)+[21..29] u32 LE x size, y size — top layer+[29..37] u32 LE x size, y size — mid layer (multi-layer thru-hole pads)+[37..45] u32 LE x size, y size — bottom layer+[45..49] u32 LE hole size (0 → SMD; > 0 → thru-hole)+[49..52] u8 × 3 shape codes (1 round, 2 rect, 3 octagonal, 9 rounded-rect)+[52..60] f64 LE rotation in degrees — ONLY in 202-byte v4 layout+```++Past offset 60 the layout drifts between Altium releases (paste / mask+offsets, plated bool, GUIDs). The parser stops at 60.++### 4. Two equally valid rotation conventions in the wild++This is the one that *looks* like a parser bug but isn't. Altium gives+library authors freedom in how to encode a rotated pad:++- **Convention A (matches KiCad):** store the pad as a "vertical stick"+  (xsize < ysize), set rotation = 90° on the side pads.+- **Convention B (post-rotation):** store the physical post-rotation+  dimensions (xsize > ysize for a horizontal pad), set rotation = 0.++Both render to the same physical pad on the PCB. A downstream tool that+compares `size[]` element-wise without applying rotation will see a 90°+mismatch in convention B → KiCad. The fix lives in the consumer (compute+the post-rotation bbox before comparing), **not** in this parser. When in+doubt, render the authoritative SVG and compare visually.++### 5. Y-axis flip++Altium Y-up vs KiCad Y-down. The parser flips Y at parse time so emitted+`at[1]` is in KiCad convention. **Do not flip again downstream.**++### 6. Round shape with unequal w/h → "oval"++KiCad's `circle` implies w == h. Altium's "round" (shape byte 1) covers+both true circles and stadium-shaped SMD pads with rounded ends. The+parser specializes: shape 1 with `(xsize - ysize).abs() < 1e-3` →+`circle`, otherwise → `oval` (KiCad's stadium).++## Trouble-shooting checklist when downstream renders pads wrong++1. **Run the authoritative SVG first.** `altium-pcblib-svg <path>.PcbLib > /tmp/auth.svg`. If pads are right in the SVG, the bug is downstream.+2. **Check which variant got picked.** `parsed.module_name` plus `_alternates[*].module_name`. Manufacturing variants often have extra pads vs the IPC pinout variant; concur uses `parse_with_consensus_hint` to pick the right one.+3. **Check rotation convention.** If pad 1 looks correct but pad 13 looks rotated 90° wrong, you're probably hitting Convention B vs A — your downstream is comparing or rendering size literally without applying rotation.+4. **Check Y direction.** If pads are mirrored top-to-bottom, your downstream double-flipped Y. The parser already emits KiCad Y-down.+5. **Scale.** SVG units are mm. If the rendered size is off by 1000×, the consumer is treating mm as µm.++## Tested chip corpus++| Chip | Package | Variants | Notes |+|---|---|---|---|+| LM358D | SOIC-8 | D8 / D8-L / D8-M | Sanity baseline. 8 horizontal pads, rot=0 throughout. |+| INA226AIDGSR | VSSOP-10 | SOP50P490X110-10N | v3 layout (Convention B). Triggers FAT overshoot quirk. |+| ADS1115IDGSR | VSSOP-10 | DGS0010A_L / _M / _N | 3 alternate density variants. |+| DRV8316RRGFR | VQFN-40 | RGF0040E-IPC_A / _B / _C / -MFG | IPC variants are Convention A; MFG variant is Convention B + extra fiducials/test points. |
page.jsonadded+65
@@ -0,0 +1,65 @@+{+  "schema_version": 1,+  "type": "skill",+  "slug": "altium-pcblib",+  "title": "altium-pcblib",+  "brief": "Native Rust parser for Altium .PcbLib binary footprint libraries — used by chipsmith + concur for cross-source agreement.",+  "version": "1.0.1",+  "tags": [],+  "license": "MIT",+  "discovery_triggers": [+    "altium pcblib",+    "altium intlib",+    "parse altium binary footprint",+    "altium ole2",+    ".PcbLib",+    ".IntLib",+    "altium-pcblib",+    "altium svg debug",+    "altium-pcblib-svg",+    "why is my altium pad rotated wrong",+    "MFG vs IPC variant",+    "altium fat overshoot",+    "altium compound file",+    "read altium footprint in rust"+  ],+  "discovery_pitch": "Use when reading Altium .PcbLib / .IntLib binaries from Rust, debugging \"why is my altium pad rotated wrong\", or wiring a fourth source into a cross-source footprint check. Ships an authoritative SVG debug renderer.",+  "sample_prompts": [+    {+      "label": "Add to Cargo",+      "prompt": "Add altium-pcblib to my Cargo.toml as a git dependency"+    },+    {+      "label": "Render SVG",+      "prompt": "Render the parser's authoritative SVG for an Altium .PcbLib file"+    },+    {+      "label": "Compare to KiCad",+      "prompt": "Compare the Altium .PcbLib pad layout to the KiCad .kicad_mod for a chip and tell me where they disagree"+    },+    {+      "label": "Debug rotation",+      "prompt": "Why are my altium pads rotated 90 degrees wrong in chipsmith?"+    },+    {+      "label": "Pick variant",+      "prompt": "This .PcbLib has multiple variants — pick the one whose pad count matches the kicad footprint"+    }+  ],+  "source_path": "SKILL.md",+  "readme": "## What this is\n\n`altium-pcblib` is a private Adom Rust crate that reads Altium Designer's\nbinary footprint libraries (`.PcbLib`, and the PCB stream embedded inside\n`.IntLib`) and emits per-pad JSON shaped exactly like KiCad's\n`parse_kicad_mod` output. One source of truth instead of every Adom tool\nre-implementing OLE2 / CFBF walking + Altium's reverse-engineered binary\nrecord format.\n\nAlready wired into:\n- **adom-chipsmith** Tab 4 cross-source agreement matrix — fourth column\n  alongside KiCad / Fusion / Datasheet.\n- **concur** consensus check — auto-picks the right multi-variant\n  footprint (IPC vs MFG) by pad-count agreement with other parsed sources.\n\nSource lives in the private GitHub repo `adom-inc/altium-pcblib`. Skills\non the wiki cover discoverability + integration; the source itself is\nnot published here.\n\n## Why a skill page, not a library page\n\nIn Adom-wiki terms a **library** is an EDA part bundle (symbol +\nfootprint + 3D + variants + pad↔pin mapping). `altium-pcblib` is a Rust\ncrate — so its wiki page is a **skill**: it teaches Claude/the team how\nto use the crate, while the source stays private.\n\n## Depending on it\n\nIn the workspace (chipsmith, concur, anything else under\n`/home/adom/project/`):\n\n```toml\n[dependencies]\naltium-pcblib = { path = \"../altium-pcblib\" }\n```\n\nOn a build container without the workspace checkout:\n\n```toml\n[dependencies]\naltium-pcblib = { git = \"https://github.com/adom-inc/altium-pcblib\", branch = \"main\" }\n```\n\nSingle direct dep (`cfb`, the OLE2 reader) plus `anyhow` and `serde_json`.\n\n## Public Rust API\n\n```rust\n// Parse from a file on disk.\npub fn parse_altium_pcblib_path(path: &Path) -> anyhow::Result<serde_json::Value>;\n\n// Parse from raw bytes (handy for include_bytes! tests and HTTP bodies).\npub fn parse_altium_pcblib_bytes(bytes: &[u8]) -> anyhow::Result<serde_json::Value>;\n\n// Render the parser's view of the footprint as standalone SVG. THIS IS THE\n// AUTHORITATIVE GROUND TRUTH — if a downstream tool draws something different,\n// the downstream tool is the bug.\npub fn render_footprint_svg(footprint: &serde_json::Value) -> String;\n\n// Render primary + every alternate variant, stacked vertically in one SVG.\npub fn render_all_footprints_svg(parsed: &serde_json::Value) -> String;\n```\n\nThe returned `Value` is shaped exactly like\n`adom-chipsmith::view_library::parse_kicad_mod_text` output:\n\n```json\n{\n  \"module_name\": \"D8\",\n  \"pad_count\": 8,\n  \"pads\": [{\n    \"number\": \"1\", \"type\": \"smd\", \"shape\": \"rect\",\n    \"at\": [-2.4638, -1.905, 0.0],\n    \"size\": [1.9812, 0.5588],\n    \"layers\": [\"F.Cu\", \"F.Paste\", \"F.Mask\"],\n    \"drill\": null\n  }],\n  \"silk_markers\": [],\n  \"pin1_silk\": { \"found\": false },\n  \"_source\": \"altium_pcblib\",\n  \"_alternates\": [ /* other footprints, sorted by pad_count desc */ ]\n}\n```\n\n## CLI bin: `altium-pcblib-svg`\n\nSingle binary that ships with the crate. Use it whenever a downstream\nrenderer looks suspect:\n\n```bash\n# Primary footprint, SVG to stdout.\naltium-pcblib-svg /path/to/Foo.PcbLib > foo.svg\n\n# Primary + every alternate variant stacked vertically.\naltium-pcblib-svg /path/to/Foo.PcbLib --all -o foo-all.svg\n```\n\nSVG conventions:\n- 1 SVG unit = 1 mm. No scaling.\n- Y-down (matches both KiCad and our emitted JSON).\n- Rotation applied via `transform=\"rotate(deg cx cy)\"` clockwise on screen.\n- Pad numbers stay un-rotated for readability.\n- F.Cu pads orange, B.Cu blue, thru-hole green, drill holes white on top.\n- Origin crosshair at (0, 0) with 1 mm arms.\n\n## Format quirks worth knowing\n\nThese bit us during reverse-engineering — they're encoded in the parser\nalready, but if you extend it or debug a weird chip, here's what's going\non.\n\n### 1. FAT overshoot\n\nINA226AIDGSR's PcbLib declares 256 FAT entries but only has 213 sectors.\nThe `cfb` crate refuses with `Malformed FAT … FAT has N entries, but\nfile has only M sectors`. Fix: pad the input bytes to 1 MiB with `0xFF`\n(CFBF \"free sector\" sentinel) before opening. The unreferenced trailing\nsectors don't hold real data.\n\n### 2. Per-record-type sub-block count\n\nAltium records aren't uniform `[type][len][payload]`. Different record\ntypes carry different numbers of `[u32 len][bytes]` sub-blocks:\n\n- Pad (0x02): 6 sub-blocks (designator, layer-name pstring, four metadata blocks).\n- Text (0x05): 2 sub-blocks (main + style trailer).\n- Track / Arc / Via / Fill / Region / ComponentBody: 1 sub-block.\n\nIf you hit an unknown type byte mid-stream, the parser stops walking\ngracefully — pads always come first, so by then we already have what we\nneed.\n\n### 3. Pad block-5 layout (the 194-byte v3 / 202-byte v4 pad data)\n\n```\n[0]      u8 layer (1 = TOP_CU, 32 = BOTTOM_CU, ...)\n[1]      u8 net/flag (0x08 in v3, 0x0c in v4 — version probe)\n[2]      u8 reserved\n[3..13]  10 bytes of 0xFF (component / net id sentinels)\n[13..17] i32 LE x position (units = 1 / 10000 mil = 2.54e-6 mm)\n[17..21] i32 LE y position (Altium Y-up — flipped to KiCad Y-down at parse)\n[21..29] u32 LE x size, y size — top layer\n[29..37] u32 LE x size, y size — mid layer (multi-layer thru-hole pads)\n[37..45] u32 LE x size, y size — bottom layer\n[45..49] u32 LE hole size (0 → SMD; > 0 → thru-hole)\n[49..52] u8 × 3 shape codes (1 round, 2 rect, 3 octagonal, 9 rounded-rect)\n[52..60] f64 LE rotation in degrees — ONLY in 202-byte v4 layout\n```\n\nPast offset 60 the layout drifts between Altium releases (paste / mask\noffsets, plated bool, GUIDs). The parser stops at 60.\n\n### 4. Two equally valid rotation conventions in the wild\n\nThis is the one that *looks* like a parser bug but isn't. Altium gives\nlibrary authors freedom in how to encode a rotated pad:\n\n- **Convention A (matches KiCad):** store the pad as a \"vertical stick\"\n  (xsize < ysize), set rotation = 90° on the side pads.\n- **Convention B (post-rotation):** store the physical post-rotation\n  dimensions (xsize > ysize for a horizontal pad), set rotation = 0.\n\nBoth render to the same physical pad on the PCB. A downstream tool that\ncompares `size[]` element-wise without applying rotation will see a 90°\nmismatch in convention B → KiCad. The fix lives in the consumer (compute\nthe post-rotation bbox before comparing), **not** in this parser. When in\ndoubt, render the authoritative SVG and compare visually.\n\n### 5. Y-axis flip\n\nAltium Y-up vs KiCad Y-down. The parser flips Y at parse time so emitted\n`at[1]` is in KiCad convention. **Do not flip again downstream.**\n\n### 6. Round shape with unequal w/h → \"oval\"\n\nKiCad's `circle` implies w == h. Altium's \"round\" (shape byte 1) covers\nboth true circles and stadium-shaped SMD pads with rounded ends. The\nparser specializes: shape 1 with `(xsize - ysize).abs() < 1e-3` →\n`circle`, otherwise → `oval` (KiCad's stadium).\n\n## Trouble-shooting checklist when downstream renders pads wrong\n\n1. **Run the authoritative SVG first.** `altium-pcblib-svg <path>.PcbLib > /tmp/auth.svg`. If pads are right in the SVG, the bug is downstream.\n2. **Check which variant got picked.** `parsed.module_name` plus `_alternates[*].module_name`. Manufacturing variants often have extra pads vs the IPC pinout variant; concur uses `parse_with_consensus_hint` to pick the right one.\n3. **Check rotation convention.** If pad 1 looks correct but pad 13 looks rotated 90° wrong, you're probably hitting Convention B vs A — your downstream is comparing or rendering size literally without applying rotation.\n4. **Check Y direction.** If pads are mirrored top-to-bottom, your downstream double-flipped Y. The parser already emits KiCad Y-down.\n5. **Scale.** SVG units are mm. If the rendered size is off by 1000×, the consumer is treating mm as µm.\n\n## Tested chip corpus\n\n| Chip | Package | Variants | Notes |\n|---|---|---|---|\n| LM358D | SOIC-8 | D8 / D8-L / D8-M | Sanity baseline. 8 horizontal pads, rot=0 throughout. |\n| INA226AIDGSR | VSSOP-10 | SOP50P490X110-10N | v3 layout (Convention B). Triggers FAT overshoot quirk. |\n| ADS1115IDGSR | VSSOP-10 | DGS0010A_L / _M / _N | 3 alternate density variants. |\n| DRV8316RRGFR | VQFN-40 | RGF0040E-IPC_A / _B / _C / -MFG | IPC variants are Convention A; MFG variant is Convention B + extra fiducials/test points. |\n",+  "author": {+    "id": "695820315b5f1e4db2fcf602",+    "name": "Kyle Bergstedt",+    "email": "[email protected]"+  },+  "visibility": {+    "public": true+  },+  "hero": null,+  "metadata": {},+  "created_at": "2026-05-28T05:29:36.057Z",+  "updated_at": "2026-05-28T05:29:36.057Z",+  "sub_skills": [],+  "parent_app": null+}\ No newline at end of file