AES-256 encryption option for project files, progress on importing KiCad symbols
This commit is contained in:
parent
41939fb47c
commit
0a0f9ff9fd
32 changed files with 493 additions and 394 deletions
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ── App entry point ───────────────────────────────────────────
|
||||
import { autoImportDeviceLib } from './kicad.js';
|
||||
import { S, NET_COLORS, initCanvas } from './state.js';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// COMPONENT LIBRARY
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// schema/db.js — Database layer for Schematic ReTrace
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { S, canvas, ctx, w2s, getNetColor, GRID } from './state.js';
|
||||
import { createComp } from './components.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ── Undo history ─────────────────────────────────────────────
|
||||
// Kept separate to avoid circular imports between interaction.js and ui.js.
|
||||
import { S } from './state.js';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ── Interaction ───────────────────────────────────────────────
|
||||
import { db } from './db.js';
|
||||
import { S, canvas, s2w, snap } from './state.js';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ── Exports ───────────────────────────────────────────────────
|
||||
import { S } from './state.js';
|
||||
import { createComp } from './components.js';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { db, uuid } from './db.js';
|
||||
|
||||
const SCALE = 20 / 2.54;
|
||||
|
|
@ -154,27 +148,66 @@ async function upsertParsedSymbols(parsedSymbols, libName) {
|
|||
|
||||
// ── Public Importers ──────────────────────────────────────────────
|
||||
|
||||
export async function importSelectedFromZip(zip, filenames) {
|
||||
let stats = { inserted: 0, updated: 0 };
|
||||
const filesByLib = {};
|
||||
for (const filename of filenames) {
|
||||
const match = filename.match(/([^\/]+)\.kicad_symdir\//);
|
||||
const libName = match ? match[1] : 'Imported';
|
||||
if (!filesByLib[libName]) filesByLib[libName] = [];
|
||||
filesByLib[libName].push(filename);
|
||||
}
|
||||
export async function importSelectedFromZip({ reader, entries }, filenames, onProgress) {
|
||||
const notify = (msg) => { if (onProgress) onProgress(msg); };
|
||||
const { TextWriter } = zip;
|
||||
const entryMap = new Map(entries.map(e => [e.filename, e]));
|
||||
const total = filenames.length;
|
||||
|
||||
for (const [libName, files] of Object.entries(filesByLib)) {
|
||||
notify(`Decompressing 0/${total} files…`);
|
||||
|
||||
// Phase 1: decompression
|
||||
let done = 0;
|
||||
zip.configure({ /*useWebWorkers: false, */ useCompressionStream: true });
|
||||
// console.time('[kicad] decompress');
|
||||
const results = await Promise.all(
|
||||
filenames.map(async filename => {
|
||||
const entry = entryMap.get(filename);
|
||||
if (!entry) return null;
|
||||
const text = await entry.getData(new TextWriter());
|
||||
notify(`Decompressing ${++done}/${total} files…`);
|
||||
return { filename, text };
|
||||
})
|
||||
);
|
||||
// console.timeEnd('[kicad] decompress');
|
||||
|
||||
// Phase 2: parsing
|
||||
notify(`Parsing symbols…`);
|
||||
// console.time('[kicad] parse');
|
||||
const textsByLib = {};
|
||||
for (const result of results) {
|
||||
if (!result) continue;
|
||||
const match = result.filename.match(/([^\/]+)\.kicad_symdir\//);
|
||||
const libName = match ? match[1] : 'Imported';
|
||||
if (!textsByLib[libName]) textsByLib[libName] = [];
|
||||
textsByLib[libName].push(result.text);
|
||||
}
|
||||
const parsedByLib = {};
|
||||
let totalSyms = 0;
|
||||
for (const [libName, fileTexts] of Object.entries(textsByLib)) {
|
||||
const parsedSymbols = {};
|
||||
for (const filename of files) {
|
||||
const text = await zip.files[filename].async("text");
|
||||
extractTopLevelSymbols(parseSexpr(text), parsedSymbols);
|
||||
}
|
||||
for (const text of fileTexts) extractTopLevelSymbols(parseSexpr(text), parsedSymbols);
|
||||
resolveInheritance(parsedSymbols);
|
||||
parsedByLib[libName] = parsedSymbols;
|
||||
totalSyms += Object.keys(parsedSymbols).length;
|
||||
}
|
||||
// console.timeEnd('[kicad] parse');
|
||||
|
||||
// Phase 3: DB writes
|
||||
notify(`Writing ${totalSyms} symbols to database…`);
|
||||
// console.time('[kicad] db-write');
|
||||
let stats = { inserted: 0, updated: 0 };
|
||||
const libs = Object.entries(parsedByLib);
|
||||
for (let i = 0; i < libs.length; i++) {
|
||||
const [libName, parsedSymbols] = libs[i];
|
||||
notify(`Writing ${libName} (${i + 1}/${libs.length} libraries)…`);
|
||||
const res = await upsertParsedSymbols(parsedSymbols, libName);
|
||||
stats.inserted += res.inserted;
|
||||
stats.updated += res.updated;
|
||||
}
|
||||
// console.timeEnd('[kicad] db-write');
|
||||
|
||||
await reader.close();
|
||||
return stats;
|
||||
}
|
||||
|
||||
|
|
@ -195,11 +228,27 @@ export async function autoImportDeviceLib() {
|
|||
const res = await fetch(ZIP_URL);
|
||||
if (!res.ok) throw new Error('Failed to fetch zip');
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const { ZipReader, Uint8ArrayReader, TextWriter } = zip;
|
||||
const buffer = new Uint8Array(await res.arrayBuffer());
|
||||
const reader = new ZipReader(new Uint8ArrayReader(buffer), {
|
||||
// useWebWorkers: false,
|
||||
useCompressionStream: true
|
||||
});
|
||||
const entries = await reader.getEntries();
|
||||
|
||||
const deviceFiles = Object.keys(zip.files).filter(name => name.includes('/Device.kicad_symdir/') && name.endsWith('.kicad_sym'));
|
||||
await importSelectedFromZip(zip, deviceFiles);
|
||||
const parsedSymbols = {};
|
||||
for (const entry of entries) {
|
||||
if (!entry.directory
|
||||
&& entry.filename.includes('/Device.kicad_symdir/')
|
||||
&& entry.filename.endsWith('.kicad_sym')) {
|
||||
const text = await entry.getData(new TextWriter());
|
||||
extractTopLevelSymbols(parseSexpr(text), parsedSymbols);
|
||||
}
|
||||
}
|
||||
|
||||
await reader.close();
|
||||
resolveInheritance(parsedSymbols);
|
||||
await upsertParsedSymbols(parsedSymbols, 'Device');
|
||||
console.log('[kicad] Default library import complete!');
|
||||
} catch (err) {
|
||||
console.error('[kicad] Auto-import failed:', err);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// LAYOUT — WireBender placement + routing
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ── Parsers ───────────────────────────────────────────────────
|
||||
import { classifyComp } from './components.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────
|
||||
// Single mutable app state object. All modules share this reference.
|
||||
export const S = {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Taras Greben
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ── UI helpers ────────────────────────────────────────────────
|
||||
import { db, uuid } from './db.js';
|
||||
import { S, getNetColor } from './state.js';
|
||||
|
|
@ -492,7 +486,13 @@ function initLibraryManager() {
|
|||
let libsData = {};
|
||||
|
||||
const log = (msg) => { logEl.innerHTML += `<div>${msg}</div>`; logEl.scrollTop = logEl.scrollHeight; };
|
||||
const resetZipUi = () => { defaultUi.style.display = 'flex'; zipUi.style.display = 'none'; currentZip = null; libsData = {}; };
|
||||
const resetZipUi = () => {
|
||||
if (currentZip?.reader) currentZip.reader.close().catch(() => {});
|
||||
currentZip = null;
|
||||
libsData = {};
|
||||
defaultUi.style.display = 'flex';
|
||||
zipUi.style.display = 'none';
|
||||
};
|
||||
|
||||
if (btnOpen) btnOpen.onclick = () => { modal.classList.add('active'); resetZipUi(); };
|
||||
|
||||
|
|
@ -504,16 +504,25 @@ function initLibraryManager() {
|
|||
|
||||
async function processZipBuffer(buffer) {
|
||||
log('Parsing ZIP directory structure...');
|
||||
currentZip = await JSZip.loadAsync(buffer);
|
||||
const files = Object.keys(currentZip.files).filter(n => n.endsWith('.kicad_sym'));
|
||||
const { ZipReader, Uint8ArrayReader } = zip;
|
||||
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(buffer)), {
|
||||
// useWebWorkers: false,
|
||||
useCompressionStream: true
|
||||
});
|
||||
const entries = await reader.getEntries();
|
||||
|
||||
// Keep reader open — needed later when user clicks Import
|
||||
currentZip = { reader, entries };
|
||||
|
||||
libsData = {};
|
||||
files.forEach(f => {
|
||||
const match = f.match(/([^\/]+)\.kicad_symdir\//);
|
||||
const libName = match ? match[1] : 'Imported';
|
||||
if (!libsData[libName]) libsData[libName] = new Array();
|
||||
libsData[libName].push({ path: f, name: f.split('/').pop().replace('.kicad_sym','') });
|
||||
});
|
||||
entries
|
||||
.filter(e => !e.directory && e.filename.endsWith('.kicad_sym'))
|
||||
.forEach(e => {
|
||||
const match = e.filename.match(/([^\/]+)\.kicad_symdir\//);
|
||||
const libName = match ? match[1] : 'Imported';
|
||||
if (!libsData[libName]) libsData[libName] = [];
|
||||
libsData[libName].push({ path: e.filename, name: e.filename.split('/').pop().replace('.kicad_sym', '') });
|
||||
});
|
||||
|
||||
treeEl.innerHTML = '';
|
||||
Object.keys(libsData).sort().forEach(libName => {
|
||||
|
|
@ -597,13 +606,28 @@ function initLibraryManager() {
|
|||
|
||||
if (selectedPaths.length === 0) return toast('No files selected', 'warn');
|
||||
|
||||
// Capture the zip reference BEFORE resetting the UI state!
|
||||
// Detach without closing — importSelectedFromZip owns the reader now and will close it
|
||||
const zipRef = currentZip;
|
||||
resetZipUi();
|
||||
currentZip = null;
|
||||
libsData = {};
|
||||
defaultUi.style.display = 'flex';
|
||||
zipUi.style.display = 'none';
|
||||
|
||||
log(`Extracting and parsing ${selectedPaths.length} files...`);
|
||||
try {
|
||||
const stats = await importSelectedFromZip(zipRef, selectedPaths);
|
||||
const stats = await importSelectedFromZip(zipRef, selectedPaths, (msg) => {
|
||||
// Update last line if it looks like a progress message, otherwise append
|
||||
const last = logEl.lastElementChild;
|
||||
if (last && last.dataset.progress) {
|
||||
last.textContent = msg;
|
||||
} else {
|
||||
const div = document.createElement('div');
|
||||
div.dataset.progress = '1';
|
||||
div.textContent = msg;
|
||||
logEl.appendChild(div);
|
||||
}
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
});
|
||||
log(`Success: <span style="color:#4ade80">${stats.inserted} inserted</span>, <span style="color:#f0c040">${stats.updated} updated</span>.`);
|
||||
toast('Import Complete', 'ok');
|
||||
} catch (err) { log(`<span style="color:var(--accent3)">Error: ${err.message}</span>`); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue