feat(libs): eeschema symbol-chooser footprint selector + preview via publish-time fp-index

The merged kicad_editor bundle made the chooser's footprint side reachable
from a schematic session; this wires up the data:

- boot.ts/constants.ts: every kicad_editor frame seeds BOTH sym-lib-table and
  fp-lib-table (+ placeholder files; a created user lib joins both lists) —
  the eeschema frame used to write fp-lib-table empty, leaving the selector
  dead. TOOL_LIB_KIND remains only the presync/primary-kind lever.
- publish-libs.ts + kicad-pretty.ts: publish fp-index.json per tag —
  [name, uniquePadCount] per footprint (countUniquePads mirrors KiCad's
  GetUniquePadCount(DO_NOT_INCLUDE_NPTH)); index-only top-up mode for
  already-published immutable tags.
- source.ts/cdn-source.ts: new bridge op "index" (source-global, dispatched
  before the lib-id parse) + LibsSource.getFpIndex; the CDN source fetches
  <tag>/fp-index.json once (404 ⇒ null ⇒ C++ default-only fallback).
- dev-demo.mjs: --libs-local serves a local publish-libs layout same-origin
  at /libs-cdn (mirrors --models-local).
- tests/web/eeschema-fp-selector.spec.ts: e2e — chooser opens in --frame=sch,
  selector fills from ONE index crossing, clicking a row per-item-gets the
  body and the cross-face GAL preview renders; adaptive for index-less
  sources (asserts crash-free default-only selector).

Submodule bumps: kicad (index-backed filterFootprints + preview AsyncLoad
fix + modal-pump crash guard), wxwidgets (modal pump logs e.stack).

Doc: pcbjam-private docs/features/libs/0014-eeschema-footprint-selector.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCWVaCRM9T847PdPwYYajX
This commit is contained in:
Gergő Törcsvári 2026-07-06 13:39:21 +02:00
commit 6fc6a64cd0
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
13 changed files with 492 additions and 31 deletions

View file

@ -63,6 +63,48 @@ function findTopFootprint(src: string): [number, number] {
* the footprint's direct children (depth 1) so a `descr`/`tags`/`model` word
* inside a quoted string can't be mistaken for a real token.
*/
/**
* Unique electrical pad count, mirroring the WASM fork's
* `FOOTPRINT::GetUniquePadCount( DO_NOT_INCLUDE_NPTH )` (pcbnew/footprint.cpp,
* `GetUniquePadNumbers`): count DISTINCT pad numbers, skipping pads that are
* not on any copper layer, pads with an empty number ("mechanical" pads), and
* NPTH pads. The symbol chooser's footprint selector filters on exactly this
* value (`filterFootprints`, pcbnew.cpp), so the published index must agree
* with what the editor would compute from the parsed footprint.
*/
export function countUniquePads(src: string): number {
const [s, e] = findTopFootprint(src);
const block = src.slice(s, e);
const numbers = new Set<string>();
let i = block.indexOf("(", 1); // first child form
while (i >= 0 && i < block.length) {
const [cs, ce] = matchParen(block, i);
const form = block.slice(cs, ce);
// (pad "<number>" <type> <shape> …) — number quoted (modern) or bare (old).
const m = form.match(
/^\(\s*pad\s+(?:"((?:[^"\\]|\\.)*)"|([^\s()"]+))\s+([a-z_]+)/,
);
if (m) {
const number = m[1] !== undefined ? unescape(m[1]) : m[2]!;
const type = m[3]!;
// The pad's own (layers …) precedes any nested primitives, so the first
// match is the right one. Copper = any token ending in ".Cu" ("F.Cu",
// "B.Cu", "*.Cu", "F&B.Cu"). No layers form ⇒ count it (be permissive).
const layers = form.match(/\(\s*layers\s+([^)]*)\)/);
const onCopper = !layers || /\.Cu\b/.test(layers[1]!);
if (number !== "" && type !== "np_thru_hole" && onCopper) {
numbers.add(number);
}
}
i = block.indexOf("(", ce); // next sibling
}
return numbers.size;
}
export function parseFootprintFile(src: string, name: string): ParsedFootprint {
const [s, e] = findTopFootprint(src);
const block = src.slice(s, e);