diff --git a/docs/docs/embedding.md b/docs/docs/embedding.md index 61f56aa4..c8d91b7b 100644 --- a/docs/docs/embedding.md +++ b/docs/docs/embedding.md @@ -98,22 +98,29 @@ You can switch between the displayed files using the project panel on the right ### Inline source -!!! warning "Not yet implemented" - - This functionality hasn't been implemented yet - -This example shows how to use `` along with inline KiCAD data. In this case, it's a symbol copied from a schematic and pasted into the HTML source: +You can embed KiCAD file contents directly inside ``. Provide either a `name` (filename) or a `type` hint so KiCanvas knows the file type. ```html + (lib_symbols (symbol "power:+12V" (power) (pin_names (offset 0)) (in_bom yes) (on_board yes) (property "Reference" "#PWR" (at 0 -3.81 0) (effects - (font (size 1.27 1.27)) hide) ) ... + (font (size 1.27 1.27)) hide))) + + + + + (kicad_sch (version 20211014) ...) ``` +Notes: + +- If `name` is omitted, a default name like `inline_1.kicad_sch` is assigned based on `type`. +- Inline sources can be mixed with URL sources; when both are provided, inline files override URL files with the same basename. + ## Attributes !!! warning "Not yet implemented" diff --git a/src/kicanvas/elements/kicanvas-embed.ts b/src/kicanvas/elements/kicanvas-embed.ts index f5f30549..cb8b1b60 100644 --- a/src/kicanvas/elements/kicanvas-embed.ts +++ b/src/kicanvas/elements/kicanvas-embed.ts @@ -15,7 +15,12 @@ import { import { KCUIElement } from "../../kc-ui"; import kc_ui_styles from "../../kc-ui/kc-ui.css"; import { Project } from "../project"; -import { FetchFileSystem, VirtualFileSystem } from "../services/vfs"; +import { + FetchFileSystem, + VirtualFileSystem, + MemoryFileSystem, + CombinedFileSystem, +} from "../services/vfs"; import type { KCBoardAppElement } from "./kc-board/app"; import type { KCSchematicAppElement } from "./kc-schematic/app"; @@ -98,27 +103,68 @@ class KiCanvasEmbedElement extends KCUIElement { async #setup_events() {} async #load_src() { - const sources = []; + const url_sources: (string | URL)[] = []; + const inline_entries: Record = {}; + const counters: Record = {}; if (this.src) { - sources.push(this.src); + url_sources.push(this.src); } for (const src_elm of this.querySelectorAll( "kicanvas-source", )) { if (src_elm.src) { - sources.push(src_elm.src); + url_sources.push(src_elm.src); + continue; + } + + const content = src_elm.textContent?.trim(); + if (!content) { + continue; } + + let filename = src_elm.name?.trim(); + if (!filename || filename.length == 0) { + const type = (src_elm.type ?? "schematic").toLowerCase(); + let ext = "kicad_sch"; + if (type == "board") { + ext = "kicad_pcb"; + } else if (type == "project") { + ext = "kicad_pro"; + } + counters[ext] = (counters[ext] ?? 0) + 1; + const index = counters[ext]; + filename = `inline_${index}.${ext}`; + } + + inline_entries[filename] = content; } - if (sources.length == 0) { + if ( + url_sources.length == 0 && + Object.keys(inline_entries).length == 0 + ) { console.warn("No valid sources specified"); return; } - const vfs = new FetchFileSystem(sources, this.custom_resolver); - await this.#setup_project(vfs); + // Construct VFS based on provided sources, then setup project once. + let vfs: VirtualFileSystem; + + // Prefer loading inline sources; combine with URLs if both provided. + if (Object.keys(inline_entries).length && url_sources.length) { + vfs = new CombinedFileSystem([ + new MemoryFileSystem(inline_entries), + new FetchFileSystem(url_sources, this.custom_resolver), + ]); + } else if (Object.keys(inline_entries).length) { + vfs = new MemoryFileSystem(inline_entries); + } else { + vfs = new FetchFileSystem(url_sources, this.custom_resolver); + } + + return await this.#setup_project(vfs); } async #setup_project(vfs: VirtualFileSystem) { @@ -182,6 +228,14 @@ class KiCanvasSourceElement extends CustomElement { @attribute({ type: String }) src: string | null; + + // Optional filename to use when providing inline content + @attribute({ type: String }) + name: string | null; + + // Hint for inline content type: schematic|board|project + @attribute({ type: String }) + type: "schematic" | "board" | "project" | null; } window.customElements.define("kicanvas-source", KiCanvasSourceElement); diff --git a/src/kicanvas/services/vfs.ts b/src/kicanvas/services/vfs.ts index 75948360..441e8cdf 100644 --- a/src/kicanvas/services/vfs.ts +++ b/src/kicanvas/services/vfs.ts @@ -197,3 +197,97 @@ export class DragAndDropFileSystem extends VirtualFileSystem { initiate_download(await this.get(name)); } } + +/** + * Virtual file system for in-memory files + */ +export class MemoryFileSystem extends VirtualFileSystem { + private files: Map = new Map(); + + constructor(entries: Record) { + super(); + + for (const [key, value] of Object.entries(entries ?? {})) { + const name = basename(key); + let file: File; + if (value instanceof File) { + file = value; + } else if (value instanceof Blob) { + file = new File([value], name); + } else if (value instanceof ArrayBuffer) { + file = new File([value], name); + } else { + // assume string + file = new File([value ?? ""], name, { type: "text/plain" }); + } + this.files.set(name, file); + } + } + + public override *list() { + yield* this.files.keys(); + } + + public override async has(name: string): Promise { + if (this.files.has(name)) { + return true; + } + const base = basename(name); + return this.files.has(base); + } + + public override async get(name: string): Promise { + const file = this.files.get(name) ?? this.files.get(basename(name)); + if (!file) { + throw new Error(`File ${name} not found!`); + } + return file; + } + + public async download(name: string) { + initiate_download(await this.get(name)); + } +} + +/** + * Combines multiple VFS backends. The first VFS has priority when names collide. + */ +export class CombinedFileSystem extends VirtualFileSystem { + constructor(private file_systems: VirtualFileSystem[]) { + super(); + } + + public override *list() { + const seen = new Set(); + for (const fs of this.file_systems) { + for (const name of fs.list()) { + if (!seen.has(name)) { + seen.add(name); + yield name; + } + } + } + } + + public override async has(name: string): Promise { + for (const fs of this.file_systems) { + if (await fs.has(name)) { + return true; + } + } + return false; + } + + public override async get(name: string): Promise { + for (const fs of this.file_systems) { + if (await fs.has(name)) { + return fs.get(name); + } + } + throw new Error(`File ${name} not found!`); + } + + public async download(name: string) { + initiate_download(await this.get(name)); + } +}