AES-256 encryption option for project files, progress on importing KiCad symbols

This commit is contained in:
Taras Greben 2026-05-23 11:18:19 +03:00
commit 0a0f9ff9fd
32 changed files with 493 additions and 394 deletions

View file

@ -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';

View file

@ -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
//

View file

@ -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
//

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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);

View file

@ -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
//

View file

@ -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';

View file

@ -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 = {

View file

@ -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>`); }