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
|
|
@ -61,7 +61,7 @@ trap 'kw_fail 130; exit 130' INT TERM
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
VALID_APPS="pcbnew | eeschema | calculator | pl_editor | symbol_editor | gerbview | all"
|
VALID_APPS="pcbnew | eeschema | calculator | pl_editor | symbol_editor | footprint_editor | gerbview | all"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: ./docker/build.sh <app>[,<app>...] [args...]" >&2
|
echo "Usage: ./docker/build.sh <app>[,<app>...] [args...]" >&2
|
||||||
|
|
@ -87,12 +87,12 @@ shift
|
||||||
# pcbnew first in "all" — its 90-min host-side wasm-opt chain is the critical
|
# pcbnew first in "all" — its 90-min host-side wasm-opt chain is the critical
|
||||||
# path, so it must start as early as possible (especially with KICAD_PIPELINE=1).
|
# path, so it must start as early as possible (especially with KICAD_PIPELINE=1).
|
||||||
if [[ "$APP_NAME" == "all" ]]; then
|
if [[ "$APP_NAME" == "all" ]]; then
|
||||||
APPS=(pcbnew eeschema calculator pl_editor symbol_editor gerbview)
|
APPS=(pcbnew eeschema calculator pl_editor symbol_editor footprint_editor gerbview)
|
||||||
else
|
else
|
||||||
IFS=',' read -r -a APPS <<< "$APP_NAME"
|
IFS=',' read -r -a APPS <<< "$APP_NAME"
|
||||||
for app in "${APPS[@]}"; do
|
for app in "${APPS[@]}"; do
|
||||||
case "$app" in
|
case "$app" in
|
||||||
pcbnew|eeschema|calculator|pl_editor|symbol_editor|gerbview) ;;
|
pcbnew|eeschema|calculator|pl_editor|symbol_editor|footprint_editor|gerbview) ;;
|
||||||
*)
|
*)
|
||||||
echo "Error: unknown app '$app' (expected: ${VALID_APPS})" >&2
|
echo "Error: unknown app '$app' (expected: ${VALID_APPS})" >&2
|
||||||
usage
|
usage
|
||||||
|
|
@ -159,10 +159,11 @@ fi
|
||||||
# but lives under the pcb_calculator/ subtree.
|
# but lives under the pcb_calculator/ subtree.
|
||||||
kicad_subdir_for() {
|
kicad_subdir_for() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
calculator) echo "pcb_calculator" ;;
|
calculator) echo "pcb_calculator" ;;
|
||||||
pl_editor) echo "pagelayout_editor" ;;
|
pl_editor) echo "pagelayout_editor" ;;
|
||||||
symbol_editor) echo "eeschema" ;;
|
symbol_editor) echo "eeschema" ;;
|
||||||
*) echo "$1" ;;
|
footprint_editor) echo "pcbnew" ;;
|
||||||
|
*) echo "$1" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit 67161915ebb89133b24e04d5d8d1e1e0cc791fbc
|
Subproject commit 3b20be780148c923d835c201eb6c3edba4bda3a3
|
||||||
9
scripts/kicad/build-footprint_editor.sh
Executable file
9
scripts/kicad/build-footprint_editor.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build KiCad Footprint Editor for WebAssembly.
|
||||||
|
# Thin wrapper around build-kicad-target.sh — see that script for options.
|
||||||
|
# (The footprint editor is the pcbnew kiface launched at FRAME_FOOTPRINT_EDITOR,
|
||||||
|
# like symbol_editor is the eeschema kiface at FRAME_SCH_SYMBOL_EDITOR.)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec "${SCRIPT_DIR}/build-kicad-target.sh" footprint_editor "$@"
|
||||||
|
|
@ -60,8 +60,13 @@ case "$APP_NAME" in
|
||||||
KICAD_TARGET="symbol_editor"
|
KICAD_TARGET="symbol_editor"
|
||||||
KICAD_SUBDIR="eeschema"
|
KICAD_SUBDIR="eeschema"
|
||||||
;;
|
;;
|
||||||
|
footprint_editor)
|
||||||
|
# served by the pcbnew kiface, so it builds in pcbnew/ (like symbol_editor in eeschema/)
|
||||||
|
KICAD_TARGET="footprint_editor"
|
||||||
|
KICAD_SUBDIR="pcbnew"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | calculator | pl_editor | symbol_editor | gerbview)" >&2
|
echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | calculator | pl_editor | symbol_editor | footprint_editor | gerbview)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -72,8 +77,18 @@ esac
|
||||||
# et al.) the shared kiface objects reference. Without this the symbol_editor link
|
# et al.) the shared kiface objects reference. Without this the symbol_editor link
|
||||||
# fails with "undefined symbol: kicadCollabOnSave" (the placeholder defines nothing).
|
# fails with "undefined symbol: kicadCollabOnSave" (the placeholder defines nothing).
|
||||||
case "$APP_NAME" in
|
case "$APP_NAME" in
|
||||||
symbol_editor) EMBIND_APP="eeschema" ;;
|
symbol_editor) EMBIND_APP="eeschema" ;;
|
||||||
*) EMBIND_APP="$APP_NAME" ;;
|
footprint_editor) EMBIND_APP="pcbnew" ;;
|
||||||
|
*) EMBIND_APP="$APP_NAME" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Which app's WASM stub libraries (scripting/frame placeholders) to link. Like
|
||||||
|
# EMBIND_APP, the footprint_editor reuses pcbnew's: it links the pcbnew kiface
|
||||||
|
# objects, which reference pcbnew's action-plugin scripting symbols
|
||||||
|
# (pcbnewGetScriptsSearchPaths et al., defined in pcbnew_scripting_stub.cpp).
|
||||||
|
case "$APP_NAME" in
|
||||||
|
footprint_editor) STUB_APP="pcbnew" ;;
|
||||||
|
*) STUB_APP="$APP_NAME" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
@ -262,20 +277,20 @@ WX_CXXFLAGS=$("${WX_BUILD}/wx-config" --cxxflags 2>/dev/null || echo "-I${WX_BUI
|
||||||
# - pcbnew: pcbnew_scripting_stub.cpp (action-plugin scripting placeholders)
|
# - pcbnew: pcbnew_scripting_stub.cpp (action-plugin scripting placeholders)
|
||||||
# - eeschema: eeschema_frame_stub.cpp (placeholder; grows as linker dictates)
|
# - eeschema: eeschema_frame_stub.cpp (placeholder; grows as linker dictates)
|
||||||
APP_STUB_LINK=""
|
APP_STUB_LINK=""
|
||||||
APP_SCRIPTING_STUB_SRC="${STUBS_DIR}/${APP_NAME}_scripting_stub.cpp"
|
APP_SCRIPTING_STUB_SRC="${STUBS_DIR}/${STUB_APP}_scripting_stub.cpp"
|
||||||
if [ -f "${APP_SCRIPTING_STUB_SRC}" ]; then
|
if [ -f "${APP_SCRIPTING_STUB_SRC}" ]; then
|
||||||
log_info "Building app scripting stub: ${APP_NAME}_scripting_stub.cpp"
|
log_info "Building app scripting stub: ${STUB_APP}_scripting_stub.cpp"
|
||||||
em++ -c ${WX_CXXFLAGS} "${APP_SCRIPTING_STUB_SRC}" -o "${STUBS_BUILD}/${APP_NAME}_scripting_stub.o"
|
em++ -c ${WX_CXXFLAGS} "${APP_SCRIPTING_STUB_SRC}" -o "${STUBS_BUILD}/${STUB_APP}_scripting_stub.o"
|
||||||
emar rcs "${STUBS_BUILD}/lib${APP_NAME}_scripting_stub.a" "${STUBS_BUILD}/${APP_NAME}_scripting_stub.o"
|
emar rcs "${STUBS_BUILD}/lib${STUB_APP}_scripting_stub.a" "${STUBS_BUILD}/${STUB_APP}_scripting_stub.o"
|
||||||
APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${APP_NAME}_scripting_stub.a"
|
APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${STUB_APP}_scripting_stub.a"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
APP_FRAME_STUB_SRC="${STUBS_DIR}/${APP_NAME}_frame_stub.cpp"
|
APP_FRAME_STUB_SRC="${STUBS_DIR}/${STUB_APP}_frame_stub.cpp"
|
||||||
if [ -f "${APP_FRAME_STUB_SRC}" ] && [ -s "${APP_FRAME_STUB_SRC}" ]; then
|
if [ -f "${APP_FRAME_STUB_SRC}" ] && [ -s "${APP_FRAME_STUB_SRC}" ]; then
|
||||||
log_info "Building app frame stub: ${APP_NAME}_frame_stub.cpp"
|
log_info "Building app frame stub: ${STUB_APP}_frame_stub.cpp"
|
||||||
em++ -c ${WX_CXXFLAGS} "${APP_FRAME_STUB_SRC}" -o "${STUBS_BUILD}/${APP_NAME}_frame_stub.o"
|
em++ -c ${WX_CXXFLAGS} "${APP_FRAME_STUB_SRC}" -o "${STUBS_BUILD}/${STUB_APP}_frame_stub.o"
|
||||||
emar rcs "${STUBS_BUILD}/lib${APP_NAME}_frame_stub.a" "${STUBS_BUILD}/${APP_NAME}_frame_stub.o"
|
emar rcs "${STUBS_BUILD}/lib${STUB_APP}_frame_stub.a" "${STUBS_BUILD}/${STUB_APP}_frame_stub.o"
|
||||||
APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${APP_NAME}_frame_stub.a"
|
APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${STUB_APP}_frame_stub.a"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "Stub libraries built"
|
log_info "Stub libraries built"
|
||||||
|
|
@ -408,7 +423,9 @@ if [ -f "${EMBIND_SRC}" ]; then
|
||||||
# ${KICAD_BUILD}/common by make_lexer custom commands on the pcbcommon target).
|
# ${KICAD_BUILD}/common by make_lexer custom commands on the pcbcommon target).
|
||||||
# On a fresh build dir they don't exist until make runs — build pcbcommon first.
|
# On a fresh build dir they don't exist until make runs — build pcbcommon first.
|
||||||
# No wasted work: the app target depends on pcbcommon anyway; incremental no-op.
|
# No wasted work: the app target depends on pcbcommon anyway; incremental no-op.
|
||||||
if [ "${APP_NAME}" = "pcbnew" ]; then
|
# Guard on EMBIND_APP (not APP_NAME) so footprint_editor — whose embind IS
|
||||||
|
# pcbnew's — also pre-builds pcbcommon.
|
||||||
|
if [ "${EMBIND_APP}" = "pcbnew" ]; then
|
||||||
log_info "Pre-building pcbcommon so generated lexer headers exist for the embind compile..."
|
log_info "Pre-building pcbcommon so generated lexer headers exist for the embind compile..."
|
||||||
emmake make -j${JOBS} pcbcommon
|
emmake make -j${JOBS} pcbcommon
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,11 @@ smart_cp() {
|
||||||
# source lives under pagelayout_editor/.
|
# source lives under pagelayout_editor/.
|
||||||
kicad_subdir_for() {
|
kicad_subdir_for() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
calculator) echo "pcb_calculator" ;;
|
calculator) echo "pcb_calculator" ;;
|
||||||
pl_editor) echo "pagelayout_editor" ;;
|
pl_editor) echo "pagelayout_editor" ;;
|
||||||
symbol_editor) echo "eeschema" ;;
|
symbol_editor) echo "eeschema" ;;
|
||||||
*) echo "$1" ;;
|
footprint_editor) echo "pcbnew" ;;
|
||||||
|
*) echo "$1" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,15 +81,16 @@ copy_app() {
|
||||||
}
|
}
|
||||||
|
|
||||||
found_any=0
|
found_any=0
|
||||||
copy_app pcbnew && found_any=1
|
copy_app pcbnew && found_any=1
|
||||||
copy_app eeschema && found_any=1
|
copy_app eeschema && found_any=1
|
||||||
copy_app calculator && found_any=1
|
copy_app calculator && found_any=1
|
||||||
copy_app pl_editor && found_any=1
|
copy_app pl_editor && found_any=1
|
||||||
copy_app symbol_editor && found_any=1
|
copy_app symbol_editor && found_any=1
|
||||||
copy_app gerbview && found_any=1
|
copy_app footprint_editor && found_any=1
|
||||||
|
copy_app gerbview && found_any=1
|
||||||
|
|
||||||
if [ "$found_any" -eq 0 ]; then
|
if [ "$found_any" -eq 0 ]; then
|
||||||
echo "Error: no pcbnew/eeschema/calculator/pl_editor/symbol_editor/gerbview artifacts found in output/ or docker volume" >&2
|
echo "Error: no pcbnew/eeschema/calculator/pl_editor/symbol_editor/footprint_editor/gerbview artifacts found in output/ or docker volume" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
171
tests/web/footprint-write-spike.spec.ts
Normal file
171
tests/web/footprint-write-spike.spec.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
|
import { waitForRegistry, clickByTooltip } from '../e2e/utils/element-tracker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0009-S footprint write-path spike: does the FOOTPRINT editor boot+render in
|
||||||
|
* WASM (with a working Prj()), and does its FootprintSave path route through the
|
||||||
|
* pcbjam write bridge, on the MAIN thread, without wedging?
|
||||||
|
*
|
||||||
|
* Mirrors the proven 0004-A symbol spike, one domain over. Boots the
|
||||||
|
* project-scoped footprint_editor with `?fpwrite=1` (an in-memory writable
|
||||||
|
* "My Footprints (spike)" lib via the new PCB_IO_PCBJAM_FP plugin + the 4th
|
||||||
|
* `kind` bridge arg), creates a new footprint in it, and saves. The plugin's
|
||||||
|
* FootprintSave serializes a fork-native (footprint …) s-expr and calls
|
||||||
|
* window.kicadLibs.request("save", …, "footprint") on the main thread
|
||||||
|
* (EM_ASYNC_JS), captured onto window.__pcbjamSaved. Assert: the body is
|
||||||
|
* well-formed fork-native s-expr (version 20251028), and the app stays live
|
||||||
|
* (no abort / no OOM respawn) — i.e. the editor-as-tool + main-thread Asyncify
|
||||||
|
* save both work. THIS IS THE GATE before the backend (0009-A) is built.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SHOT = (name: string) => `test-results/fpwrite-${name}.png`;
|
||||||
|
|
||||||
|
async function bootFootprintEditor(page: Page): Promise<void> {
|
||||||
|
await page.goto('/p/demo/footprint_editor/?fpwrite=1');
|
||||||
|
// GATE part 1: the footprint editor frame renders in WASM (canvas + GL).
|
||||||
|
await expect(page.locator('#canvas')).toBeVisible({ timeout: 180000 });
|
||||||
|
await waitForRegistry(page, 180000);
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => !!window.wxElementRegistry && window.wxElementRegistry.findAll({}).length > 5,
|
||||||
|
null,
|
||||||
|
{ timeout: 180000 },
|
||||||
|
);
|
||||||
|
await page.waitForFunction(() => !!(window as any).kicadLibs, null, { timeout: 60000 });
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function focusCanvas(page: Page): Promise<void> {
|
||||||
|
const box = await page.locator('#canvas').boundingBox();
|
||||||
|
if (box) await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('footprint editor save routes through the pcbjam_fp write bridge (main thread)', async ({ page }) => {
|
||||||
|
const logs: string[] = [];
|
||||||
|
page.on('console', (m) => logs.push(`[${m.type()}] ${m.text()}`));
|
||||||
|
page.on('pageerror', (e) => logs.push(`[pageerror] ${e.message}`));
|
||||||
|
|
||||||
|
await bootFootprintEditor(page);
|
||||||
|
await page.screenshot({ path: SHOT('01-boot'), scale: 'css' });
|
||||||
|
|
||||||
|
// Spike provider is active.
|
||||||
|
expect(await page.evaluate(() => !!(window as any).__pcbjamSaved), 'spike marker present').toBe(true);
|
||||||
|
|
||||||
|
// Select the writable lib in the tree so New Footprint targets it. Same LIB_TREE
|
||||||
|
// widget as the symbol editor: the dataviewitem's registry y sits on the column
|
||||||
|
// header, so anchor off the "Item" column header and click one row below it.
|
||||||
|
const hdr = await page.evaluate(() => {
|
||||||
|
const rd = window.wxElementRegistry.findAllRendered({});
|
||||||
|
const h = rd.find((e: any) => e.elementType === 'columnheader' && e.label === 'Item');
|
||||||
|
return h ? { cx: h.centerX, cy: h.centerY, hgt: h.height } : null;
|
||||||
|
});
|
||||||
|
if (hdr) {
|
||||||
|
await page.mouse.click(hdr.cx, hdr.cy + hdr.hgt + 8); // focus the tree
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Home');
|
||||||
|
await page.waitForTimeout(150);
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await page.waitForTimeout(150);
|
||||||
|
await page.keyboard.press('ArrowUp');
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
} else {
|
||||||
|
logs.push('[spec] WARN: Item column header not found (tree may differ); proceeding');
|
||||||
|
}
|
||||||
|
await page.screenshot({ path: SHOT('02-lib-selected'), scale: 'css' });
|
||||||
|
|
||||||
|
// New Footprint via the toolbar button. The fork action's FriendlyName is
|
||||||
|
// "New Footprint" (PCB_ACTIONS::newFootprint); the "..." variant is a fallback.
|
||||||
|
let clicked = await clickByTooltip(page, 'New Footprint');
|
||||||
|
if (!clicked) clicked = await clickByTooltip(page, 'New Footprint...');
|
||||||
|
if (!clicked) {
|
||||||
|
// Diagnostics: dump available tooltips so we can fix the label if needed.
|
||||||
|
const tips = await page.evaluate(() =>
|
||||||
|
window.wxElementRegistry
|
||||||
|
.findAll({})
|
||||||
|
.map((e: any) => e.tooltip)
|
||||||
|
.filter((t: string) => !!t),
|
||||||
|
);
|
||||||
|
logs.push(`[spec] New Footprint tooltip not found; tooltips: ${JSON.stringify(tips)}`);
|
||||||
|
}
|
||||||
|
expect(clicked, 'New Footprint toolbar button clicked').toBe(true);
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
await page.screenshot({ path: SHOT('03-newfp-dialog'), scale: 'css' });
|
||||||
|
|
||||||
|
// Diagnostics: what dialog/controls are present now.
|
||||||
|
const dlg = await page.evaluate(() => {
|
||||||
|
const all = window.wxElementRegistry.findAll({ visible: true });
|
||||||
|
const pick = (re: RegExp) => all.filter((e: any) => re.test(e.typeName))
|
||||||
|
.map((e: any) => ({ type: e.typeName, name: e.name, label: e.label, cx: Math.round(e.centerX), cy: Math.round(e.centerY), en: e.enabled }));
|
||||||
|
return { dialogs: pick(/Dialog/i), texts: pick(/TextCtrl/i), buttons: pick(/Button/i), combos: pick(/Choice|ComboBox/i) };
|
||||||
|
});
|
||||||
|
logs.push(`[spec] after New Footprint: ${JSON.stringify(dlg)}`);
|
||||||
|
|
||||||
|
// If a name dialog is present, type our name and confirm with Enter (default
|
||||||
|
// button). Some flows create directly with a default name — handle both.
|
||||||
|
const FP = 'SpikeFP';
|
||||||
|
if (dlg.texts.length > 0) {
|
||||||
|
const nameField = dlg.texts[0];
|
||||||
|
await page.mouse.click(nameField.cx, nameField.cy);
|
||||||
|
await page.waitForTimeout(150);
|
||||||
|
await page.keyboard.press('Control+a');
|
||||||
|
await page.keyboard.press('Delete');
|
||||||
|
await page.keyboard.type(FP, { delay: 40 });
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.screenshot({ path: SHOT('04-name-typed'), scale: 'css' });
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
}
|
||||||
|
await page.screenshot({ path: SHOT('05-fp-created'), scale: 'css' });
|
||||||
|
logs.push(`[spec] title after create: ${await page.title()}`);
|
||||||
|
|
||||||
|
// Save: focus the canvas, Ctrl+S (the proven save trigger). The New Footprint
|
||||||
|
// flow may already auto-save into the writable lib; Ctrl+S is belt-and-braces.
|
||||||
|
await focusCanvas(page);
|
||||||
|
await page.keyboard.press('Control+s');
|
||||||
|
|
||||||
|
// Wait for the bridge to capture the saved body (name-agnostic — the editor may
|
||||||
|
// massage or default the footprint name).
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const saved = (window as any).__pcbjamSaved as Record<string, string> | undefined;
|
||||||
|
return !!saved && Object.values(saved).some((v) => typeof v === 'string' && v.length > 0);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ timeout: 30000 },
|
||||||
|
);
|
||||||
|
await page.screenshot({ path: SHOT('06-saved'), scale: 'css' });
|
||||||
|
|
||||||
|
const { savedName, body } = await page.evaluate(() => {
|
||||||
|
const saved = (window as any).__pcbjamSaved as Record<string, string>;
|
||||||
|
const k = Object.keys(saved).find((n) => saved[n]?.length)!;
|
||||||
|
return { savedName: k, body: saved[k] };
|
||||||
|
});
|
||||||
|
logs.push(`[spec] saved "${savedName}" (${body.length} bytes):\n${body}`);
|
||||||
|
|
||||||
|
// The captured body is a well-formed fork-native footprint at the fork's native
|
||||||
|
// board/footprint file version (20251028) — proves verbatim round-trip, no shim.
|
||||||
|
expect(body).toContain('(footprint');
|
||||||
|
expect(body).toContain('(version 20251028)');
|
||||||
|
expect(body).toContain('(generator "pcbnew")');
|
||||||
|
expect(body).toContain(savedName);
|
||||||
|
|
||||||
|
// No post-save error dialog (the MEMFS placeholder-file fix for the footprint
|
||||||
|
// editor's setFPWatcher -> GetModificationTime stat).
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const errDialog = await page.evaluate(() =>
|
||||||
|
window.wxElementRegistry
|
||||||
|
.findAll({ visible: true })
|
||||||
|
.some((e: any) => /Dialog/i.test(e.typeName) && /Error/i.test(e.label || '')),
|
||||||
|
);
|
||||||
|
expect(errDialog, 'no post-save error dialog').toBe(false);
|
||||||
|
expect(
|
||||||
|
logs.some((l) => l.includes('Failed to retrieve file times')),
|
||||||
|
'no file-times error',
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
// App stayed live: no abort, no OOM respawn (the main-thread Asyncify save gate).
|
||||||
|
expect(logs.some((l) => l.includes('Aborted(')), 'no WASM abort').toBe(false);
|
||||||
|
expect(new URL(page.url()).searchParams.get('oomRetry'), 'no OOM respawn').toBeNull();
|
||||||
|
|
||||||
|
console.log('--- console + spec log ---\n' + logs.join('\n'));
|
||||||
|
});
|
||||||
|
|
@ -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 type { ProviderConfig, ProviderKind } from "@/wasm/collab";
|
||||||
import { remoteLibsSource } from "@/wasm/libs/remote-source";
|
import { remoteLibsSource } from "@/wasm/libs/remote-source";
|
||||||
import type { LibsSource } from "@/wasm/libs/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";
|
import { staticLibsSource } from "@/wasm/libs/static-source";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -78,14 +81,18 @@ export function libsSourceConfig(projectId?: string): LibsSource | null {
|
||||||
? staticLibsSource()
|
? staticLibsSource()
|
||||||
: remoteLibsSource(API_BASE_URL, libsOwner(), projectId);
|
: 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
|
// 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.
|
// write path (0004-C) needs no flag — boot ensures a user lib via createLib.
|
||||||
if (
|
// 0009-S spike: `?fpwrite=1` does the same for a writable FOOTPRINT lib.
|
||||||
typeof window !== "undefined" &&
|
if (typeof window !== "undefined") {
|
||||||
new URLSearchParams(window.location.search).get("libwrite") === "1"
|
const params = new URLSearchParams(window.location.search);
|
||||||
) {
|
if (params.get("fpwrite") === "1") {
|
||||||
return withSpikeWritableLib(base, (m) => console.log(m));
|
return withSpikeWritableFpLib(base, (m) => console.log(m));
|
||||||
|
}
|
||||||
|
if (params.get("libwrite") === "1") {
|
||||||
|
return withSpikeWritableLib(base, (m) => console.log(m));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base;
|
return base;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,15 @@ import {
|
||||||
KICAD_CONFIG_DIR,
|
KICAD_CONFIG_DIR,
|
||||||
RESOURCE_PATH,
|
RESOURCE_PATH,
|
||||||
TOOL_ARGV0,
|
TOOL_ARGV0,
|
||||||
|
TOOL_LIB_KIND,
|
||||||
TOOL_NEEDS_CONFIG_SEED,
|
TOOL_NEEDS_CONFIG_SEED,
|
||||||
} from "./constants";
|
} 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";
|
import { libUri, PCBJAM_LIB_MOUNT } from "./libs/uri";
|
||||||
|
|
||||||
/** The default user lib boot ensures exists, so there's a writable save target. */
|
/** 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
|
// the wasm boots (the table is seeded in preRun below). No source → empty
|
||||||
// table, libs disabled.
|
// table, libs disabled.
|
||||||
let symLibTable = "(sym_lib_table\n (version 7)\n)\n";
|
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):
|
// 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
|
// the editor save path stat()s the lib file after a successful save
|
||||||
// (SetSymModificationTime -> wxFileName::GetModificationTime), which errors on
|
// (symbol: SetSymModificationTime; footprint: setFPWatcher -> GetModificationTime),
|
||||||
// a non-existent path. The bytes are virtual (served via window.kicadLibs);
|
// which errors on a non-existent path. The bytes are virtual (served via
|
||||||
// this file only satisfies incidental fs checks.
|
// window.kicadLibs); this file only satisfies incidental fs checks.
|
||||||
let libPlaceholderUris: string[] = [];
|
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);
|
installLibsProvider(libsSource, log);
|
||||||
try {
|
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();
|
let libsList = await libsSource.listLibs();
|
||||||
if (libsSource.createLib && !libsList.some((l) => l.type === "user")) {
|
if (libsSource.createLib && !libsList.some((l) => l.type === "user")) {
|
||||||
const created = await libsSource.createLib(DEFAULT_USER_LIB_NAME);
|
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}"`);
|
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));
|
libPlaceholderUris = libsList.map((l) => libUri(l.id));
|
||||||
log(`[libs] seeded ${libsList.length} lib(s) into sym-lib-table`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`[libs] listLibs failed, seeding empty table: ${String(e)}`);
|
log(`[libs] listLibs failed, seeding empty table: ${String(e)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -222,13 +237,10 @@ async function doBoot(opts: BootOptions): Promise<void> {
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// libs: rows generated in doBoot from the lib source; the PCBJAM plugin
|
// libs: rows generated in doBoot from the lib source; the PCBJAM / PCBJAM_FP
|
||||||
// resolves each via window.kicadLibs.
|
// plugins resolve each via window.kicadLibs.
|
||||||
writeIfAbsent(`${KICAD_CONFIG_DIR}/sym-lib-table`, symLibTable);
|
writeIfAbsent(`${KICAD_CONFIG_DIR}/sym-lib-table`, symLibTable);
|
||||||
writeIfAbsent(
|
writeIfAbsent(`${KICAD_CONFIG_DIR}/fp-lib-table`, fpLibTable);
|
||||||
`${KICAD_CONFIG_DIR}/fp-lib-table`,
|
|
||||||
"(fp_lib_table\n (version 7)\n)\n",
|
|
||||||
);
|
|
||||||
writeIfAbsent(
|
writeIfAbsent(
|
||||||
`${KICAD_CONFIG_DIR}/design-block-lib-table`,
|
`${KICAD_CONFIG_DIR}/design-block-lib-table`,
|
||||||
"(design_block_lib_table\n (version 7)\n)\n",
|
"(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",
|
calculator: "/usr/bin/pcb_calculator",
|
||||||
pl_editor: "/usr/bin/pl_editor",
|
pl_editor: "/usr/bin/pl_editor",
|
||||||
symbol_editor: "/usr/bin/symbol_editor",
|
symbol_editor: "/usr/bin/symbol_editor",
|
||||||
|
footprint_editor: "/usr/bin/footprint_editor",
|
||||||
gerbview: "/usr/bin/gerbview",
|
gerbview: "/usr/bin/gerbview",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -44,9 +45,26 @@ export const TOOL_NEEDS_CONFIG_SEED: Record<Tool, boolean> = {
|
||||||
calculator: true,
|
calculator: true,
|
||||||
pl_editor: true,
|
pl_editor: true,
|
||||||
symbol_editor: true,
|
symbol_editor: true,
|
||||||
|
footprint_editor: true,
|
||||||
gerbview: 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()). */
|
/** KiCad user settings dir for this build (PATHS::GetUserSettingsPath()). */
|
||||||
export const KICAD_CONFIG_DIR = `/home/kicad/.config/kicad/kicad/${KICAD_VERSION_DIR}`;
|
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>;
|
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 = (
|
export type KicadLibsRequest = (
|
||||||
op: string,
|
op: string,
|
||||||
lib: string,
|
lib: string,
|
||||||
arg: string,
|
arg: string,
|
||||||
|
kind?: string,
|
||||||
) => Promise<string | null>;
|
) => Promise<string | null>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -89,10 +96,32 @@ export function buildSymLibTable(libsList: LibInfo[]): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install `window.kicadLibs` backed by a `LibsSource`. The plugin calls
|
* Build fp-lib-table content (KiCad v7) with one PCBJAM_FP row per lib. The
|
||||||
* `request(op, "/mnt/pcbjam[-rw]/<id>", arg)`:
|
* footprint editor selects the plugin from this row's `type` field (via
|
||||||
* "list" -> JSON {"symbols":[...]} (symbol names in the lib)
|
* PCB_IO_MGR::EnumFromStr), so it MUST be "PCBJAM_FP" to match the registered
|
||||||
* "get" -> the item body s-expr (arg = symbol name; null if absent)
|
* 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":..})
|
* "save" -> "ok" / null (arg = JSON {"name":..,"body":..})
|
||||||
*/
|
*/
|
||||||
export function installLibsProvider(
|
export function installLibsProvider(
|
||||||
|
|
@ -102,9 +131,9 @@ export function installLibsProvider(
|
||||||
if (window.kicadLibs) return;
|
if (window.kicadLibs) return;
|
||||||
const delay = artificialDelayMs();
|
const delay = artificialDelayMs();
|
||||||
|
|
||||||
const request: KicadLibsRequest = async (op, lib, arg) => {
|
const request: KicadLibsRequest = async (op, lib, arg, kind = "symbol") => {
|
||||||
const id = libIdFromUri(lib);
|
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 (!id) return null;
|
||||||
if (delay) await sleep(delay);
|
if (delay) await sleep(delay);
|
||||||
|
|
||||||
|
|
@ -112,13 +141,15 @@ export function installLibsProvider(
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case "list": {
|
case "list": {
|
||||||
const items = await source.listItems(id);
|
const items = await source.listItems(id);
|
||||||
const symbols = items
|
const names = items
|
||||||
.filter((i) => i.kind === "symbol")
|
.filter((i) => i.kind === kind)
|
||||||
.map((i) => i.name);
|
.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":
|
case "get":
|
||||||
return await source.getItemBody(id, "symbol", arg);
|
return await source.getItemBody(id, kind, arg);
|
||||||
case "save": {
|
case "save": {
|
||||||
let parsed: { name?: string; body?: string };
|
let parsed: { name?: string; body?: string };
|
||||||
try {
|
try {
|
||||||
|
|
@ -134,7 +165,7 @@ export function installLibsProvider(
|
||||||
}
|
}
|
||||||
const ok = await source.saveItemBody(
|
const ok = await source.saveItemBody(
|
||||||
id,
|
id,
|
||||||
"symbol",
|
kind,
|
||||||
parsed.name,
|
parsed.name,
|
||||||
parsed.body,
|
parsed.body,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,39 @@
|
||||||
import type { LibInfo, LibItemInfo, LibsSource } from "./source";
|
import type { LibInfo, LibItemInfo, LibsSource } from "./source";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 0004-A write-path spike (no backend). Wraps a `LibsSource` with ONE in-memory
|
* Write-path spike (no backend). Wraps a `LibsSource` with ONE in-memory writable
|
||||||
* writable user lib so the editor's save-symbol flow has a target and we can
|
* user lib so the editor's save flow has a target and we can exercise the full
|
||||||
* exercise the full save → enumerate → load round-trip through the same WASM
|
* save → enumerate → load round-trip through the same WASM bridge — without
|
||||||
* bridge — without committing to the backend design first. Gated by
|
* committing to the backend design first.
|
||||||
* `?libwrite=1`. Saved bodies are mirrored onto `window.__pcbjamSaved` so the
|
*
|
||||||
* Playwright probe can inspect them. Replaced in 0004-C by real remote writes.
|
* - 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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__pcbjamSaved?: Record<string, string>;
|
__pcbjamSaved?: Record<string, string>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withSpikeWritableLib(
|
interface SpikeLibSpec {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
kind: "symbol" | "footprint";
|
||||||
|
}
|
||||||
|
|
||||||
|
function withSpikeKindLib(
|
||||||
inner: LibsSource | null,
|
inner: LibsSource | null,
|
||||||
log: (msg: string) => void,
|
log: (msg: string) => void,
|
||||||
|
spec: SpikeLibSpec,
|
||||||
): LibsSource {
|
): LibsSource {
|
||||||
const store = new Map<string, string>(); // symbol name -> body
|
const store = new Map<string, string>(); // item name -> body
|
||||||
window.__pcbjamSaved = Object.create(null) as Record<string, string>;
|
// 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 {
|
return {
|
||||||
async listLibs(): Promise<LibInfo[]> {
|
async listLibs(): Promise<LibInfo[]> {
|
||||||
|
|
@ -37,15 +46,12 @@ export function withSpikeWritableLib(
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`[libs] spike: inner listLibs failed, origins omitted: ${String(e)}`);
|
log(`[libs] spike: inner listLibs failed, origins omitted: ${String(e)}`);
|
||||||
}
|
}
|
||||||
return [
|
return [...base, { id: spec.id, name: spec.name, type: "user" }];
|
||||||
...base,
|
|
||||||
{ id: SPIKE_RW_LIB_ID, name: SPIKE_RW_LIB_NAME, type: "user" },
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async listItems(libId: string): Promise<LibItemInfo[]> {
|
async listItems(libId: string): Promise<LibItemInfo[]> {
|
||||||
if (isSpike(libId))
|
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) : [];
|
return inner ? inner.listItems(libId) : [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -71,8 +77,32 @@ export function withSpikeWritableLib(
|
||||||
}
|
}
|
||||||
store.set(name, body);
|
store.set(name, body);
|
||||||
window.__pcbjamSaved![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;
|
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