feat: open library files + lib-scoped editors; fix wx-dom.js boot
Open a single library scoped to itself in its editor: a backend lib (HomePage chip → scopedLibsSource) or a local .kicad_sym/.kicad_mod file (LocalProjectView → localFileLibsSource + a browser-side multi-symbol .kicad_sym parser). WasmTool gains an optional libsSource override (transparent default). All via the existing lib bridge — no MEMFS doc open, no fork changes. Also fix a systemic editor boot failure: the build split the wx glue into wx.js + wx-dom.js, but boot.ts only injected wx.js + <tool>.js. wx-dom.js defines window.wxDomCreateControl (used by every tool to build its UI), so without it the canvas rendered but no wx controls did (ReferenceError: wxDomCreateControl is not defined). Inject wx-dom.js between wx.js and the tool, best-effort (older non-DOM builds tolerated). Bump pcbjam-shared for LIB_EXTENSION_TOOL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b0b7f69846
commit
4b404f3771
7 changed files with 326 additions and 9 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit f21b6ea006bcfb8bd92c2d6d2afa42504db6bda4
|
Subproject commit 43b28a47e7df8be7e659f9512eb8afa860719df5
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import {
|
import {
|
||||||
EXTENSION_TOOL,
|
EXTENSION_TOOL,
|
||||||
FILELESS_TOOLS,
|
FILELESS_TOOLS,
|
||||||
|
LIB_EXTENSION_TOOL,
|
||||||
TOOL_LABELS,
|
TOOL_LABELS,
|
||||||
type Tool,
|
type Tool,
|
||||||
} from "@pcbjam/shared";
|
} from "@pcbjam/shared";
|
||||||
import { ArrowLeft, ExternalLink, FolderOpen } from "lucide-react";
|
import { ArrowLeft, ExternalLink, FolderOpen, Library } from "lucide-react";
|
||||||
import { formatBytes } from "@/lib/utils";
|
import { formatBytes } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
|
@ -13,10 +14,18 @@ export interface LocalFile {
|
||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toolForPath(path: string): Tool | null {
|
function ext(path: string): string {
|
||||||
const dot = path.lastIndexOf(".");
|
const dot = path.lastIndexOf(".");
|
||||||
if (dot < 0) return null;
|
return dot < 0 ? "" : path.slice(dot).toLowerCase();
|
||||||
return EXTENSION_TOOL[path.slice(dot).toLowerCase()] ?? null;
|
}
|
||||||
|
|
||||||
|
function toolForPath(path: string): Tool | null {
|
||||||
|
return EXTENSION_TOOL[ext(path)] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A library file (.kicad_sym / .kicad_mod) → the editor that opens it scoped. */
|
||||||
|
function libToolForPath(path: string): Tool | null {
|
||||||
|
return LIB_EXTENSION_TOOL[ext(path)] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,12 +40,15 @@ export function LocalProjectView({
|
||||||
name,
|
name,
|
||||||
files,
|
files,
|
||||||
onOpen,
|
onOpen,
|
||||||
|
onOpenLib,
|
||||||
onBack,
|
onBack,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
files: LocalFile[];
|
files: LocalFile[];
|
||||||
/** Launch a tool; `path` is undefined for file-less tools. */
|
/** Launch a tool; `path` is undefined for file-less tools. */
|
||||||
onOpen: (tool: Tool, path?: string) => void;
|
onOpen: (tool: Tool, path?: string) => void;
|
||||||
|
/** Open a local library FILE (.kicad_sym/.kicad_mod) scoped in its editor. */
|
||||||
|
onOpenLib: (tool: Tool, path: string) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -70,6 +82,7 @@ export function LocalProjectView({
|
||||||
<div className="divide-y rounded-lg border">
|
<div className="divide-y rounded-lg border">
|
||||||
{files.map((f) => {
|
{files.map((f) => {
|
||||||
const tool = toolForPath(f.path);
|
const tool = toolForPath(f.path);
|
||||||
|
const libTool = tool ? null : libToolForPath(f.path);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={f.path}
|
key={f.path}
|
||||||
|
|
@ -89,6 +102,14 @@ export function LocalProjectView({
|
||||||
<ExternalLink size={14} /> Open in {TOOL_LABELS[tool]}
|
<ExternalLink size={14} /> Open in {TOOL_LABELS[tool]}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{libTool && (
|
||||||
|
<button
|
||||||
|
className="inline-flex shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-sm hover:bg-accent"
|
||||||
|
onClick={() => onOpenLib(libTool, f.path)}
|
||||||
|
>
|
||||||
|
<Library size={14} /> Open in {TOOL_LABELS[libTool]}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
type DocSource,
|
type DocSource,
|
||||||
} from "@/lib/config";
|
} from "@/lib/config";
|
||||||
import { bootKicadTool } from "@/wasm/boot";
|
import { bootKicadTool } from "@/wasm/boot";
|
||||||
|
import type { LibsSource } from "@/wasm/libs/source";
|
||||||
import { memfsFilePath, memfsProjectDir } from "@/wasm/constants";
|
import { memfsFilePath, memfsProjectDir } from "@/wasm/constants";
|
||||||
import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner";
|
import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner";
|
||||||
import { registerSaveHook, type SaveBytes } from "@/wasm/save-flow";
|
import { registerSaveHook, type SaveBytes } from "@/wasm/save-flow";
|
||||||
|
|
@ -348,6 +349,7 @@ export function WasmTool({
|
||||||
saveBytes,
|
saveBytes,
|
||||||
docSource,
|
docSource,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
|
libsSource,
|
||||||
}: {
|
}: {
|
||||||
tool: Tool;
|
tool: Tool;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|
@ -355,6 +357,12 @@ export function WasmTool({
|
||||||
projectId: string;
|
projectId: string;
|
||||||
files: ToolFile[];
|
files: ToolFile[];
|
||||||
targetPath?: string;
|
targetPath?: string;
|
||||||
|
/**
|
||||||
|
* Override the library source the editor browses. Omitted ⇒ the configured
|
||||||
|
* default (`libsSourceConfig`). Used to open a single library scoped to itself
|
||||||
|
* — a specific backend lib, or a local `.kicad_sym`/`.kicad_mod` file.
|
||||||
|
*/
|
||||||
|
libsSource?: LibsSource | null;
|
||||||
/** Fetch one project-relative file's bytes (contract loader or local folder). */
|
/** Fetch one project-relative file's bytes (contract loader or local folder). */
|
||||||
fetchBytes: (relPath: string) => Promise<Uint8Array>;
|
fetchBytes: (relPath: string) => Promise<Uint8Array>;
|
||||||
/**
|
/**
|
||||||
|
|
@ -442,7 +450,8 @@ export function WasmTool({
|
||||||
log: append,
|
log: append,
|
||||||
onStatus: setStatus,
|
onStatus: setStatus,
|
||||||
onAbort: oom.onAbort,
|
onAbort: oom.onAbort,
|
||||||
libsSource: libsSourceConfig(projectId),
|
libsSource:
|
||||||
|
libsSource !== undefined ? libsSource : libsSourceConfig(projectId),
|
||||||
});
|
});
|
||||||
// Register the save sink before the file opens: from here on, every
|
// Register the save sink before the file opens: from here on, every
|
||||||
// editor File→Save (MEMFS write) is routed onward through saveBytes.
|
// editor File→Save (MEMFS write) is routed onward through saveBytes.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { Link } from "react-router-dom";
|
||||||
import type { Lib, Tool } from "@pcbjam/shared";
|
import type { Lib, Tool } from "@pcbjam/shared";
|
||||||
import { FolderOpen, Library, Loader2, Package } from "lucide-react";
|
import { FolderOpen, Library, Loader2, Package } from "lucide-react";
|
||||||
import { useLibs, useProjects } from "@/lib/api";
|
import { useLibs, useProjects } from "@/lib/api";
|
||||||
|
import { libsSourceConfig } from "@/lib/config";
|
||||||
|
import { scopedLibsSource } from "@/wasm/libs/scoped-source";
|
||||||
|
import { localFileLibsSource } from "@/wasm/libs/local-file-source";
|
||||||
|
import type { LibsSource } from "@/wasm/libs/source";
|
||||||
import { downloadBytes } from "@/lib/download";
|
import { downloadBytes } from "@/lib/download";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ToolGrid } from "@/components/ToolGrid";
|
import { ToolGrid } from "@/components/ToolGrid";
|
||||||
|
|
@ -110,7 +114,21 @@ export function HomePage() {
|
||||||
// A tool launched straight from the home page — no local folder, no backend
|
// A tool launched straight from the home page — no local folder, no backend
|
||||||
// project. File-less editors browse backend libraries; document editors
|
// project. File-less editors browse backend libraries; document editors
|
||||||
// (schematic/PCB/drawing sheet) boot to a blank document (KiCad-launcher style).
|
// (schematic/PCB/drawing sheet) boot to a blank document (KiCad-launcher style).
|
||||||
const [launchedTool, setLaunchedTool] = React.useState<{ tool: Tool } | null>(null);
|
// `libsSource`, when set, scopes the editor to ONE library (a backend lib, or a
|
||||||
|
// local lib file); omitted ⇒ the editor's configured default source.
|
||||||
|
const [launchedTool, setLaunchedTool] = React.useState<{
|
||||||
|
tool: Tool;
|
||||||
|
libsSource?: LibsSource | null;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
/** Open a backend library scoped to itself in its matching editor. */
|
||||||
|
const openScopedLib = (tool: Tool, lib: Lib) => {
|
||||||
|
const base = libsSourceConfig("local");
|
||||||
|
setLaunchedTool({
|
||||||
|
tool,
|
||||||
|
libsSource: base ? scopedLibsSource(base, lib.id) : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// <input webkitdirectory> is non-standard; set it imperatively.
|
// <input webkitdirectory> is non-standard; set it imperatively.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -128,6 +146,7 @@ export function HomePage() {
|
||||||
slug="local"
|
slug="local"
|
||||||
projectId="local"
|
projectId="local"
|
||||||
files={[]}
|
files={[]}
|
||||||
|
libsSource={launchedTool.libsSource}
|
||||||
fetchBytes={async (p) => {
|
fetchBytes={async (p) => {
|
||||||
throw new Error(`no project file to fetch: ${p}`);
|
throw new Error(`no project file to fetch: ${p}`);
|
||||||
}}
|
}}
|
||||||
|
|
@ -158,6 +177,21 @@ export function HomePage() {
|
||||||
name={local.name}
|
name={local.name}
|
||||||
files={local.files}
|
files={local.files}
|
||||||
onOpen={(tool, path) => setLaunched({ tool, target: path })}
|
onOpen={(tool, path) => setLaunched({ tool, target: path })}
|
||||||
|
onOpenLib={(tool, path) => {
|
||||||
|
void (async () => {
|
||||||
|
const bytes = await local.fetchBytes(path);
|
||||||
|
const text = new TextDecoder().decode(bytes);
|
||||||
|
const id = (path.split("/").pop() ?? path).replace(
|
||||||
|
/\.(kicad_sym|kicad_mod)$/i,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const kind = tool === "footprint_editor" ? "footprint" : "symbol";
|
||||||
|
setLaunchedTool({
|
||||||
|
tool,
|
||||||
|
libsSource: localFileLibsSource(id, text, kind),
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}}
|
||||||
onBack={() => setLocal(null)}
|
onBack={() => setLocal(null)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -265,13 +299,13 @@ export function HomePage() {
|
||||||
icon={<Library size={16} />}
|
icon={<Library size={16} />}
|
||||||
label="Symbols"
|
label="Symbols"
|
||||||
query={symbolLibs}
|
query={symbolLibs}
|
||||||
onOpen={() => setLaunchedTool({ tool: "symbol_editor" })}
|
onOpen={(lib) => openScopedLib("symbol_editor", lib)}
|
||||||
/>
|
/>
|
||||||
<LibGroup
|
<LibGroup
|
||||||
icon={<Package size={16} />}
|
icon={<Package size={16} />}
|
||||||
label="Footprints"
|
label="Footprints"
|
||||||
query={footprintLibs}
|
query={footprintLibs}
|
||||||
onOpen={() => setLaunchedTool({ tool: "footprint_editor" })}
|
onOpen={(lib) => openScopedLib("footprint_editor", lib)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
162
web/standalone/src/wasm/libs/kicad-sym-parse.ts
Normal file
162
web/standalone/src/wasm/libs/kicad-sym-parse.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* Browser-side parser for a single-file multi-symbol `.kicad_sym` library, so a
|
||||||
|
* local file can be served to the symbol editor as a one-lib `LibsSource`.
|
||||||
|
*
|
||||||
|
* Ported from the GPL backend's `extract/kicad-symdir.ts` (string-scanning only,
|
||||||
|
* no deps). The backend reads the unpacked one-symbol-per-file layout; here a
|
||||||
|
* single `.kicad_sym` holds the whole lib, so we enumerate ALL top-level
|
||||||
|
* `(symbol …)` blocks and resolve `extends` against siblings in the same file.
|
||||||
|
* Bodies are emitted self-contained + version-capped, exactly like the bridge
|
||||||
|
* expects (the editor's plugin parses one `kicad_symbol_lib` per item).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Max symbol-lib format version the WASM fork parses (mirror the backend cap).
|
||||||
|
const FORK_MAX_LIB_VERSION = 20250925;
|
||||||
|
// Symbol-level leaf tokens the fork's parser can't handle (lossless to drop).
|
||||||
|
const FORK_UNSUPPORTED_SYMBOL_TOKENS = ["in_pos_files", "embedded_fonts"];
|
||||||
|
const UNSUPPORTED_RE = new RegExp(
|
||||||
|
`^[\\t ]*\\(\\s*(?:${FORK_UNSUPPORTED_SYMBOL_TOKENS.join("|")})\\s+[^()]*\\)\\s*\\n?`,
|
||||||
|
"gm",
|
||||||
|
);
|
||||||
|
|
||||||
|
const unesc = (s: string): string => s.replace(/\\(.)/g, "$1");
|
||||||
|
|
||||||
|
/** Skip from `i` (just after an opening quote) to the index past the close. */
|
||||||
|
function endOfString(src: string, i: number): number {
|
||||||
|
while (i < src.length) {
|
||||||
|
const c = src[i];
|
||||||
|
if (c === "\\") {
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '"') return i + 1;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [start, end) of the balanced block beginning at `open` (a `(`), quote-aware. */
|
||||||
|
function matchParen(src: string, open: number): [number, number] {
|
||||||
|
let depth = 0;
|
||||||
|
let i = open;
|
||||||
|
while (i < src.length) {
|
||||||
|
const c = src[i];
|
||||||
|
if (c === '"') {
|
||||||
|
i = endOfString(src, i + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "(") depth += 1;
|
||||||
|
else if (c === ")") {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) return [open, i + 1];
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
throw new Error("unbalanced parentheses in s-expr");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Direct children (depth-1 forms) of the outermost `(kicad_symbol_lib …)`. */
|
||||||
|
function* topChildren(src: string): Generator<string> {
|
||||||
|
const open = src.indexOf("(");
|
||||||
|
if (open < 0) return;
|
||||||
|
let i = src.indexOf("(", open + 1);
|
||||||
|
while (i >= 0) {
|
||||||
|
const [s, e] = matchParen(src, i);
|
||||||
|
yield src.slice(s, e);
|
||||||
|
i = src.indexOf("(", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function symbolName(block: string): string | null {
|
||||||
|
const m = block.match(/^\(\s*symbol\s+"((?:[^"\\]|\\.)*)"/);
|
||||||
|
return m ? unesc(m[1]!) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Direct child `(extends "Parent")` of a symbol block, or null. */
|
||||||
|
function symbolExtends(block: string): string | null {
|
||||||
|
let i = block.indexOf("(", 1);
|
||||||
|
while (i >= 0 && i < block.length) {
|
||||||
|
const [s, e] = matchParen(block, i);
|
||||||
|
const form = block.slice(s, e);
|
||||||
|
const em = form.match(/^\(\s*extends\s+"((?:[^"\\]|\\.)*)"/);
|
||||||
|
if (em) return unesc(em[1]!);
|
||||||
|
i = block.indexOf("(", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The `kicad_symbol_lib` header forms (version/generator), before any symbol. */
|
||||||
|
function libHeader(src: string): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const form of topChildren(src)) {
|
||||||
|
if (/^\(\s*symbol\b/.test(form)) break;
|
||||||
|
parts.push(form);
|
||||||
|
}
|
||||||
|
return parts.join("\n\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
function capVersion(header: string): string {
|
||||||
|
return header.replace(/\(\s*version\s+(\d+)\s*\)/, (m, v) =>
|
||||||
|
Number(v) > FORK_MAX_LIB_VERSION ? `(version ${FORK_MAX_LIB_VERSION})` : m,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitize = (block: string): string => block.replace(UNSUPPORTED_RE, "");
|
||||||
|
|
||||||
|
function buildSelfContainedLib(
|
||||||
|
header: string,
|
||||||
|
parentBlocks: string[],
|
||||||
|
primaryBlock: string,
|
||||||
|
): string {
|
||||||
|
const body = [...parentBlocks, primaryBlock]
|
||||||
|
.map((b) => sanitize(b).replace(/^/gm, "\t").trimStart())
|
||||||
|
.join("\n\t");
|
||||||
|
return `(kicad_symbol_lib\n\t${capVersion(header)}\n\t${body}\n)\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedSymLib {
|
||||||
|
/** All top-level symbol names, file order. */
|
||||||
|
names: string[];
|
||||||
|
/** Self-contained single-symbol body (parents bundled), or null if absent. */
|
||||||
|
bodyFor(name: string): string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse a single-file `.kicad_sym` lib into per-symbol self-contained bodies. */
|
||||||
|
export function parseKicadSymLib(src: string): ParsedSymLib {
|
||||||
|
const header = libHeader(src);
|
||||||
|
const blocks = new Map<string, string>();
|
||||||
|
const exts = new Map<string, string | null>();
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const form of topChildren(src)) {
|
||||||
|
if (!/^\(\s*symbol\b/.test(form)) continue;
|
||||||
|
const name = symbolName(form);
|
||||||
|
if (!name) continue;
|
||||||
|
names.push(name);
|
||||||
|
blocks.set(name, form);
|
||||||
|
exts.set(name, symbolExtends(form));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveChain = (name: string): string[] => {
|
||||||
|
const chain: string[] = [];
|
||||||
|
const seen = new Set<string>([name]);
|
||||||
|
let parent = exts.get(name) ?? null;
|
||||||
|
while (parent) {
|
||||||
|
if (seen.has(parent)) break; // cycle guard
|
||||||
|
seen.add(parent);
|
||||||
|
const pb = blocks.get(parent);
|
||||||
|
if (!pb) break; // parent not in this file — emit what we have
|
||||||
|
chain.unshift(pb);
|
||||||
|
parent = exts.get(parent) ?? null;
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
names,
|
||||||
|
bodyFor(name: string): string | null {
|
||||||
|
const block = blocks.get(name);
|
||||||
|
if (!block) return null;
|
||||||
|
return buildSelfContainedLib(header, resolveChain(name), block);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
66
web/standalone/src/wasm/libs/local-file-source.ts
Normal file
66
web/standalone/src/wasm/libs/local-file-source.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type { LibInfo, LibItemInfo, LibsSource } from "./source";
|
||||||
|
import { parseKicadSymLib } from "./kicad-sym-parse";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A one-lib `LibsSource` over a single local library FILE (a `.kicad_sym` symbol
|
||||||
|
* lib, or a `.kicad_mod` footprint) the user picked from a folder. The editor
|
||||||
|
* browses it scoped to itself — same bridge path as a backend lib, no MEMFS doc
|
||||||
|
* open. Read-only (local files aren't written back through the lib bridge).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Max board/footprint file-format version the WASM fork parses (mirror backend).
|
||||||
|
const FORK_MAX_BOARD_VERSION = 20251028;
|
||||||
|
|
||||||
|
function capFootprintVersion(body: string): string {
|
||||||
|
return body.replace(/\(\s*version\s+(\d+)\s*\)/, (m, v) =>
|
||||||
|
Number(v) > FORK_MAX_BOARD_VERSION ? `(version ${FORK_MAX_BOARD_VERSION})` : m,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function footprintName(text: string, fallback: string): string {
|
||||||
|
const m = text.match(/\(\s*footprint\s+"((?:[^"\\]|\\.)*)"/);
|
||||||
|
return m ? m[1]!.replace(/\\(.)/g, "$1") : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localFileLibsSource(
|
||||||
|
libId: string,
|
||||||
|
text: string,
|
||||||
|
kind: "symbol" | "footprint",
|
||||||
|
): LibsSource {
|
||||||
|
const lib: LibInfo = {
|
||||||
|
id: libId,
|
||||||
|
name: libId,
|
||||||
|
description: `Local ${kind} library (${libId})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kind === "footprint") {
|
||||||
|
const name = footprintName(text, libId);
|
||||||
|
const body = capFootprintVersion(text);
|
||||||
|
return {
|
||||||
|
async listLibs(k?: string): Promise<LibInfo[]> {
|
||||||
|
return !k || k === "footprint" ? [lib] : [];
|
||||||
|
},
|
||||||
|
async listItems(id: string): Promise<LibItemInfo[]> {
|
||||||
|
return id === libId ? [{ kind: "footprint", name }] : [];
|
||||||
|
},
|
||||||
|
async getItemBody(id, k, n): Promise<string | null> {
|
||||||
|
return id === libId && k === "footprint" && n === name ? body : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseKicadSymLib(text);
|
||||||
|
return {
|
||||||
|
async listLibs(k?: string): Promise<LibInfo[]> {
|
||||||
|
return !k || k === "symbol" ? [lib] : [];
|
||||||
|
},
|
||||||
|
async listItems(id: string): Promise<LibItemInfo[]> {
|
||||||
|
return id === libId
|
||||||
|
? parsed.names.map((name) => ({ kind: "symbol", name }))
|
||||||
|
: [];
|
||||||
|
},
|
||||||
|
async getItemBody(id, k, n): Promise<string | null> {
|
||||||
|
return id === libId && k === "symbol" ? parsed.bodyFor(n) : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
25
web/standalone/src/wasm/libs/scoped-source.ts
Normal file
25
web/standalone/src/wasm/libs/scoped-source.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { LibInfo, LibItemInfo, LibsSource } from "./source";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a `LibsSource` so the editor sees exactly ONE library — the lib-table
|
||||||
|
* gets a single row, instead of "browse all backend libs". Used to open a
|
||||||
|
* specific backend library scoped to itself (HomePage → a lib chip).
|
||||||
|
*
|
||||||
|
* Read-only scope: `listItems`/`getItemBody` forward to the base for the target
|
||||||
|
* lib; no `createLib`/`saveItemBody` is exposed (the editor opens this origin to
|
||||||
|
* browse, not to write — matching today's read-only-origin behavior).
|
||||||
|
*/
|
||||||
|
export function scopedLibsSource(base: LibsSource, libId: string): LibsSource {
|
||||||
|
return {
|
||||||
|
async listLibs(kind?: string): Promise<LibInfo[]> {
|
||||||
|
const all = await base.listLibs(kind);
|
||||||
|
return all.filter((l) => l.id === libId);
|
||||||
|
},
|
||||||
|
listItems(id: string): Promise<LibItemInfo[]> {
|
||||||
|
return base.listItems(id);
|
||||||
|
},
|
||||||
|
getItemBody(id: string, kind: string, name: string): Promise<string | null> {
|
||||||
|
return base.getItemBody(id, kind, name);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue