← Commit history

board→library→components resolution; drop F.Fab from PCB; GLB with real component models; whole-project schematic render; publish-board skill

barrett-land ·91bf7746cb ·3mo ago ·parent fefd1ec
4 files changed +215−10
package.json+16−4
@@ -48,8 +48,20 @@   ],   "discovery_pitch": "Browse your wiki boards/projects, inspect each component's symbol, footprint, 3D model and per-EDA layer stackup, add parts to your KiCad/Altium libraries, and deliver self-contained KiCad projects — built on adom-lbr.",   "sample_prompts": [-    { "prompt": "Open the Adom Project Manager", "label": "Open manager" },-    { "prompt": "Add this board's components to my KiCad libraries", "label": "Add to library" },-    { "prompt": "Deliver this project as a self-contained KiCad project", "label": "Add project" }+    {+      "prompt": "Open the Adom Project Manager",+      "label": "Open manager"+    },+    {+      "prompt": "Add this board's components to my KiCad libraries",+      "label": "Add to library"+    },+    {+      "prompt": "Deliver this project as a self-contained KiCad project",+      "label": "Add project"+    }+  ],+  "confirmed_user_skills": [+    "publish-board"   ]-}+}\ No newline at end of file
skills/publish-board/SKILL.mdadded+93
@@ -0,0 +1,93 @@+---+name: publish-board+description: Publish or update a KiCad project to the Adom wiki so it renders in the Adom Project Manager — 3D board, hierarchical schematic, PCB — backed by a component library. Triggers include "upload my kicad project to the wiki", "publish my board", "push my kicad project so I can view it in the project manager", "update my board on the wiki".+user-invocable: true+---++# Publish a KiCad board to the wiki++Turn a local KiCad project into a wiki board page that renders in the **Adom+Project Manager** (rotatable 3D board with real components, hierarchical+schematic, KiCad-colored PCB), backed by a component **library**.++## Data model: board → library → components++- **Board page** — `type: component`, `category: board`, tags `["board", …]` (or+  `molecule`). Ships the render files: `.kicad_pro`, `.kicad_pcb`, EVERY+  `.kicad_sch` (root + sub-sheets), and a `<Board>.3dshapes/` of custom 3D+  models. Its single dependency is the library.+- **Library page** — `type: component`, tag `["library"]`, title+  `"<Board Name> Library"`, slug `<board-slug>-library`. `dependencies` = every+  component on the board. Ships the board's `.kicad_sym` + `.pretty`.+- **Component pages** — the existing per-component pages, matched by MPN → slug.++The Manager reads the board's one dependency (the library), then lists the+library's dependencies (the components). `board_deps` expands any dependency that+itself has dependencies, so this resolves automatically.++## Workflow++### 1. Pull the project (from the user's machine via `adom-desktop`)+Ask for / find the `.kicad_pro`. Pull the project folder (or at least):+`.kicad_pro`, `.kicad_pcb`, **every** `.kicad_sch`, `.kicad_sym`, `.pretty/`.+Use `adom-desktop shell_execute` (find the files) + `pull_file`.++### 2. Analyze it+Run the helper on the pulled folder:+```+python3 skills/publish-board/kicad_board_bundle.py <project-dir>+```+It reports the render files, `board_name`, the 3D model refs+(`models_std` vs `models_custom`), and the `components` (ref, value, mpn, slug).++### 3. Bundle the custom 3D models  → `<Board>.3dshapes/`+`models_std` (`${KICAD*_3DMODEL_DIR}/…`) resolve on the render service — leave+them. For each `models_custom` (absolute path), pull the `.step`/`.wrl` from the+user's machine into `<Board>.3dshapes/<basename>` (basename must match the PCB+ref — that's how the Manager's GLB render rewrites `${KIPRJMOD}/3d/<basename>`).++### 4. Map components → wiki slugs (MPN → slug)+Slug = MPN lowercased, non-alphanumeric → `-`. **The helper falls back to the+schematic `Value` when no MPN property exists** — verify the slugs actually match+real wiki component pages (`GET /api/v1/pages/<slug>`), because value-named+symbols (`R_4.3K_0402`) are NOT wiki component slugs. Resolve the real MPN per+part (BOM, the symbol's MPN/LCSC field, or ask the user). Warn + list any that+aren't on the wiki — they won't appear in the component list until published.++### 5. Library page — create or update, and VERIFY completeness+- Slug `<board-slug>-library`, title `"<Board Name> Library"`, `type: component`,+  tags `["library"]`, `dependencies` = the owner-qualified component slugs.+- **On every update, ensure every component on the board is present in the+  library's dependencies; add any missing ones.**+- Ship `.kicad_sym` + `.pretty`.+- Publish needs a hero (generate a simple 1200×630 billboard) + a 3D file for the+  component type (reuse any component `.step`): `adom-wiki pkg publish [--org <org>] --skip-lint`,+  then `repo push` the `package.json` + `.kicad_sym` + `.pretty`.++### 6. Board page — create or update+- `type: component`, `category: board`, tags `["board", …]`,+  `dependencies` = `{ "<owner>/<board-slug>-library": "^0.1.0" }`.+- Needs a hero + a 3D file (reuse a component model or a board render).+- `pkg publish` (this updates the wiki dependency GRAPH — `repo push` alone does+  NOT), then `repo push`: `.kicad_pro`, `.kicad_pcb`, all `.kicad_sch`, the+  `<Board>.3dshapes/`. Set visibility open.++### 7. Verify+Open the Manager; confirm the board appears (Personal/Organization by owner) and+its 3D + schematic + PCB render, and the component list matches the board.++## Gotchas (learned the hard way)++- **repo push ref**: the component namespace is a single shared namespace. Use the+  **bare slug** for `repo push <slug>`; `pkg publish` needs `--org <org>` if the+  slug is owned by an org (a `409 already exists … owned by <org>` tells you which).+- **Dependency graph vs file**: the Manager reads the `package.json` FILE (so+  `repo push` is enough for it), but the wiki's dependency-graph UI comes from+  `pkg publish` — re-publish (bump the version) to update the graph.+- **Hierarchical schematics**: upload the `.kicad_pro` + EVERY `.kicad_sch`. The+  Manager renders the whole project so the root's sheet-symbol boxes resolve; a+  single sheet alone renders wrong.+- **3D models**: only ABSOLUTE-path `(model …)` refs need bundling into+  `.3dshapes/`; `${KICAD*_3DMODEL_DIR}/…` are already on the render service.+- **Every page needs a hero**; components additionally need a 3D file — even the+  library. Generate/reuse one to pass the (hard-enforced) publish lint.
skills/publish-board/kicad_board_bundle.pyadded+75
@@ -0,0 +1,75 @@+#!/usr/bin/env python3+"""Analyze a KiCad project for publishing to the wiki (see publish-board SKILL).++Given a project directory (already pulled from the user's machine), report the+render files, the 3D-model refs (standard vs custom-absolute — the custom ones+must be bundled into <Board>.3dshapes/), and the component list with MPN → slug+so the library's dependencies can be filled.++usage: kicad_board_bundle.py <project-dir>+prints JSON: { pro, pcb, schematics[], sym, pretty[], board_name,+               models_std[], models_custom[], components[{ref,value,mpn,slug}] }+"""+import sys, os, re, json, glob+++def slugify(s):+    return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")+++def main(d):+    g = lambda pat: sorted(glob.glob(os.path.join(d, "**", pat), recursive=True))+    pro = (g("*.kicad_pro") or [None])[0]+    pcb = (g("*.kicad_pcb") or [None])[0]+    schs = g("*.kicad_sch")+    sym = (g("*.kicad_sym") or [None])[0]+    pretty = sorted(p for p in g("*.pretty") if os.path.isdir(p))+    board_name = os.path.splitext(os.path.basename(pro or pcb or "board"))[0]++    # 3D model refs from the PCB: ${KICAD*_3DMODEL_DIR}/... resolve on the render+    # service; absolute paths must be bundled into <Board>.3dshapes/ by basename.+    models_std, models_custom = set(), set()+    if pcb:+        txt = open(pcb, encoding="utf-8", errors="ignore").read()+        for m in re.findall(r'\(model\s+"([^"]+)"', txt):+            (models_std if m.startswith("${") else models_custom).add(m)++    # components: each schematic symbol instance's Reference + an MPN-like property+    mpn_keys = ("MPN", "Manufacturer Part Number", "Mfr Part #", "Mfr. Part #",+                "LCSC", "LCSC Part", "LCSC Part #", "Part Number", "Value")+    comps = {}+    for sch in schs:+        t = open(sch, encoding="utf-8", errors="ignore").read()+        for block in re.split(r"\n\s*\(symbol\s", t):+            ref_m = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', block)+            if not ref_m:+                continue+            ref = ref_m.group(1)+            if ref.startswith("#") or ref in comps:  # power symbols / dupes+                continue+            mpn = None+            for k in mpn_keys:+                mm = re.search(r'\(property\s+"' + re.escape(k) + r'"\s+"([^"]+)"', block)+                if mm and mm.group(1).strip() and mm.group(1) not in ("~", ""):+                    mpn = mm.group(1).strip()+                    break+            val_m = re.search(r'\(property\s+"Value"\s+"([^"]+)"', block)+            comps[ref] = {+                "ref": ref,+                "value": val_m.group(1) if val_m else "",+                "mpn": mpn,+                "slug": slugify(mpn) if mpn else None,+            }++    print(json.dumps({+        "pro": pro, "pcb": pcb, "schematics": schs, "sym": sym, "pretty": pretty,+        "board_name": board_name,+        "models_std": sorted(models_std), "models_custom": sorted(models_custom),+        "components": list(comps.values()),+    }, indent=2))+++if __name__ == "__main__":+    if len(sys.argv) < 2:+        sys.exit("usage: kicad_board_bundle.py <project-dir>")+    main(sys.argv[1])
src/main.rs+31−6
@@ -267,12 +267,12 @@ fn get_part(slug: &str) -> Value { /// npm-style manifest the wiki dependency graph is built from, so the two agree). /// Values may be `{slug: ver}`, `[slug,…]`, or `[{slug|ref|name}, …]`; owner /// prefixes (`adom/foo`) are stripped to the slug.-fn board_deps(slug: &str) -> Value {+/// A page's package.json dependencies as bare slugs (owner prefix stripped).+fn deps_of(slug: &str) -> Vec<String> {     let bare = |k: &str| k.rsplit('/').next().unwrap_or(k).trim().to_string();-    let mut deps: Vec<String> = vec![];     if let Some(txt) = wiki_text(slug, "package.json") {         if let Ok(v) = serde_json::from_str::<Value>(&txt) {-            deps = match v.get("dependencies") {+            return match v.get("dependencies") {                 Some(Value::Object(m)) => m.keys().map(|k| bare(k)).collect(),                 Some(Value::Array(a)) => a                     .iter()@@ -287,14 +287,39 @@ fn board_deps(slug: &str) -> Value {             };         }     }-    let items: Vec<Value> = deps.into_iter().map(|s| json!({"slug": s})).collect();+    vec![]+}++/// Components on a board. Architecture: a board depends on a component LIBRARY,+/// and the library depends on the components. Resolve one hop — any dependency+/// that itself has dependencies is a library and is expanded to its components;+/// a dependency with none is treated as a direct component (backward-compatible+/// with boards that still list components directly).+fn board_deps(slug: &str) -> Value {+    let mut components: Vec<String> = vec![];+    let mut seen = std::collections::HashSet::new();+    for d in deps_of(slug) {+        let sub = deps_of(&d);+        if sub.is_empty() {+            if seen.insert(d.clone()) {+                components.push(d);+            }+        } else {+            for c in sub {+                if seen.insert(c.clone()) {+                    components.push(c);+                }+            }+        }+    }+    let items: Vec<Value> = components.into_iter().map(|s| json!({"slug": s})).collect();     // the page's git commit history (the wiki's Activity tab)     let log = curl(&format!("{WIKI}/api/v1/pages/{slug}/log"))         .and_then(|b| serde_json::from_slice::<Value>(&b).ok())         .and_then(|v| v.get("log").cloned())         .unwrap_or_else(|| json!([]));     json!({ "status": "ok", "board": slug, "deps": items, "log": log,-        "hints": ["deps come from the board's package.json dependencies (same as the wiki dependency graph)"] })+        "hints": ["board → component library → components (the library's dependencies)"] }) }  /// List the file names on a page (robust to any MPN→filename convention — the@@ -899,7 +924,7 @@ fn render_board_media(slug: &str) -> BoardMedia {     // draw order = z-order (last on top): bottom/inner copper first (dimmed in     // the UI), then F.Cu on top (kept opaque = the solid top layer), then the     // silk/edge/fab docs. No F.Mask (its magenta would paint the pads purple).-    let pcb_svg = pcb_bytes.as_ref().and_then(|b| kicad_export(slug, "pcb", "svg", "?layers=B.Cu,In2.Cu,In1.Cu,F.Cu,F.Silkscreen,Edge.Cuts,F.Fab", b)).and_then(|b| String::from_utf8(b).ok());+    let pcb_svg = pcb_bytes.as_ref().and_then(|b| kicad_export(slug, "pcb", "svg", "?layers=B.Cu,In2.Cu,In1.Cu,F.Cu,F.Silkscreen,Edge.Cuts", b)).and_then(|b| String::from_utf8(b).ok());     // GLB with the real component 3D models (bundle the custom .3dshapes models)     let glb = render_board_glb(slug);     (sheets, pcb_svg, glb)