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:
parent
8e99f5c5ac
commit
6fc6a64cd0
13 changed files with 492 additions and 31 deletions
|
|
@ -74,6 +74,7 @@ function parseArgs(argv) {
|
|||
noGallery: false, // disable the example gallery (local-folder + IDB only)
|
||||
modelsTag: null, // 3D models snapshot tag (live CDN, or the local dir's tag)
|
||||
modelsLocal: null, // local publish-models --driver local output dir (serve same-origin)
|
||||
libsLocal: null, // local publish-libs --driver local output dir (serve same-origin)
|
||||
port: null,
|
||||
repo: "https://github.com/emergence-engineering/pcbjam",
|
||||
};
|
||||
|
|
@ -91,6 +92,7 @@ function parseArgs(argv) {
|
|||
case "--repo": a.repo = next(); break;
|
||||
case "--models-tag": a.modelsTag = next(); break;
|
||||
case "--models-local": a.modelsLocal = next(); break;
|
||||
case "--libs-local": a.libsLocal = next(); break;
|
||||
case "-h": case "--help": a.help = true; break;
|
||||
default: throw new Error(`unknown arg: ${argv[i]}`);
|
||||
}
|
||||
|
|
@ -115,6 +117,8 @@ const HELP = `dev-demo.mjs — run the standalone locally in demo mode (R2-only
|
|||
--models-tag <tag> enable lazy 3D models from the CDN snapshot at this tag
|
||||
--models-local <dir> serve a local publish-models layout (--driver local --compress none)
|
||||
same-origin instead of the CDN (requires --models-tag)
|
||||
--libs-local <dir> serve a local publish-libs layout (--driver local) same-origin
|
||||
instead of the CDN (uses --lib-tag as the snapshot tag)
|
||||
--port <n> dev server port
|
||||
|
||||
By default the read-only example gallery (deploy/demo/gallery.json) is built
|
||||
|
|
@ -141,7 +145,20 @@ function main() {
|
|||
const env = { ...process.env };
|
||||
|
||||
// --- Libraries: live R2 CDN (the lazy/fat lib-load path), or offline examples.
|
||||
if (a.libTag) {
|
||||
// --libs-local <publish-libs --out dir> serves that layout same-origin at
|
||||
// /libs-cdn via a public/ symlink (mirrors --models-local) — for testing
|
||||
// an unpublished snapshot, e.g. one with a fresh fp-index.json.
|
||||
if (a.libTag && a.libsLocal) {
|
||||
const link = join(repoRoot, "web/standalone/public/libs-cdn");
|
||||
try {
|
||||
if (lstatSync(link)) rmSync(link, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* no existing link */
|
||||
}
|
||||
symlinkSync(resolve(a.libsLocal, "libs/kicad"), link);
|
||||
env.VITE_LIBS_SOURCE = "cdn";
|
||||
env.VITE_LIBS_MANIFEST_URL = `/libs-cdn/${a.libTag}/manifest.json`;
|
||||
} else if (a.libTag) {
|
||||
env.VITE_LIBS_SOURCE = "cdn";
|
||||
env.VITE_LIBS_MANIFEST_URL = `${a.cdn}/libs/kicad/${a.libTag}/manifest.json`;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -17,12 +17,19 @@
|
|||
// manifest SyncManifest { version, entries: { "<kind>/<name>": {hash,size,mtime} } }
|
||||
// bundle encodeBundle(manifest, bodies) — cold-init payload (all bodies)
|
||||
// + top `<prefix>/<libTag>/manifest.json` { schema, tag, libs:[{id,name,kind,itemCount}] }
|
||||
// + `<prefix>/<libTag>/fp-index.json` { schema, tag, libs: { <libId>: [[name, pads], …] } }
|
||||
// — the publish-time footprint index: unique electrical pad count per footprint,
|
||||
// so the editor's symbol-chooser footprint selector can filter EVERY footprint
|
||||
// lib without fat-loading a single body (kicad pcbnew.cpp `filterFootprints`).
|
||||
// All immutable (content is pinned by the tag).
|
||||
// A tag published before fp-index.json existed gets an INDEX-ONLY top-up run:
|
||||
// bundles/manifests are skipped (immutable + present), only the index is put.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { extractAllLibs } from "../../web/backend/src/extract/extract-libs.js";
|
||||
import { countUniquePads } from "../../web/backend/src/extract/kicad-pretty.js";
|
||||
import { encodeBundle, type SyncManifest } from "../../web/pcbjam-shared/src/sync-wire.js";
|
||||
import { IMMUTABLE, makeStore, putJSON, sha256hex } from "./lib/cdn-store.mjs";
|
||||
|
||||
|
|
@ -99,14 +106,21 @@ async function main(): Promise<void> {
|
|||
const store = makeStore(a.driver, a);
|
||||
const enc = new TextEncoder();
|
||||
const topKey = `${a.prefix}/${a.libTag}/manifest.json`;
|
||||
const indexKey = `${a.prefix}/${a.libTag}/fp-index.json`;
|
||||
|
||||
// Skip-if-exists: the snapshot is immutable + content-pinned by the tag.
|
||||
if (!a.force && store.getJSON(topKey)) {
|
||||
// A tag published before fp-index.json existed drops into INDEX-ONLY mode:
|
||||
// recompute + put just the footprint index (pure local work; no bundle puts).
|
||||
const indexOnly = !a.force && !!store.getJSON(topKey);
|
||||
if (indexOnly && store.getJSON(indexKey)) {
|
||||
console.log(`publish-libs: ${topKey} already published — skipping (use --force)`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`publish-libs: tag=${a.libTag} driver=${store.kind} → ${a.prefix}/${a.libTag}/`);
|
||||
console.log(
|
||||
`publish-libs: tag=${a.libTag} driver=${store.kind} → ${a.prefix}/${a.libTag}/` +
|
||||
(indexOnly ? " (index-only top-up)" : ""),
|
||||
);
|
||||
|
||||
// Source the full set from upstream when asked (CI path); --symbols-src /
|
||||
// --footprints-src still win if also given (local checkouts).
|
||||
|
|
@ -120,13 +134,20 @@ async function main(): Promise<void> {
|
|||
}
|
||||
|
||||
const libs = await extractAllLibs({
|
||||
symbolsSrc: a.symbolsSrc ?? undefined,
|
||||
// Index-only: the index covers footprints only, so skip symbol extraction.
|
||||
symbolsSrc: indexOnly ? undefined : (a.symbolsSrc ?? undefined),
|
||||
footprintsSrc: a.footprintsSrc ?? undefined,
|
||||
});
|
||||
|
||||
const topLibs: Array<{ id: string; name: string; kind: string; itemCount: number }> = [];
|
||||
const fpIndexLibs: Record<string, Array<[string, number]>> = {};
|
||||
let totalItems = 0;
|
||||
for (const { lib, kind, items } of libs) {
|
||||
if (kind === "footprint") {
|
||||
fpIndexLibs[lib] = items.map((it) => [it.name, countUniquePads(it.body)]);
|
||||
}
|
||||
if (indexOnly) continue; // bundles/manifests already live under this tag
|
||||
|
||||
const bodies = items.map(
|
||||
(it): [string, Uint8Array] => [`${it.kind}/${it.name}`, enc.encode(it.body)],
|
||||
);
|
||||
|
|
@ -146,11 +167,18 @@ async function main(): Promise<void> {
|
|||
totalItems += items.length;
|
||||
}
|
||||
|
||||
topLibs.sort((x, y) => x.id.localeCompare(y.id));
|
||||
putJSON(store, topKey, { schema: 1, tag: a.libTag, libs: topLibs }, IMMUTABLE);
|
||||
if (!indexOnly) {
|
||||
topLibs.sort((x, y) => x.id.localeCompare(y.id));
|
||||
putJSON(store, topKey, { schema: 1, tag: a.libTag, libs: topLibs }, IMMUTABLE);
|
||||
}
|
||||
putJSON(store, indexKey, { schema: 1, tag: a.libTag, libs: fpIndexLibs }, IMMUTABLE);
|
||||
|
||||
const fpIndexCount = Object.values(fpIndexLibs).reduce((n, v) => n + v.length, 0);
|
||||
console.log(
|
||||
`publish-libs: done — ${topLibs.length} libs, ${totalItems} items → ${topKey}`,
|
||||
indexOnly
|
||||
? `publish-libs: done — fp-index only (${fpIndexCount} footprints) → ${indexKey}`
|
||||
: `publish-libs: done — ${topLibs.length} libs, ${totalItems} items ` +
|
||||
`(+fp-index: ${fpIndexCount} footprints) → ${topKey}`,
|
||||
);
|
||||
if (store.kind === "local") console.log(`local layout under: ${a.out}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue