diff --git a/debug/embedded.html b/debug/embedded.html index 90a1d7e9..cd4aff80 100644 --- a/debug/embedded.html +++ b/debug/embedded.html @@ -51,6 +51,18 @@ nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + (kicad_sch (version 20230121) (generator eeschema) (uuid + 5d5ad125-5ef1-42a1-a410-a0c4ab262ca6) (paper "A4") + (title_block (title "KiCanvas inline sources test") (date + "2023-11-10") ) (lib_symbols ) (text "KiCanvas inline + sources test" (at 93.98 104.14 0) (effects (font (size 5 5) + (thickness 1) bold) (justify left bottom)) (uuid + 27eb63d7-7111-4c0e-9985-c1ed90138e31) ) (sheet_instances + (path "/" (page "1")) ) ) + + diff --git a/docs/docs/embedding.md b/docs/docs/embedding.md index 61f56aa4..0a70a769 100644 --- a/docs/docs/embedding.md +++ b/docs/docs/embedding.md @@ -125,6 +125,7 @@ This example shows how to use `` along with inline KiCAD data. - `basic` - zoom, pan, and select are available. - `full` - complete interactive viewer, including side panels. - `controlslist` - further customizes the available controls. + - `nooverlay` - don't show the "click or tap to interact" overlay. - `nofullscreen` - don't show the fullscreen button. ⚠️ - `nodownload` - don't show the download button. @@ -136,7 +137,11 @@ This example shows how to use `` along with inline KiCAD data. - `noinfo` - don't show the document info panel. ⚠️ - `nopreferences` - don't show the user preferences panel. ⚠️ - `nohelp` - don't show the help panel. ⚠️ + - `src` - the URL of the document to embed. If you want to show multiple documents within a single viewer, you can use multiple child `` elements. +- `type` - the type of inline source. Available values include `sch` and `pcb`. When the `src` attribute is not empty and the inline source exists, the `src` attribute specified file will be loaded and be determined. Otherwise, the file type will be determined by this attribute. If this attribute is empty, the loader will try determined type by the first few characters. +- `originname` - the origin file name. Due to the KiCad dependence on the file name, when using an inline source specify it is a good choice (e.g. using the extern render for Gitea). The default name is `noname` when using the inline source, or file name in the URL when using the `src` attribute. + - `theme` - sets the color theme to use, valid values are `kicad` and `witchhazel`. ⚠️ - `zoom` - sets the initial view into the document. ⚠️ - `objects` - zooms to show all visible objects (default). ⚠️ diff --git a/src/base/dom/drag-drop.ts b/src/base/dom/drag-drop.ts index d79527d5..0a81d536 100644 --- a/src/base/dom/drag-drop.ts +++ b/src/base/dom/drag-drop.ts @@ -4,10 +4,8 @@ Full text available at: https://opensource.org/licenses/MIT */ -import { - DragAndDropFileSystem, - VirtualFileSystem, -} from "../../kicanvas/services/vfs"; +import VirtualFileSystem from "../../kicanvas/services/vfs"; +import DragAndDropFileSystem from "../../kicanvas/services/drop-vfs"; export class DropTarget { constructor(elm: HTMLElement, callback: (fs: VirtualFileSystem) => void) { diff --git a/src/kc-ui/focus-overlay.ts b/src/kc-ui/focus-overlay.ts index 82718437..8601b89b 100644 --- a/src/kc-ui/focus-overlay.ts +++ b/src/kc-ui/focus-overlay.ts @@ -94,7 +94,6 @@ export class KCUIFocusOverlay extends KCUIElement { this.#intersection_observer = new IntersectionObserver((entries) => { for (const entry of entries) { - console.log(entry); if (!entry.isIntersecting) { this.classList.remove("has-focus"); } diff --git a/src/kicanvas/elements/kicanvas-embed.ts b/src/kicanvas/elements/kicanvas-embed.ts index a25d8a83..a7e9d04c 100644 --- a/src/kicanvas/elements/kicanvas-embed.ts +++ b/src/kicanvas/elements/kicanvas-embed.ts @@ -15,12 +15,16 @@ 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 VirtualFileSystem from "../services/vfs"; +import FetchFileSystem, { FetchFileSource } from "../services/fetch-vfs"; import type { KCBoardAppElement } from "./kc-board/app"; import type { KCSchematicAppElement } from "./kc-schematic/app"; +import { Logger } from "../../base/log"; + +const log = new Logger("kicanvas:embedtag"); /** - * + * The `kicanvas-embed` label */ class KiCanvasEmbedElement extends KCUIElement { static override styles = [ @@ -96,22 +100,72 @@ class KiCanvasEmbedElement extends KCUIElement { async #setup_events() {} async #load_src() { - const sources = []; + const sources: FetchFileSource[] = []; if (this.src) { - sources.push(this.src); + sources.push(new FetchFileSource("uri", this.src)); } - for (const src_elm of this.querySelectorAll( - "kicanvas-source", - )) { + const sre_eles = + this.querySelectorAll("kicanvas-source"); + for (const src_elm of sre_eles) { if (src_elm.src) { - sources.push(src_elm.src); + // Append the source uri firstly + sources.push(new FetchFileSource("uri", src_elm.src)); + } else if (src_elm.childNodes.length > 0) { + let content = ""; + + for (const child of src_elm.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + // Get the content and triming the CR,LF,space. + content += child.nodeValue ?? ""; + } else { + log.warn( + "kicanvas-source children type is not vaild and that be skiped.", + ); + continue; + } + } + + content = content.trimStart(); + + // Determine the file extension name. + // That make `project.ts` determine the file type is possible. + let file_extname = ""; + if (src_elm.type) { + if (src_elm.type === "sch") { + file_extname = "kicad_sch"; + } else if (src_elm.type === "pcb") { + file_extname = "kicad_pcb"; + } else { + log.warn('Invaild value of attribute "type"'); + continue; + } + } else { + // "type" attribute is null, Try to determined the file type. + // sch: (kicad_sch .... + // pcb: (kicad_pcb .... + if (content.startsWith("(kicad_sch")) { + file_extname = "kicad_sch"; + } else if (content.startsWith("(kicad_pcb")) { + file_extname = "kicad_pcb"; + } else { + log.warn('Cannot determine the file "type"'); + continue; + } + } + const filename = src_elm.originname ?? `noname.${file_extname}`; + log.info(`Determined the inline source as "${filename}"`); + // append to the sources + sources.push(new FetchFileSource("content", content, filename)); + } else { + // That means this element is empty. + log.warn("kicanvas-source is empty."); } } if (sources.length == 0) { - console.warn("No valid sources specified"); + log.warn("No valid sources specified"); return; } @@ -180,6 +234,12 @@ class KiCanvasSourceElement extends CustomElement { @attribute({ type: String }) src: string | null; + + @attribute({ type: String }) + type: string | null; + + @attribute({ type: String }) + originname: string | null; } window.customElements.define("kicanvas-source", KiCanvasSourceElement); diff --git a/src/kicanvas/elements/kicanvas-shell.ts b/src/kicanvas/elements/kicanvas-shell.ts index 9c2bc355..35645ed6 100644 --- a/src/kicanvas/elements/kicanvas-shell.ts +++ b/src/kicanvas/elements/kicanvas-shell.ts @@ -12,7 +12,8 @@ import { sprites_url } from "../icons/sprites"; import { Project } from "../project"; import { GitHub } from "../services/github"; import { GitHubFileSystem } from "../services/github-vfs"; -import { FetchFileSystem, type VirtualFileSystem } from "../services/vfs"; +import FetchFileSystem, { FetchFileSource } from "../services/fetch-vfs"; +import type VirtualFileSystem from "../services/vfs"; import { KCBoardAppElement } from "./kc-board/app"; import { KCSchematicAppElement } from "./kc-schematic/app"; @@ -82,7 +83,9 @@ class KiCanvasShellElement extends KCUIElement { later(async () => { if (this.src) { - const vfs = new FetchFileSystem([this.src]); + const vfs = new FetchFileSystem([ + new FetchFileSource("uri", this.src), + ]); await this.setup_project(vfs); return; } diff --git a/src/kicanvas/project.ts b/src/kicanvas/project.ts index 164df91e..0da168b0 100644 --- a/src/kicanvas/project.ts +++ b/src/kicanvas/project.ts @@ -15,7 +15,7 @@ import type { SchematicSheet, SchematicSheetInstance, } from "../kicad/schematic"; -import type { VirtualFileSystem } from "./services/vfs"; +import type VirtualFileSystem from "./services/vfs"; const log = new Logger("kicanvas:project"); diff --git a/src/kicanvas/services/drop-vfs.ts b/src/kicanvas/services/drop-vfs.ts new file mode 100644 index 00000000..8c20eaa8 --- /dev/null +++ b/src/kicanvas/services/drop-vfs.ts @@ -0,0 +1,89 @@ +/* + Copyright (c) 2023 XiangYyang +*/ + +import { initiate_download } from "../../base/dom/download"; +import VirtualFileSystem from "./vfs"; + +/** + * Virtual file system for HTML drag and drop (DataTransfer) + */ +export default class DragAndDropFileSystem extends VirtualFileSystem { + constructor(private items: FileSystemFileEntry[]) { + super(); + } + + static async fromDataTransfer(dt: DataTransfer) { + let items: FileSystemEntry[] = []; + + // Pluck items out as webkit entries (either FileSystemFileEntry or + // FileSystemDirectoryEntry) + for (let i = 0; i < dt.items.length; i++) { + const item = dt.items[i]?.webkitGetAsEntry(); + if (item) { + items.push(item); + } + } + + // If it's just one directory then open it and set all of our items + // to its contents. + if (items.length == 1 && items[0]?.isDirectory) { + const reader = ( + items[0] as FileSystemDirectoryEntry + ).createReader(); + + items = []; + + await new Promise((resolve, reject) => { + reader.readEntries((entries) => { + for (const entry of entries) { + if (!entry.isFile) { + continue; + } + items.push(entry); + } + resolve(true); + }, reject); + }); + } + + return new DragAndDropFileSystem(items as FileSystemFileEntry[]); + } + + public override *list() { + for (const entry of this.items) { + yield entry.name; + } + } + + public override async has(name: string): Promise { + for (const entry of this.items) { + if (entry.name == name) { + return true; + } + } + return false; + } + + public override async get(name: string): Promise { + let file_entry: FileSystemFileEntry | null = null; + for (const entry of this.items) { + if (entry.name == name) { + file_entry = entry; + break; + } + } + + if (file_entry == null) { + throw new Error(`File ${name} not found!`); + } + + return await new Promise((resolve, reject) => { + file_entry!.file(resolve, reject); + }); + } + + public async download(name: string) { + initiate_download(await this.get(name)); + } +} diff --git a/src/kicanvas/services/fetch-vfs.ts b/src/kicanvas/services/fetch-vfs.ts new file mode 100644 index 00000000..4f6177b0 --- /dev/null +++ b/src/kicanvas/services/fetch-vfs.ts @@ -0,0 +1,123 @@ +/* + Copyright (c) 2023 XiangYyang +*/ + +import { initiate_download } from "../../base/dom/download"; +import { basename } from "../../base/paths"; +import VirtualFileSystem from "./vfs"; +import { Logger } from "../../base/log"; + +const log = new Logger("kicanvas:fetchfs"); + +/** + * File sources + */ +export class FetchFileSource { + constructor( + origin: "uri" | "content", + value: string, + name: string | undefined = undefined, + ) { + this.origin = origin; + if (this.origin === "uri") { + const url = new URL(value, window.location.toString()); + this.value = url; + this.origin_name = name ?? basename(url); + log.info( + `Load file from url ${url}, origin name: ${this.origin_name}`, + ); + } else { + this.value = value; + this.origin_name = name ?? "noname"; + log.info( + `Load file from inline source, origin name: ${this.origin_name}`, + ); + } + } + + /** + * Get the origin name of this item + */ + public get_origin_name(): string { + return this.origin_name; + } + + /** + * Get the file content from URL or inline sources + */ + public async get_content(): Promise { + if (this.origin === "uri") { + const url = this.value as URL; + + const request = new Request(url, { method: "GET" }); + + const response = await fetch(request); + + if (!response.ok) { + throw new Error( + `Unable to load ${url}: ${response.status} ${response.statusText}`, + ); + } + + const blob = await response.blob(); + + return new File([blob], this.origin_name); + } else { + const content = this.value as string; + const blob = new Blob([content], { type: "text/plain" }); + return new File([blob], this.origin_name); + } + } + + /** + * Origin, from URI or from raw content. + */ + private origin: "uri" | "content"; + + /** + * Value, URL (origin === "uri") or string (origin === "content") + */ + private value: URL | string; + + /** + * File name + */ + private origin_name: string; +} + +/** + * Virtual file system for URLs via Fetch or inline sources + */ +export default class FetchFileSystem extends VirtualFileSystem { + private items: Map = new Map(); + + constructor(items: FetchFileSource[]) { + super(); + + for (const item of items) { + this.items.set(item.get_origin_name(), item); + } + } + + public override *list() { + yield* this.items.keys(); + } + + public override async has(name: string) { + return Promise.resolve(this.items.has(name)); + } + + public override async get(name: string): Promise { + const item = this.items.get(name); + + if (!item) { + throw new Error(`File ${name} not found!`); + } + + return item.get_content(); + } + + public async download(name: string) { + initiate_download(await this.get(name)); + } +} diff --git a/src/kicanvas/services/github-vfs.ts b/src/kicanvas/services/github-vfs.ts index 9eb3e243..9f64426a 100644 --- a/src/kicanvas/services/github-vfs.ts +++ b/src/kicanvas/services/github-vfs.ts @@ -7,7 +7,7 @@ import { initiate_download } from "../../base/dom/download"; import { basename, dirname, extension } from "../../base/paths"; import { GitHub, GitHubUserContent } from "./github"; -import { VirtualFileSystem } from "./vfs"; +import VirtualFileSystem from "./vfs"; const kicad_extensions = ["kicad_pcb", "kicad_pro", "kicad_sch"]; const gh_user_content = new GitHubUserContent(); diff --git a/src/kicanvas/services/vfs.ts b/src/kicanvas/services/vfs.ts index 3710848e..e96e4b0a 100644 --- a/src/kicanvas/services/vfs.ts +++ b/src/kicanvas/services/vfs.ts @@ -4,9 +4,6 @@ Full text available at: https://opensource.org/licenses/MIT */ -import { initiate_download } from "../../base/dom/download"; -import { basename } from "../../base/paths"; - /** * Virtual file system abstract class. * @@ -14,7 +11,7 @@ import { basename } from "../../base/paths"; * It's implemented using Drag and Drop and GitHub to provide a common interface * for interacting and loading files. */ -export abstract class VirtualFileSystem { +export default abstract class VirtualFileSystem { public abstract list(): Generator; public abstract get(name: string): Promise; public abstract has(name: string): Promise; @@ -40,136 +37,3 @@ export abstract class VirtualFileSystem { } } } - -/** - * Virtual file system for URLs via Fetch - */ -export class FetchFileSystem extends VirtualFileSystem { - private urls: Map = new Map(); - - constructor(urls: (string | URL)[]) { - super(); - - for (const item of urls) { - const url = new URL(item, window.location.toString()); - const name = basename(url); - this.urls.set(name, url); - } - } - - public override *list() { - yield* this.urls.keys(); - } - - public override async has(name: string) { - return Promise.resolve(this.urls.has(name)); - } - - public override async get(name: string): Promise { - const url = this.urls.get(name); - - if (!url) { - throw new Error(`File ${name} not found!`); - } - - const request = new Request(url, { method: "GET" }); - const response = await fetch(request); - - if (!response.ok) { - throw new Error( - `Unable to load ${url}: ${response.status} ${response.statusText}`, - ); - } - - const blob = await response.blob(); - - return new File([blob], name); - } - - public async download(name: string) { - initiate_download(await this.get(name)); - } -} - -/** - * Virtual file system for HTML drag and drop (DataTransfer) - */ -export class DragAndDropFileSystem extends VirtualFileSystem { - constructor(private items: FileSystemFileEntry[]) { - super(); - } - - static async fromDataTransfer(dt: DataTransfer) { - let items: FileSystemEntry[] = []; - - // Pluck items out as webkit entries (either FileSystemFileEntry or - // FileSystemDirectoryEntry) - for (let i = 0; i < dt.items.length; i++) { - const item = dt.items[i]?.webkitGetAsEntry(); - if (item) { - items.push(item); - } - } - - // If it's just one directory then open it and set all of our items - // to its contents. - if (items.length == 1 && items[0]?.isDirectory) { - const reader = ( - items[0] as FileSystemDirectoryEntry - ).createReader(); - - items = []; - - await new Promise((resolve, reject) => { - reader.readEntries((entries) => { - for (const entry of entries) { - if (!entry.isFile) { - continue; - } - items.push(entry); - } - resolve(true); - }, reject); - }); - } - - return new DragAndDropFileSystem(items as FileSystemFileEntry[]); - } - - public override *list() { - for (const entry of this.items) { - yield entry.name; - } - } - - public override async has(name: string): Promise { - for (const entry of this.items) { - if (entry.name == name) { - return true; - } - } - return false; - } - - public override async get(name: string): Promise { - let file_entry: FileSystemFileEntry | null = null; - for (const entry of this.items) { - if (entry.name == name) { - file_entry = entry; - break; - } - } - - if (file_entry == null) { - throw new Error(`File ${name} not found!`); - } - - return await new Promise((resolve, reject) => { - file_entry!.file(resolve, reject); - }); - } - - public async download(name: string) { - initiate_download(await this.get(name)); - } -}