feat: libs 0009-S — footprint write-path spike DONE + verified (footprint editor as project-scoped WASM tool + PCB_IO_PCBJAM_FP plugin + bridge kind 4th-arg; New Footprint→FootprintSave fires through bridge on main thread, fork-native body v20251028, no wedge — Firefox e2e green); bump kicad + pcbjam-shared
This commit is contained in:
parent
f04915ee1d
commit
19fce866c1
12 changed files with 385 additions and 87 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit df297291b0bdfd0228c900e9c1a0c7279ffd1533
|
||||
Subproject commit 3cb0a2ff3f746e52d48d389c19154ffa076cc288
|
||||
|
|
@ -11,7 +11,10 @@ export const WASM_ASSET_BASE_URL =
|
|||
import type { ProviderConfig, ProviderKind } from "@/wasm/collab";
|
||||
import { remoteLibsSource } from "@/wasm/libs/remote-source";
|
||||
import type { LibsSource } from "@/wasm/libs/source";
|
||||
import { withSpikeWritableLib } from "@/wasm/libs/spike-writable";
|
||||
import {
|
||||
withSpikeWritableFpLib,
|
||||
withSpikeWritableLib,
|
||||
} from "@/wasm/libs/spike-writable";
|
||||
import { staticLibsSource } from "@/wasm/libs/static-source";
|
||||
|
||||
/**
|
||||
|
|
@ -78,14 +81,18 @@ export function libsSourceConfig(projectId?: string): LibsSource | null {
|
|||
? staticLibsSource()
|
||||
: remoteLibsSource(API_BASE_URL, libsOwner(), projectId);
|
||||
|
||||
// 0004-A spike: `?libwrite=1` adds one in-memory writable user lib so the
|
||||
// 0004-A spike: `?libwrite=1` adds one in-memory writable user SYMBOL lib so the
|
||||
// editor save path works with no backend (a dev/test aid). The real remote
|
||||
// write path (0004-C) needs no flag — boot ensures a user lib via createLib.
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
new URLSearchParams(window.location.search).get("libwrite") === "1"
|
||||
) {
|
||||
return withSpikeWritableLib(base, (m) => console.log(m));
|
||||
// 0009-S spike: `?fpwrite=1` does the same for a writable FOOTPRINT lib.
|
||||
if (typeof window !== "undefined") {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get("fpwrite") === "1") {
|
||||
return withSpikeWritableFpLib(base, (m) => console.log(m));
|
||||
}
|
||||
if (params.get("libwrite") === "1") {
|
||||
return withSpikeWritableLib(base, (m) => console.log(m));
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,15 @@ import {
|
|||
KICAD_CONFIG_DIR,
|
||||
RESOURCE_PATH,
|
||||
TOOL_ARGV0,
|
||||
TOOL_LIB_KIND,
|
||||
TOOL_NEEDS_CONFIG_SEED,
|
||||
} from "./constants";
|
||||
import { buildSymLibTable, installLibsProvider, type LibsSource } from "./libs/source";
|
||||
import {
|
||||
buildFpLibTable,
|
||||
buildSymLibTable,
|
||||
installLibsProvider,
|
||||
type LibsSource,
|
||||
} from "./libs/source";
|
||||
import { libUri, PCBJAM_LIB_MOUNT } from "./libs/uri";
|
||||
|
||||
/** The default user lib boot ensures exists, so there's a writable save target. */
|
||||
|
|
@ -101,16 +107,20 @@ async function doBoot(opts: BootOptions): Promise<void> {
|
|||
// the wasm boots (the table is seeded in preRun below). No source → empty
|
||||
// table, libs disabled.
|
||||
let symLibTable = "(sym_lib_table\n (version 7)\n)\n";
|
||||
let fpLibTable = "(fp_lib_table\n (version 7)\n)\n";
|
||||
// Every lib gets an empty placeholder FILE at its URI (not just the mount dir):
|
||||
// the symbol-editor save path stat()s the lib file after a successful save
|
||||
// (SetSymModificationTime -> wxFileName::GetModificationTime), which errors on
|
||||
// a non-existent path. The bytes are virtual (served via window.kicadLibs);
|
||||
// this file only satisfies incidental fs checks.
|
||||
// the editor save path stat()s the lib file after a successful save
|
||||
// (symbol: SetSymModificationTime; footprint: setFPWatcher -> GetModificationTime),
|
||||
// which errors on a non-existent path. The bytes are virtual (served via
|
||||
// window.kicadLibs); this file only satisfies incidental fs checks.
|
||||
let libPlaceholderUris: string[] = [];
|
||||
if (libsSource) {
|
||||
// Which lib table this tool consumes: symbol → sym-lib-table, footprint →
|
||||
// fp-lib-table. The same lib source feeds whichever table the tool reads.
|
||||
const libKind = TOOL_LIB_KIND[tool];
|
||||
if (libsSource && libKind) {
|
||||
installLibsProvider(libsSource, log);
|
||||
try {
|
||||
// Ensure the owner has at least one writable user lib to create symbols in.
|
||||
// Ensure the owner has at least one writable user lib to save items into.
|
||||
let libsList = await libsSource.listLibs();
|
||||
if (libsSource.createLib && !libsList.some((l) => l.type === "user")) {
|
||||
const created = await libsSource.createLib(DEFAULT_USER_LIB_NAME);
|
||||
|
|
@ -119,9 +129,14 @@ async function doBoot(opts: BootOptions): Promise<void> {
|
|||
log(`[libs] created default user lib "${created.name}"`);
|
||||
}
|
||||
}
|
||||
symLibTable = buildSymLibTable(libsList);
|
||||
if (libKind === "footprint") {
|
||||
fpLibTable = buildFpLibTable(libsList);
|
||||
log(`[libs] seeded ${libsList.length} lib(s) into fp-lib-table`);
|
||||
} else {
|
||||
symLibTable = buildSymLibTable(libsList);
|
||||
log(`[libs] seeded ${libsList.length} lib(s) into sym-lib-table`);
|
||||
}
|
||||
libPlaceholderUris = libsList.map((l) => libUri(l.id));
|
||||
log(`[libs] seeded ${libsList.length} lib(s) into sym-lib-table`);
|
||||
} catch (e) {
|
||||
log(`[libs] listLibs failed, seeding empty table: ${String(e)}`);
|
||||
}
|
||||
|
|
@ -222,13 +237,10 @@ async function doBoot(opts: BootOptions): Promise<void> {
|
|||
2,
|
||||
),
|
||||
);
|
||||
// libs: rows generated in doBoot from the lib source; the PCBJAM plugin
|
||||
// resolves each via window.kicadLibs.
|
||||
// libs: rows generated in doBoot from the lib source; the PCBJAM / PCBJAM_FP
|
||||
// plugins resolve each via window.kicadLibs.
|
||||
writeIfAbsent(`${KICAD_CONFIG_DIR}/sym-lib-table`, symLibTable);
|
||||
writeIfAbsent(
|
||||
`${KICAD_CONFIG_DIR}/fp-lib-table`,
|
||||
"(fp_lib_table\n (version 7)\n)\n",
|
||||
);
|
||||
writeIfAbsent(`${KICAD_CONFIG_DIR}/fp-lib-table`, fpLibTable);
|
||||
writeIfAbsent(
|
||||
`${KICAD_CONFIG_DIR}/design-block-lib-table`,
|
||||
"(design_block_lib_table\n (version 7)\n)\n",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const TOOL_ARGV0: Record<Tool, string> = {
|
|||
calculator: "/usr/bin/pcb_calculator",
|
||||
pl_editor: "/usr/bin/pl_editor",
|
||||
symbol_editor: "/usr/bin/symbol_editor",
|
||||
footprint_editor: "/usr/bin/footprint_editor",
|
||||
gerbview: "/usr/bin/gerbview",
|
||||
};
|
||||
|
||||
|
|
@ -44,9 +45,26 @@ export const TOOL_NEEDS_CONFIG_SEED: Record<Tool, boolean> = {
|
|||
calculator: true,
|
||||
pl_editor: true,
|
||||
symbol_editor: true,
|
||||
footprint_editor: true,
|
||||
gerbview: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Which library kind a tool consumes — drives which lib-table boot populates
|
||||
* from the lib source (symbol → sym-lib-table; footprint → fp-lib-table). A
|
||||
* user lib is a kind-agnostic container, so the same lib id can land in both
|
||||
* tables depending on the tool. `null` = the tool uses no libraries.
|
||||
*/
|
||||
export const TOOL_LIB_KIND: Record<Tool, "symbol" | "footprint" | null> = {
|
||||
pcbnew: "footprint",
|
||||
eeschema: "symbol",
|
||||
calculator: null,
|
||||
pl_editor: null,
|
||||
symbol_editor: "symbol",
|
||||
footprint_editor: "footprint",
|
||||
gerbview: null,
|
||||
};
|
||||
|
||||
/** KiCad user settings dir for this build (PATHS::GetUserSettingsPath()). */
|
||||
export const KICAD_CONFIG_DIR = `/home/kicad/.config/kicad/kicad/${KICAD_VERSION_DIR}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,11 +48,18 @@ export interface LibsSource {
|
|||
createLib?(name: string): Promise<LibInfo | null>;
|
||||
}
|
||||
|
||||
/** The function the WASM `SCH_IO_PCBJAM_LIB` plugin calls via the JS bridge. */
|
||||
/**
|
||||
* The function the WASM lib plugins call via the JS bridge. Both the symbol
|
||||
* plugin (`SCH_IO_PCBJAM_LIB`) and the footprint plugin (`PCB_IO_PCBJAM_FP`)
|
||||
* call the same hook; `kind` (4th arg) discriminates the item kind. The symbol
|
||||
* plugin omits it (passes 3 args) so it defaults to "symbol" — keeping the
|
||||
* existing eeschema binary correct with no rebuild.
|
||||
*/
|
||||
export type KicadLibsRequest = (
|
||||
op: string,
|
||||
lib: string,
|
||||
arg: string,
|
||||
kind?: string,
|
||||
) => Promise<string | null>;
|
||||
|
||||
declare global {
|
||||
|
|
@ -89,10 +96,32 @@ export function buildSymLibTable(libsList: LibInfo[]): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Install `window.kicadLibs` backed by a `LibsSource`. The plugin calls
|
||||
* `request(op, "/mnt/pcbjam[-rw]/<id>", arg)`:
|
||||
* "list" -> JSON {"symbols":[...]} (symbol names in the lib)
|
||||
* "get" -> the item body s-expr (arg = symbol name; null if absent)
|
||||
* Build fp-lib-table content (KiCad v7) with one PCBJAM_FP row per lib. The
|
||||
* footprint editor selects the plugin from this row's `type` field (via
|
||||
* PCB_IO_MGR::EnumFromStr), so it MUST be "PCBJAM_FP" to match the registered
|
||||
* plugin name. Same /mnt/pcbjam/<id> URI as symbols (the same lib id can appear
|
||||
* in both tables — user libs are kind-agnostic containers).
|
||||
*/
|
||||
export function buildFpLibTable(libsList: LibInfo[]): string {
|
||||
const rows = libsList.map((l) => {
|
||||
const descr = l.description ? sexprEscape(l.description) : "";
|
||||
return ` (lib (name "${sexprEscape(
|
||||
l.name,
|
||||
)}")(type "PCBJAM_FP")(uri "${libUri(
|
||||
l.id,
|
||||
)}")(options "")(descr "${descr}"))`;
|
||||
});
|
||||
return `(fp_lib_table\n (version 7)\n${rows.join("\n")}${
|
||||
rows.length ? "\n" : ""
|
||||
})\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install `window.kicadLibs` backed by a `LibsSource`. Both lib plugins call
|
||||
* `request(op, "/mnt/pcbjam/<id>", arg, kind)` (kind defaults to "symbol" so the
|
||||
* symbol plugin's 3-arg calls still work):
|
||||
* "list" -> JSON {"symbols":[...]} | {"footprints":[...]} (names of that kind)
|
||||
* "get" -> the item body s-expr (arg = item name; null if absent)
|
||||
* "save" -> "ok" / null (arg = JSON {"name":..,"body":..})
|
||||
*/
|
||||
export function installLibsProvider(
|
||||
|
|
@ -102,9 +131,9 @@ export function installLibsProvider(
|
|||
if (window.kicadLibs) return;
|
||||
const delay = artificialDelayMs();
|
||||
|
||||
const request: KicadLibsRequest = async (op, lib, arg) => {
|
||||
const request: KicadLibsRequest = async (op, lib, arg, kind = "symbol") => {
|
||||
const id = libIdFromUri(lib);
|
||||
log(`[libs] request op=${op} lib=${lib} (id=${id}) arg=${arg}`);
|
||||
log(`[libs] request op=${op} kind=${kind} lib=${lib} (id=${id}) arg=${arg}`);
|
||||
if (!id) return null;
|
||||
if (delay) await sleep(delay);
|
||||
|
||||
|
|
@ -112,13 +141,15 @@ export function installLibsProvider(
|
|||
switch (op) {
|
||||
case "list": {
|
||||
const items = await source.listItems(id);
|
||||
const symbols = items
|
||||
.filter((i) => i.kind === "symbol")
|
||||
const names = items
|
||||
.filter((i) => i.kind === kind)
|
||||
.map((i) => i.name);
|
||||
return JSON.stringify({ symbols });
|
||||
// Each plugin parses its own key: footprints / symbols.
|
||||
const key = kind === "footprint" ? "footprints" : "symbols";
|
||||
return JSON.stringify({ [key]: names });
|
||||
}
|
||||
case "get":
|
||||
return await source.getItemBody(id, "symbol", arg);
|
||||
return await source.getItemBody(id, kind, arg);
|
||||
case "save": {
|
||||
let parsed: { name?: string; body?: string };
|
||||
try {
|
||||
|
|
@ -134,7 +165,7 @@ export function installLibsProvider(
|
|||
}
|
||||
const ok = await source.saveItemBody(
|
||||
id,
|
||||
"symbol",
|
||||
kind,
|
||||
parsed.name,
|
||||
parsed.body,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,39 @@
|
|||
import type { LibInfo, LibItemInfo, LibsSource } from "./source";
|
||||
|
||||
/**
|
||||
* 0004-A write-path spike (no backend). Wraps a `LibsSource` with ONE in-memory
|
||||
* writable user lib so the editor's save-symbol flow has a target and we can
|
||||
* exercise the full save → enumerate → load round-trip through the same WASM
|
||||
* bridge — without committing to the backend design first. Gated by
|
||||
* `?libwrite=1`. Saved bodies are mirrored onto `window.__pcbjamSaved` so the
|
||||
* Playwright probe can inspect them. Replaced in 0004-C by real remote writes.
|
||||
* Write-path spike (no backend). Wraps a `LibsSource` with ONE in-memory writable
|
||||
* user lib so the editor's save flow has a target and we can exercise the full
|
||||
* save → enumerate → load round-trip through the same WASM bridge — without
|
||||
* committing to the backend design first.
|
||||
*
|
||||
* - Symbols (0004-A): `?libwrite=1` → `withSpikeWritableLib`.
|
||||
* - Footprints (0009-S): `?fpwrite=1` → `withSpikeWritableFpLib`.
|
||||
*
|
||||
* Saved bodies are mirrored onto `window.__pcbjamSaved` so the Playwright probe
|
||||
* can inspect them. Replaced by real remote writes once the backend lands.
|
||||
*/
|
||||
const SPIKE_RW_LIB_ID = "spike-user";
|
||||
const SPIKE_RW_LIB_NAME = "My Symbols (spike)";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__pcbjamSaved?: Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
export function withSpikeWritableLib(
|
||||
interface SpikeLibSpec {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: "symbol" | "footprint";
|
||||
}
|
||||
|
||||
function withSpikeKindLib(
|
||||
inner: LibsSource | null,
|
||||
log: (msg: string) => void,
|
||||
spec: SpikeLibSpec,
|
||||
): LibsSource {
|
||||
const store = new Map<string, string>(); // symbol name -> body
|
||||
window.__pcbjamSaved = Object.create(null) as Record<string, string>;
|
||||
const store = new Map<string, string>(); // item name -> body
|
||||
// Don't clobber an existing capture map (so symbol + footprint spikes coexist).
|
||||
window.__pcbjamSaved ??= Object.create(null) as Record<string, string>;
|
||||
|
||||
const isSpike = (libId: string) => libId === SPIKE_RW_LIB_ID;
|
||||
const isSpike = (libId: string) => libId === spec.id;
|
||||
|
||||
return {
|
||||
async listLibs(): Promise<LibInfo[]> {
|
||||
|
|
@ -37,15 +46,12 @@ export function withSpikeWritableLib(
|
|||
} catch (e) {
|
||||
log(`[libs] spike: inner listLibs failed, origins omitted: ${String(e)}`);
|
||||
}
|
||||
return [
|
||||
...base,
|
||||
{ id: SPIKE_RW_LIB_ID, name: SPIKE_RW_LIB_NAME, type: "user" },
|
||||
];
|
||||
return [...base, { id: spec.id, name: spec.name, type: "user" }];
|
||||
},
|
||||
|
||||
async listItems(libId: string): Promise<LibItemInfo[]> {
|
||||
if (isSpike(libId))
|
||||
return [...store.keys()].map((name) => ({ kind: "symbol", name }));
|
||||
return [...store.keys()].map((name) => ({ kind: spec.kind, name }));
|
||||
return inner ? inner.listItems(libId) : [];
|
||||
},
|
||||
|
||||
|
|
@ -71,8 +77,32 @@ export function withSpikeWritableLib(
|
|||
}
|
||||
store.set(name, body);
|
||||
window.__pcbjamSaved![name] = body;
|
||||
log(`[libs] spike saved symbol "${name}" (${body.length} bytes)`);
|
||||
log(`[libs] spike saved ${spec.kind} "${name}" (${body.length} bytes)`);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 0004-A symbol write spike (`?libwrite=1`). */
|
||||
export function withSpikeWritableLib(
|
||||
inner: LibsSource | null,
|
||||
log: (msg: string) => void,
|
||||
): LibsSource {
|
||||
return withSpikeKindLib(inner, log, {
|
||||
id: "spike-user",
|
||||
name: "My Symbols (spike)",
|
||||
kind: "symbol",
|
||||
});
|
||||
}
|
||||
|
||||
/** 0009-S footprint write spike (`?fpwrite=1`). */
|
||||
export function withSpikeWritableFpLib(
|
||||
inner: LibsSource | null,
|
||||
log: (msg: string) => void,
|
||||
): LibsSource {
|
||||
return withSpikeKindLib(inner, log, {
|
||||
id: "spike-fp-user",
|
||||
name: "My Footprints (spike)",
|
||||
kind: "footprint",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue