Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions docs/docs/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<kicanvas-source>` 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 `<kicanvas-source>`. Provide either a `name` (filename) or a `type` hint so KiCanvas knows the file type.

```html
<kicanvas-embed>
<!-- With a type hint (defaults: schematic -> .kicad_sch, board -> .kicad_pcb, project -> .kicad_pro) -->
<kicanvas-source type="schematic">
(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)))
</kicanvas-source>

<!-- Or explicitly name the file to match hierarchical references -->
<kicanvas-source name="child.kicad_sch">
(kicad_sch (version 20211014) ...)
</kicanvas-source>
</kicanvas-embed>
```

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"
Expand Down
70 changes: 63 additions & 7 deletions src/kicanvas/elements/kicanvas-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -98,27 +103,70 @@ class KiCanvasEmbedElement extends KCUIElement {
async #setup_events() {}

async #load_src() {
const sources = [];
const url_sources: (string | URL)[] = [];
const inline_entries: Record<string, string> = {};
const counters: Record<string, number> = {};

if (this.src) {
sources.push(this.src);
url_sources.push(this.src);
}

for (const src_elm of this.querySelectorAll<KiCanvasSourceElement>(
"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();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because querySelectorAll is used, the custom element property is not reliably accessible and may return undefined at runtime. To address this problem, getAttribute should be used on the returned element.

I've changed this to src_elm.getAttribute('name')?.trim() in my local copy to resolve the issue.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting - I'm wondering if this is executing before the element has finished being registered?

if (!filename || filename.length == 0) {
const type = (src_elm.type ?? "schematic").toLowerCase();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to the comment above I've changed this to src_elm.getAttribute('type') in my local copy to resolve the issue.

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);
// Prefer loading inline sources; combine with URLs if both provided.
if (Object.keys(inline_entries).length && url_sources.length) {
const vfs = new CombinedFileSystem([
new MemoryFileSystem(inline_entries),
new FetchFileSystem(url_sources, this.custom_resolver),
]);
await this.#setup_project(vfs);
return;
}

if (Object.keys(inline_entries).length) {
await this.#setup_project(new MemoryFileSystem(inline_entries));
return;
}

await this.#setup_project(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of three separate invocations of await this.#setup_project, have the preceding code construct a vfs and then have the last statement be:

return await #this.setup_project(vfs);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, done!

new FetchFileSystem(url_sources, this.custom_resolver),
);
}

async #setup_project(vfs: VirtualFileSystem) {
Expand Down Expand Up @@ -182,6 +230,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);
Expand Down
94 changes: 94 additions & 0 deletions src/kicanvas/services/vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, File> = new Map();

constructor(entries: Record<string, string | Blob | ArrayBuffer | File>) {
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<boolean> {
if (this.files.has(name)) {
return true;
}
const base = basename(name);
return this.files.has(base);
}

public override async get(name: string): Promise<File> {
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<string>();
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<boolean> {
for (const fs of this.file_systems) {
if (await fs.has(name)) {
return true;
}
}
return false;
}

public override async get(name: string): Promise<File> {
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));
}
}