From d2d2c19a8c5e2d0ea38c971da569a1e03b6a67a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Fri, 12 Jun 2026 19:15:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20libs=200003=20=E2=80=94=20remote=20prov?= =?UTF-8?q?ider=20+=20example=20backend=20serving;=20bump=20kicad,=20share?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit standalone: LibsSource abstraction (remote over the contract + static fallback), generic window.kicadLibs provider, sym-lib-table generated from the backend's lib list at boot (replaces the spike). backend: serve pre-built self-contained symbol bodies from LIBS_DIR (listLibs/listLibItems + raw item-body route). Co-Authored-By: Claude Fable 5 --- kicad | 2 +- web/backend/src/libs.ts | 107 +++++++++++++++ web/backend/src/server.ts | 33 +++++ web/pcbjam-shared | 2 +- web/standalone/src/components/WasmTool.tsx | 8 +- web/standalone/src/lib/config.ts | 18 +++ web/standalone/src/wasm/boot.ts | 38 ++++-- web/standalone/src/wasm/libs/remote-source.ts | 44 ++++++ web/standalone/src/wasm/libs/source.ts | 114 ++++++++++++++++ .../src/wasm/libs/spike-provider.ts | 128 ------------------ web/standalone/src/wasm/libs/static-source.ts | 78 +++++++++++ web/standalone/src/wasm/libs/uri.ts | 20 +++ 12 files changed, 447 insertions(+), 145 deletions(-) create mode 100644 web/backend/src/libs.ts create mode 100644 web/standalone/src/wasm/libs/remote-source.ts create mode 100644 web/standalone/src/wasm/libs/source.ts delete mode 100644 web/standalone/src/wasm/libs/spike-provider.ts create mode 100644 web/standalone/src/wasm/libs/static-source.ts create mode 100644 web/standalone/src/wasm/libs/uri.ts diff --git a/kicad b/kicad index 51743e6..7932b3a 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 51743e6c7981d4349adaabc9416eb35469a8c0d6 +Subproject commit 7932b3ace1e5e21fb6ab38ece2dc1faa41a25a12 diff --git a/web/backend/src/libs.ts b/web/backend/src/libs.ts new file mode 100644 index 0000000..63d4c32 --- /dev/null +++ b/web/backend/src/libs.ts @@ -0,0 +1,107 @@ +// Minimal reference libraries serving for the @pcbjam/shared libs contract. +// +// Serves PRE-BUILT self-contained symbol bodies from a directory (LIBS_DIR), +// laid out as: +// +// //index.json { items: [{kind,name,description,keywords}], ... } +// //.kicad_sym a complete kicad_symbol_lib s-expr +// //LICENSE.md attribution (CC-BY-SA travels with data) +// +// This backend does NO parsing — bodies are produced upstream (the closed +// ingestion/extractor) so the open reference server stays trivial. If LIBS_DIR +// is unset or empty, the lib endpoints simply report no libraries. + +import { createReadStream } from "node:fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import type { Lib, LibItem } from "@pcbjam/shared"; + +export interface LibsConfig { + dir: string | null; +} + +export function libsConfig(): LibsConfig { + const dir = process.env.LIBS_DIR; + return { dir: dir ? path.resolve(process.cwd(), dir) : null }; +} + +/** A lib id is its directory name; reject anything that isn't a plain segment. */ +function safeLibDir(root: string, lib: string): string | null { + if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(lib)) return null; + return path.join(root, lib); +} + +interface IndexFile { + items?: { kind: string; name: string; description?: string | null; keywords?: string | null }[]; + description?: string | null; +} + +async function readIndex(dir: string): Promise { + try { + return JSON.parse(await fs.readFile(path.join(dir, "index.json"), "utf8")); + } catch { + return null; + } +} + +export async function listLibs(cfg: LibsConfig): Promise { + if (!cfg.dir) return []; + let entries: import("node:fs").Dirent[]; + try { + entries = await fs.readdir(cfg.dir, { withFileTypes: true }); + } catch { + return []; + } + const libs: Lib[] = []; + for (const e of entries) { + if (!e.isDirectory() || e.name.startsWith(".")) continue; + const idx = await readIndex(path.join(cfg.dir, e.name)); + libs.push({ + id: e.name, + name: e.name, + type: "origin", + description: idx?.description ?? null, + itemCount: idx?.items?.length ?? undefined, + }); + } + libs.sort((a, b) => a.name.localeCompare(b.name)); + return libs; +} + +export async function listLibItems( + cfg: LibsConfig, + lib: string, +): Promise { + if (!cfg.dir) return null; + const dir = safeLibDir(cfg.dir, lib); + if (!dir) return null; + const idx = await readIndex(dir); + if (!idx) return null; + return (idx.items ?? []).map((i) => ({ + kind: i.kind, + name: i.name, + description: i.description ?? null, + keywords: i.keywords ?? null, + })); +} + +/** Resolve the on-disk body file for one item, or null (guarded). */ +export function itemBodyPath( + cfg: LibsConfig, + lib: string, + kind: string, + name: string, +): string | null { + if (!cfg.dir || kind !== "symbol") return null; + const dir = safeLibDir(cfg.dir, lib); + if (!dir) return null; + // Symbol names allow a wide charset but never path separators. + if (name.includes("/") || name.includes("\\") || name.includes("..")) { + return null; + } + return path.join(dir, `${name}.kicad_sym`); +} + +export function streamBody(absPath: string) { + return createReadStream(absPath); +} diff --git a/web/backend/src/server.ts b/web/backend/src/server.ts index e5516f9..f20dfce 100644 --- a/web/backend/src/server.ts +++ b/web/backend/src/server.ts @@ -19,6 +19,13 @@ import { type Project, type ProjectFile, } from "@pcbjam/shared"; +import { + itemBodyPath, + type LibsConfig, + libsConfig, + listLibItems, + listLibs, +} from "./libs.js"; const PROJECT_DIR = path.resolve( process.cwd(), @@ -121,6 +128,8 @@ async function main(): Promise { }); app.get("/health", async () => ({ ok: true })); + const libs: LibsConfig = libsConfig(); + const s = initServer(); const router = s.router(contract, { listProjects: async () => ({ status: 200, body: [await project()] }), @@ -139,9 +148,33 @@ async function main(): Promise { } return { status: 200 as const, body: await walk(PROJECT_DIR) }; }, + listLibs: async () => ({ status: 200 as const, body: await listLibs(libs) }), + listLibItems: async ({ params }) => { + const items = await listLibItems(libs, params.lib); + if (items === null) { + return { status: 404 as const, body: { message: "library not found" } }; + } + return { status: 200 as const, body: items }; + }, }); await app.register(s.plugin(router)); + // Streamed item-body fetch (text; intentionally not a ts-rest endpoint). + app.get<{ Params: { lib: string; kind: string; name: string } }>( + "/api/libs/:lib/items/:kind/:name", + async (req, reply) => { + const abs = itemBodyPath(libs, req.params.lib, req.params.kind, req.params.name); + if (!abs) return reply.code(400).send({ message: "invalid item" }); + const st = await fs.stat(abs).catch(() => null); + if (!st?.isFile()) { + return reply.code(404).send({ message: "item not found" }); + } + reply.header("Content-Type", "text/plain; charset=utf-8"); + reply.header("Content-Length", st.size); + return reply.send(createReadStream(abs)); + }, + ); + // Streamed file-byte download (binary; intentionally not a ts-rest endpoint). app.get<{ Params: { project: string; "*": string } }>( "/api/projects/:project/files/*", diff --git a/web/pcbjam-shared b/web/pcbjam-shared index 92b2634..eb806fc 160000 --- a/web/pcbjam-shared +++ b/web/pcbjam-shared @@ -1 +1 @@ -Subproject commit 92b2634c6d17fb387574b43a38890fd22b30891d +Subproject commit eb806fc14ba1f91d8f99f03186add68d2a19a033 diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index f6b32bb..0c7e0e5 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -12,7 +12,12 @@ import { type Tool, } from "@pcbjam/shared"; import { ChevronDown, ChevronUp } from "lucide-react"; -import { WASM_ASSET_BASE_URL, yjsProviderConfig, type DocSource } from "@/lib/config"; +import { + libsSourceConfig, + WASM_ASSET_BASE_URL, + yjsProviderConfig, + type DocSource, +} from "@/lib/config"; import { bootKicadTool } from "@/wasm/boot"; import { memfsFilePath, memfsProjectDir } from "@/wasm/constants"; import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner"; @@ -437,6 +442,7 @@ export function WasmTool({ log: append, onStatus: setStatus, onAbort: oom.onAbort, + libsSource: libsSourceConfig(), }); // Register the save sink before the file opens: from here on, every // editor File→Save (MEMFS write) is routed onward through saveBytes. diff --git a/web/standalone/src/lib/config.ts b/web/standalone/src/lib/config.ts index 90bbe00..5206fb2 100644 --- a/web/standalone/src/lib/config.ts +++ b/web/standalone/src/lib/config.ts @@ -9,6 +9,9 @@ export const WASM_ASSET_BASE_URL = import.meta.env.VITE_WASM_ASSET_BASE_URL ?? "/wasm"; import type { ProviderConfig, ProviderKind } from "@/wasm/collab"; +import { remoteLibsSource } from "@/wasm/libs/remote-source"; +import type { LibsSource } from "@/wasm/libs/source"; +import { staticLibsSource } from "@/wasm/libs/static-source"; /** * Which Yjs collab provider this deployment uses (one active per env), and its @@ -43,3 +46,18 @@ export type DocSource = "api" | "ydoc"; export function docSourceConfig(): DocSource { return import.meta.env.VITE_DOC_SOURCE === "ydoc" ? "ydoc" : "api"; } + +/** + * Which library source backs the editor's symbol chooser (env `VITE_LIBS_SOURCE`): + * "remote" (default) — fetch from the backend at `API_BASE_URL` over the + * shared contract (origins served by the registry, or the + * GPL example backend). + * "static" — built-in offline example symbols (no backend). + * "off" — disable libs (empty sym-lib-table). + */ +export function libsSourceConfig(): LibsSource | null { + const kind = import.meta.env.VITE_LIBS_SOURCE ?? "remote"; + if (kind === "off") return null; + if (kind === "static") return staticLibsSource(); + return remoteLibsSource(API_BASE_URL); +} diff --git a/web/standalone/src/wasm/boot.ts b/web/standalone/src/wasm/boot.ts index 22f7d7b..2176510 100644 --- a/web/standalone/src/wasm/boot.ts +++ b/web/standalone/src/wasm/boot.ts @@ -5,11 +5,8 @@ import { TOOL_ARGV0, TOOL_NEEDS_CONFIG_SEED, } from "./constants"; -import { - installSpikeLibsProvider, - PCBJAM_LIB_MOUNT, - SPIKE_LIB_TABLE_ROW, -} from "./libs/spike-provider"; +import { buildSymLibTable, installLibsProvider, type LibsSource } from "./libs/source"; +import { PCBJAM_LIB_MOUNT } from "./libs/uri"; /** * Boot a KiCad tool directly in the main React document — no iframe. @@ -45,6 +42,9 @@ export interface BootOptions { /** OOM recovery hook (feature 0002): emscripten `abort()` routes here so a * soft OOM can respawn a fresh tab. Optional — boot works without it. */ onAbort?: (what: string) => void; + /** Library source backing `window.kicadLibs`. Null/omitted disables libs + * (an empty sym-lib-table is seeded). Its libs become sym-lib-table rows. */ + libsSource?: LibsSource | null; } let booted: { tool: Tool; promise: Promise } | null = null; @@ -83,7 +83,7 @@ function loadScript(src: string): Promise { } async function doBoot(opts: BootOptions): Promise { - const { tool, base, container, log, onStatus, onAbort } = opts; + const { tool, base, container, log, onStatus, onAbort, libsSource } = opts; const w = window as ToolWindow; // The wasm reads the top-level frame geometry from a GLOBAL `mainWindow` @@ -93,8 +93,21 @@ async function doBoot(opts: BootOptions): Promise { // mismatches the viewport, breaking the whole AUI layout (toolbars/panels). (w as unknown as { mainWindow: HTMLElement }).mainWindow = container; - // libs 0002 spike: must exist before any plugin call can suspend on it. - installSpikeLibsProvider(log); + // libs: install the provider (must exist before any plugin call can suspend + // on it) and generate the sym-lib-table from the source's libs — both before + // 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"; + if (libsSource) { + installLibsProvider(libsSource, log); + try { + const libsList = await libsSource.listLibs(); + symLibTable = buildSymLibTable(libsList); + log(`[libs] seeded ${libsList.length} lib(s) into sym-lib-table`); + } catch (e) { + log(`[libs] listLibs failed, seeding empty table: ${String(e)}`); + } + } onStatus("Downloading…"); @@ -186,12 +199,9 @@ async function doBoot(opts: BootOptions): Promise { 2, ), ); - // libs 0002 spike: one remote lib row; the PCBJAM plugin resolves it - // through window.kicadLibs (installed in doBoot). - writeIfAbsent( - `${KICAD_CONFIG_DIR}/sym-lib-table`, - `(sym_lib_table\n (version 7)\n${SPIKE_LIB_TABLE_ROW}\n)\n`, - ); + // libs: rows generated in doBoot from the lib source; the PCBJAM plugin + // resolves 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", diff --git a/web/standalone/src/wasm/libs/remote-source.ts b/web/standalone/src/wasm/libs/remote-source.ts new file mode 100644 index 0000000..d6dbd36 --- /dev/null +++ b/web/standalone/src/wasm/libs/remote-source.ts @@ -0,0 +1,44 @@ +import { contract } from "@pcbjam/shared"; +import { initClient } from "@ts-rest/core"; +import type { LibInfo, LibItemInfo, LibsSource } from "./source"; + +/** + * A `LibsSource` backed by a contract-conforming backend (the closed registry + * server, or the GPL example backend). List ops go through the ts-rest client; + * item bodies stream from the raw text route `GET /api/libs/:lib/items/:kind/:name` + * (binary/text does not round-trip ts-rest — same as file-byte download). + */ +export function remoteLibsSource(apiBase: string): LibsSource { + const client = initClient(contract, { baseUrl: apiBase, baseHeaders: {} }); + + return { + async listLibs(): Promise { + const res = await client.listLibs(); + if (res.status !== 200) return []; + return res.body.map((l) => ({ + id: l.id, + name: l.name, + description: l.description ?? null, + })); + }, + + async listItems(libId: string): Promise { + const res = await client.listLibItems({ params: { lib: libId } }); + if (res.status !== 200) return []; + return res.body.map((i) => ({ kind: i.kind, name: i.name })); + }, + + async getItemBody( + libId: string, + kind: string, + name: string, + ): Promise { + const url = + `${apiBase}/api/libs/${encodeURIComponent(libId)}/items/` + + `${encodeURIComponent(kind)}/${encodeURIComponent(name)}`; + const res = await fetch(url); + if (!res.ok) return null; + return await res.text(); + }, + }; +} diff --git a/web/standalone/src/wasm/libs/source.ts b/web/standalone/src/wasm/libs/source.ts new file mode 100644 index 0000000..f7d3711 --- /dev/null +++ b/web/standalone/src/wasm/libs/source.ts @@ -0,0 +1,114 @@ +import { libIdFromUri, libUri } from "./uri"; + +/** + * The data a `LibsSource` provides. A source abstracts WHERE library data comes + * from (a remote backend over the shared contract, a static public snapshot, a + * local folder); the WASM-facing provider below is the same regardless. + */ +export interface LibInfo { + /** Opaque id used in the lib-table URI (/mnt/pcbjam/). */ + id: string; + /** Display nickname for the sym-lib-table row. */ + name: string; + description?: string | null; +} + +export interface LibItemInfo { + kind: string; // 'symbol' | 'footprint' | 'model3d' + name: string; +} + +export interface LibsSource { + /** Libraries to expose to the editor (one sym-lib-table row each). */ + listLibs(): Promise; + /** Items in a library (by lib id). */ + listItems(libId: string): Promise; + /** + * One self-contained item body (a complete `kicad_symbol_lib` s-expr), or + * null if absent. `kind` is 'symbol' for now. + */ + getItemBody(libId: string, kind: string, name: string): Promise; +} + +/** The function the WASM `SCH_IO_PCBJAM_LIB` plugin calls via the JS bridge. */ +export type KicadLibsRequest = ( + op: string, + lib: string, + arg: string, +) => Promise; + +declare global { + interface Window { + kicadLibs?: { request: KicadLibsRequest }; + } +} + +/** Optional artificial latency (`?libdelay=1500`) to exercise the bridge. */ +function artificialDelayMs(): number { + const raw = new URLSearchParams(window.location.search).get("libdelay"); + const n = raw ? Number(raw) : 0; + return Number.isFinite(n) && n > 0 ? n : 0; +} + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +/** Escape a string for a KiCad s-expr quoted token. */ +function sexprEscape(s: string): string { + return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); +} + +/** Build sym-lib-table content (KiCad v7) with one PCBJAM row per lib. */ +export function buildSymLibTable(libsList: LibInfo[]): string { + const rows = libsList.map((l) => { + const descr = l.description ? sexprEscape(l.description) : ""; + return ` (lib (name "${sexprEscape(l.name)}")(type "PCBJAM")(uri "${libUri( + l.id, + )}")(options "")(descr "${descr}"))`; + }); + return `(sym_lib_table\n (version 7)\n${rows.join("\n")}${ + rows.length ? "\n" : "" + })\n`; +} + +/** + * Install `window.kicadLibs` backed by a `LibsSource`. The plugin calls + * `request(op, "/mnt/pcbjam/", arg)`: + * "list" -> JSON {"symbols":[...]} (symbol names in the lib) + * "get" -> the item body s-expr (arg = symbol name) + */ +export function installLibsProvider( + source: LibsSource, + log: (msg: string) => void, +): void { + if (window.kicadLibs) return; + const delay = artificialDelayMs(); + + const request: KicadLibsRequest = async (op, lib, arg) => { + const id = libIdFromUri(lib); + log(`[libs] request op=${op} lib=${lib} (id=${id}) arg=${arg}`); + if (!id) return null; + if (delay) await sleep(delay); + + try { + switch (op) { + case "list": { + const items = await source.listItems(id); + const symbols = items + .filter((i) => i.kind === "symbol") + .map((i) => i.name); + return JSON.stringify({ symbols }); + } + case "get": + return await source.getItemBody(id, "symbol", arg); + default: + return null; + } + } catch (e) { + log(`[libs] request failed: ${String(e)}`); + return null; + } + }; + + window.kicadLibs = { request }; + log("[libs] provider installed"); +} diff --git a/web/standalone/src/wasm/libs/spike-provider.ts b/web/standalone/src/wasm/libs/spike-provider.ts deleted file mode 100644 index 0385d75..0000000 --- a/web/standalone/src/wasm/libs/spike-provider.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * libs 0002 spike: a static JS-side library provider. - * - * The kicad fork's SCH_IO_PCBJAM_LIB plugin (lib-table type "PCBJAM") calls - * `globalThis.kicadLibs.request(op, lib, arg)` through an Asyncify suspension - * (EM_ASYNC_JS) and blocks the C++ stack until the returned promise resolves: - * - * request("list", "pcbjam://spike", "") -> JSON {"symbols": [...]} - * request("get", "pcbjam://spike", "R") -> full kicad_symbol_lib s-expr - * document holding that symbol - * - * This stub serves two handcrafted symbols from memory — no server. An - * artificial delay (`?libdelay=1500` in the URL) exercises the reentrancy - * question: the tab must stay alive while C++ is suspended mid-fetch. - */ - -export const SPIKE_LIB_NICKNAME = "pcbjam-spike"; - -/** - * pcbjam lib URIs are absolute POSIX paths under this prefix. Absolute so that - * KiCad's lib-table URI expansion (ExpandURI -> wxFileName::MakeAbsolute) is a - * no-op and the path reaches the plugin/provider unmangled — a "scheme://" URI - * gets rewritten to "/scheme:/..." against the cwd, which differs per project. - */ -export const PCBJAM_LIB_MOUNT = "/mnt/pcbjam"; -export const PCBJAM_LIB_PREFIX = `${PCBJAM_LIB_MOUNT}/`; -export const SPIKE_LIB_URI = `${PCBJAM_LIB_PREFIX}spike`; - -/** sym-lib-table row seeded at boot (see boot.ts seedKicadConfig). */ -export const SPIKE_LIB_TABLE_ROW = ` (lib (name "${SPIKE_LIB_NICKNAME}")(type "PCBJAM")(uri "${SPIKE_LIB_URI}")(options "")(descr "pcbjam libs 0002 spike"))`; - -const wrapLib = (symbolBody: string) => - `(kicad_symbol_lib (version 20241209) (generator "pcbjam") (generator_version "0.1") -${symbolBody} -) -`; - -const SYMBOLS: Record = { - R: ` (symbol "R" (pin_numbers hide) (pin_names (offset 0)) (exclude_from_sim no) (in_bom yes) (on_board yes) - (property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27)))) - (property "Value" "R" (at 0 0 90) (effects (font (size 1.27 1.27)))) - (property "Footprint" "" (at -1.778 0 90) (effects (font (size 1.27 1.27)) hide)) - (property "Datasheet" "~" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) - (property "Description" "Resistor (pcbjam spike)" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) - (property "ki_keywords" "R res resistor" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) - (symbol "R_0_1" - (rectangle (start -1.016 -2.54) (end 1.016 2.54) (stroke (width 0.254) (type default)) (fill (type none))) - ) - (symbol "R_1_1" - (pin passive line (at 0 3.81 270) (length 1.27) - (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) - (pin passive line (at 0 -3.81 90) (length 1.27) - (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27))))) - ) - )`, - C: ` (symbol "C" (pin_numbers hide) (pin_names (offset 0.254)) (exclude_from_sim no) (in_bom yes) (on_board yes) - (property "Reference" "C" (at 0.635 2.54 0) (effects (font (size 1.27 1.27)) (justify left))) - (property "Value" "C" (at 0.635 -2.54 0) (effects (font (size 1.27 1.27)) (justify left))) - (property "Footprint" "" (at 0.9652 -3.81 0) (effects (font (size 1.27 1.27)) hide)) - (property "Datasheet" "~" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) - (property "Description" "Capacitor (pcbjam spike)" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) - (property "ki_keywords" "cap capacitor" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) - (symbol "C_0_1" - (polyline (pts (xy -2.032 -0.762) (xy 2.032 -0.762)) (stroke (width 0.508) (type default)) (fill (type none))) - (polyline (pts (xy -2.032 0.762) (xy 2.032 0.762)) (stroke (width 0.508) (type default)) (fill (type none))) - ) - (symbol "C_1_1" - (pin passive line (at 0 3.81 270) (length 2.794) - (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) - (pin passive line (at 0 -3.81 90) (length 2.794) - (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27))))) - ) - )`, -}; - -export type KicadLibsRequest = ( - op: string, - lib: string, - arg: string, -) => Promise; - -declare global { - interface Window { - kicadLibs?: { request: KicadLibsRequest }; - } -} - -function artificialDelayMs(): number { - const raw = new URLSearchParams(window.location.search).get("libdelay"); - const n = raw ? Number(raw) : 0; - return Number.isFinite(n) && n > 0 ? n : 0; -} - -const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -/** Recover the lib id from a "/mnt/pcbjam/" URI (arrives unmangled). */ -function libIdFromUri(lib: string): string | null { - return lib.startsWith(PCBJAM_LIB_PREFIX) - ? lib.slice(PCBJAM_LIB_PREFIX.length) - : null; -} - -export function installSpikeLibsProvider(log: (msg: string) => void): void { - if (window.kicadLibs) return; - - const delay = artificialDelayMs(); - - const request: KicadLibsRequest = async (op, lib, arg) => { - log(`[libs] request op=${op} lib=${lib} arg=${arg} (delay=${delay}ms)`); - - if (libIdFromUri(lib) !== "spike") return null; - if (delay) await sleep(delay); - - switch (op) { - case "list": - return JSON.stringify({ symbols: Object.keys(SYMBOLS) }); - case "get": { - const body = SYMBOLS[arg]; - return body ? wrapLib(body) : null; - } - default: - return null; - } - }; - - window.kicadLibs = { request }; - log(`[libs] spike provider installed (${SPIKE_LIB_URI})`); -} diff --git a/web/standalone/src/wasm/libs/static-source.ts b/web/standalone/src/wasm/libs/static-source.ts new file mode 100644 index 0000000..b7bbc98 --- /dev/null +++ b/web/standalone/src/wasm/libs/static-source.ts @@ -0,0 +1,78 @@ +import type { LibInfo, LibItemInfo, LibsSource } from "./source"; + +/** + * A static, in-memory `LibsSource` — two handcrafted symbols, no backend. The + * offline fallback when no API base is configured (and the origin of the libs + * 0002 spike). Keeps the editor's lib chooser populated in a bare checkout. + */ + +const STATIC_LIB: LibInfo = { + id: "examples", + name: "pcbjam-examples", + description: "Built-in example symbols (offline)", +}; + +const wrapLib = (symbolBody: string) => + `(kicad_symbol_lib (version 20241209) (generator "pcbjam") (generator_version "0.1") +${symbolBody} +) +`; + +const SYMBOLS: Record = { + R: ` (symbol "R" (pin_numbers hide) (pin_names (offset 0)) (exclude_from_sim no) (in_bom yes) (on_board yes) + (property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27)))) + (property "Value" "R" (at 0 0 90) (effects (font (size 1.27 1.27)))) + (property "Footprint" "" (at -1.778 0 90) (effects (font (size 1.27 1.27)) hide)) + (property "Datasheet" "~" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (property "Description" "Resistor (pcbjam example)" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (property "ki_keywords" "R res resistor" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (symbol "R_0_1" + (rectangle (start -1.016 -2.54) (end 1.016 2.54) (stroke (width 0.254) (type default)) (fill (type none))) + ) + (symbol "R_1_1" + (pin passive line (at 0 3.81 270) (length 1.27) + (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) + (pin passive line (at 0 -3.81 90) (length 1.27) + (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27))))) + ) + )`, + C: ` (symbol "C" (pin_numbers hide) (pin_names (offset 0.254)) (exclude_from_sim no) (in_bom yes) (on_board yes) + (property "Reference" "C" (at 0.635 2.54 0) (effects (font (size 1.27 1.27)) (justify left))) + (property "Value" "C" (at 0.635 -2.54 0) (effects (font (size 1.27 1.27)) (justify left))) + (property "Footprint" "" (at 0.9652 -3.81 0) (effects (font (size 1.27 1.27)) hide)) + (property "Datasheet" "~" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (property "Description" "Capacitor (pcbjam example)" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (property "ki_keywords" "cap capacitor" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (symbol "C_0_1" + (polyline (pts (xy -2.032 -0.762) (xy 2.032 -0.762)) (stroke (width 0.508) (type default)) (fill (type none))) + (polyline (pts (xy -2.032 0.762) (xy 2.032 0.762)) (stroke (width 0.508) (type default)) (fill (type none))) + ) + (symbol "C_1_1" + (pin passive line (at 0 3.81 270) (length 2.794) + (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) + (pin passive line (at 0 -3.81 90) (length 2.794) + (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27))))) + ) + )`, +}; + +export function staticLibsSource(): LibsSource { + return { + async listLibs(): Promise { + return [STATIC_LIB]; + }, + async listItems(libId: string): Promise { + if (libId !== STATIC_LIB.id) return []; + return Object.keys(SYMBOLS).map((name) => ({ kind: "symbol", name })); + }, + async getItemBody( + libId: string, + _kind: string, + name: string, + ): Promise { + if (libId !== STATIC_LIB.id) return null; + const body = SYMBOLS[name]; + return body ? wrapLib(body) : null; + }, + }; +} diff --git a/web/standalone/src/wasm/libs/uri.ts b/web/standalone/src/wasm/libs/uri.ts new file mode 100644 index 0000000..aac62ba --- /dev/null +++ b/web/standalone/src/wasm/libs/uri.ts @@ -0,0 +1,20 @@ +/** + * pcbjam lib URIs are absolute POSIX paths under this mount. Absolute so that + * KiCad's lib-table URI expansion (ExpandURI -> wxFileName::MakeAbsolute) is a + * no-op and the path reaches the plugin/provider unmangled — a "scheme://" URI + * gets rewritten to "/scheme:/..." against the cwd, which differs per project. + */ +export const PCBJAM_LIB_MOUNT = "/mnt/pcbjam"; +export const PCBJAM_LIB_PREFIX = `${PCBJAM_LIB_MOUNT}/`; + +/** The lib-table URI for a lib id. */ +export function libUri(id: string): string { + return `${PCBJAM_LIB_PREFIX}${id}`; +} + +/** Recover the lib id from a "/mnt/pcbjam/" URI (arrives unmangled). */ +export function libIdFromUri(uri: string): string | null { + return uri.startsWith(PCBJAM_LIB_PREFIX) + ? uri.slice(PCBJAM_LIB_PREFIX.length) + : null; +}