pcb-retrace/docs/schema/db.js

475 lines
18 KiB
JavaScript
Raw Normal View History

/*
* 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
//
// Opens the shared PcbReTrace IndexedDB at version 2.
// Provides typed read/write methods for both existing Studio
// stores (read-only from Schema's perspective) and the new
// Schema ReTrace stores introduced in version 2.
//
// Coordinate convention for stored positions:
// x, y — world-space centre coordinates (pixels, floating)
// rotation — integer degrees (0, 90, 180, 270)
// ═══════════════════════════════════════════════════════════════
const DB_NAME = window.PcbDbCore.DB_NAME;
const DB_VER = window.PcbDbCore.DB_VER;
// ── uuid helper (shared from db-core.js) ────────────
export const uuid = window.uuid;
// ── Seed data ─────────────────────────────────────────────────
// pinDirections rows — id values intentionally match WireBender PinDirection flags.
const SEED_PIN_DIRECTIONS = [
{ id: 0, name: 'None', description: 'No preferred direction', angle: null },
{ id: 1, name: 'Up', description: 'Wire exits upward', angle: 270 },
{ id: 2, name: 'Down', description: 'Wire exits downward', angle: 90 },
{ id: 4, name: 'Left', description: 'Wire exits to the left', angle: 180 },
{ id: 8, name: 'Right', description: 'Wire exits to the right', angle: 0 },
{ id: 15, name: 'All', description: 'Router chooses best direction', angle: null },
];
// symbolTypes — classification tree.
// IDs are stable UUIDs so they can be referenced as FKs from componentTypes.
const ST_PASSIVE = 'st-passive-0000-0000-000000000001';
const ST_SEMI = 'st-semi-00000-0000-0000-000000000002';
const ST_IC = 'st-ic-000000-0000-0000-000000000003';
const ST_RESISTOR = 'st-resistor-00-0000-0000-000000000004';
const ST_CAPACITOR = 'st-capacitor-0-0000-0000-000000000005';
const ST_INDUCTOR = 'st-inductor-00-0000-0000-000000000006';
const ST_DIODE = 'st-diode-0000-0000-0000-000000000007';
const ST_ZENER = 'st-zener-0000-0000-0000-000000000008';
const ST_GENERIC_IC = 'st-generic-ic-0-000-0000-000000000009';
const SEED_SYMBOL_TYPES = [
{ id: ST_PASSIVE, name: 'Passive', parentId: null },
{ id: ST_SEMI, name: 'Semiconductor', parentId: null },
{ id: ST_IC, name: 'IC', parentId: null },
{ id: ST_RESISTOR, name: 'Resistor', parentId: ST_PASSIVE },
{ id: ST_CAPACITOR, name: 'Capacitor', parentId: ST_PASSIVE },
{ id: ST_INDUCTOR, name: 'Inductor', parentId: ST_PASSIVE },
{ id: ST_DIODE, name: 'Diode', parentId: ST_SEMI },
{ id: ST_ZENER, name: 'Zener Diode', parentId: ST_DIODE },
{ id: ST_GENERIC_IC, name: 'Generic IC', parentId: ST_IC },
];
// componentTypes — built-in types matching components.js classes.
// Stable IDs so schemaComponents can reference them reliably.
export const CT_RESISTOR = 'ct-resistor-000-0000-0000-000000000001';
export const CT_CAPACITOR = 'ct-capacitor-0-0000-0000-000000000002';
export const CT_INDUCTOR = 'ct-inductor-00-0000-0000-000000000003';
export const CT_DIODE = 'ct-diode-0000-0000-0000-000000000004';
export const CT_ZENER = 'ct-zener-0000-0000-0000-000000000005';
export const CT_GENERIC_IC = 'ct-generic-ic-0-000-0000-000000000006';
// Maps the type key used in S.components[].type → componentType id
export const TYPE_KEY_TO_CT = {
R: CT_RESISTOR,
C: CT_CAPACITOR,
L: CT_INDUCTOR,
D: CT_DIODE,
Z: CT_ZENER,
IC: CT_GENERIC_IC,
};
const NOW = () => Date.now();
const SEED_COMPONENT_TYPES = [
{ id: CT_RESISTOR, symbolTypeId: ST_RESISTOR, name: 'Resistor', pinCount: 2, created: 0, lastModified: 0 },
{ id: CT_CAPACITOR, symbolTypeId: ST_CAPACITOR, name: 'Capacitor', pinCount: 2, created: 0, lastModified: 0 },
{ id: CT_INDUCTOR, symbolTypeId: ST_INDUCTOR, name: 'Inductor', pinCount: 2, created: 0, lastModified: 0 },
{ id: CT_DIODE, symbolTypeId: ST_DIODE, name: 'Diode', pinCount: 2, created: 0, lastModified: 0 },
{ id: CT_ZENER, symbolTypeId: ST_ZENER, name: 'Zener Diode', pinCount: 2, created: 0, lastModified: 0 },
{ id: CT_GENERIC_IC, symbolTypeId: ST_GENERIC_IC, name: 'Generic IC', pinCount: null, created: 0, lastModified: 0 },
];
// componentTypePins — pin positions in centre-origin local space (pixels).
// DirLeft=4, DirRight=8, DirUp=1, DirDown=2 (matches pinDirections.id).
// Dimensions match geometry defined in components.js:
// Resistor: 60×24 Capacitor: 28×40 Inductor: 60×20 Diode/Zener: 44×24
const SEED_COMPONENT_TYPE_PINS = [
// Resistor — horizontal, pins left/right at half-width
{ id: uuid(), componentTypeId: CT_RESISTOR, number: 1, name: '1', description: null, x: -30, y: 0, directionId: 4 },
{ id: uuid(), componentTypeId: CT_RESISTOR, number: 2, name: '2', description: null, x: 30, y: 0, directionId: 8 },
// Capacitor — vertical, pins top/bottom at half-height
{ id: uuid(), componentTypeId: CT_CAPACITOR, number: 1, name: '+', description: 'Positive / anode', x: 0, y: -20, directionId: 1 },
{ id: uuid(), componentTypeId: CT_CAPACITOR, number: 2, name: '-', description: 'Negative / cathode', x: 0, y: 20, directionId: 2 },
// Inductor — horizontal
{ id: uuid(), componentTypeId: CT_INDUCTOR, number: 1, name: '1', description: null, x: -30, y: 0, directionId: 4 },
{ id: uuid(), componentTypeId: CT_INDUCTOR, number: 2, name: '2', description: null, x: 30, y: 0, directionId: 8 },
// Diode — Anode left, Cathode right
{ id: uuid(), componentTypeId: CT_DIODE, number: 1, name: 'A', description: 'Anode', x: -22, y: 0, directionId: 4 },
{ id: uuid(), componentTypeId: CT_DIODE, number: 2, name: 'K', description: 'Cathode', x: 22, y: 0, directionId: 8 },
// Zener — same physical layout as Diode
{ id: uuid(), componentTypeId: CT_ZENER, number: 1, name: 'A', description: 'Anode', x: -22, y: 0, directionId: 4 },
{ id: uuid(), componentTypeId: CT_ZENER, number: 2, name: 'K', description: 'Cathode', x: 22, y: 0, directionId: 8 },
// Generic IC — no fixed pins in type definition; pins are instance-specific
];
// ═══════════════════════════════════════════════════════════════
// SchemaDatabase class
// ═══════════════════════════════════════════════════════════════
export class SchemaDatabase {
constructor() {
this._db = null;
}
// ── Initialise: open DB, upgrade if needed, seed static data ─
async init() {
await this._open();
await this._seed();
await this._migratePinCounts();
}
_open() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VER);
req.onupgradeneeded = e => {
console.log('[schema/db] onupgradeneeded — upgrading from v', e.oldVersion, '→', e.newVersion);
window.PcbDbCore.setupDatabase(e.target.result, e.target.transaction);
};
req.onsuccess = e => { this._db = e.target.result; console.log('[schema/db] opened v', this._db.version); resolve(); };
req.onerror = e => { console.error('[SchemaDB] Open error:', e); reject(e); };
});
}
// ── Seed static lookup data on first run ──────────────────────
async _seed() {
// pinDirections — only seed if table is empty
const existingDirs = await this._getAll('pinDirections');
if (existingDirs.length === 0) {
const tx = this._db.transaction('pinDirections', 'readwrite');
const st = tx.objectStore('pinDirections');
SEED_PIN_DIRECTIONS.forEach(d => st.put(d));
await this._txDone(tx);
}
// symbolTypes — only seed if table is empty
const existingSymTypes = await this._getAll('symbolTypes');
if (existingSymTypes.length === 0) {
const tx = this._db.transaction('symbolTypes', 'readwrite');
const st = tx.objectStore('symbolTypes');
SEED_SYMBOL_TYPES.forEach(s => st.put(s));
await this._txDone(tx);
}
// componentTypes — only seed if table is empty
const existingTypes = await this._getAll('componentTypes');
if (existingTypes.length === 0) {
const tx = this._db.transaction(['componentTypes', 'componentTypePins'], 'readwrite');
SEED_COMPONENT_TYPES.forEach(t => tx.objectStore('componentTypes').put(t));
SEED_COMPONENT_TYPE_PINS.forEach(p => tx.objectStore('componentTypePins').put(p));
await this._txDone(tx);
}
}
// ── Low-level helpers ─────────────────────────────────────────
_tx(store, mode, cb) {
return new Promise((resolve, reject) => {
const t = this._db.transaction(store, mode);
const q = cb(t.objectStore(store));
q.onsuccess = () => resolve(q.result);
q.onerror = () => reject(q.error);
});
}
_ix(store, index, value) {
return new Promise((resolve, reject) => {
const q = this._db.transaction(store, 'readonly')
.objectStore(store).index(index).getAll(value);
q.onsuccess = () => resolve(q.result);
q.onerror = () => reject(q.error);
});
}
_getAll(store) {
return this._tx(store, 'readonly', s => s.getAll());
}
_txDone(tx) {
return new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}
// ── Studio store reads (read-only from Schema's perspective) ──
getDevice(id) {
return this._tx('devices', 'readonly', s => s.get(id));
}
getDevices() {
return this._getAll('devices');
}
getBoard(id) {
return this._tx('boards', 'readonly', s => s.get(id));
}
getBoardsByDevice(deviceId) {
return this._ix('boards', 'deviceId', deviceId);
}
/** All components belonging to a specific board. */
getComponentsByBoard(boardId) {
return this._ix('components', 'boardId', boardId);
}
/**
* All nets whose projectId matches boardId.
* Falls back to a full-table JS filter when the projectId index
* doesn't exist yet (DB still at v1 before Studio upgrade runs).
*/
async getNetsByBoard(boardId) {
const store = this._db.transaction('nets', 'readonly').objectStore('nets');
if (store.indexNames.contains('projectId')) {
return this._ix('nets', 'projectId', boardId);
}
// Index not yet created — full scan + filter
return new Promise((resolve, reject) => {
const req = store.getAll();
req.onsuccess = () => resolve((req.result || []).filter(n => n.projectId === boardId));
req.onerror = () => reject(req.error);
});
}
// ── Schemas ───────────────────────────────────────────────────
/**
* Returns the existing schema for this board, or creates one.
* boardId may be null for device-level schemas.
*/
async getOrCreateSchema(deviceId, boardId) {
// Try to find an existing schema for this board
let existing = null;
if (boardId) {
const byBoard = await this._ix('schemas', 'boardId', boardId);
existing = byBoard.find(s => s.deviceId === deviceId) || null;
} else {
// Device-level schema: boardId is null — find by deviceId with null boardId
const byDevice = await this._ix('schemas', 'deviceId', deviceId);
existing = byDevice.find(s => !s.boardId) || null;
}
if (existing) return existing;
const schema = {
id: uuid(),
deviceId,
boardId: boardId || null,
name: 'Main Schema',
created: NOW(),
lastModified: NOW(),
viewportZoom: 1,
viewportX: 0,
viewportY: 0,
};
await this._tx('schemas', 'readwrite', s => s.put(schema));
return schema;
}
saveSchema(schema) {
schema.lastModified = NOW();
return this._tx('schemas', 'readwrite', s => s.put(schema));
}
// ── SchemaComponents ──────────────────────────────────────────
getSchemaComponents(schemaId) {
return this._ix('schemaComponents', 'schemaId', schemaId);
}
saveSchemaComponent(sc) {
return this._tx('schemaComponents', 'readwrite', s => s.put(sc));
}
deleteSchemaComponent(id) {
return this._tx('schemaComponents', 'readwrite', s => s.delete(id));
}
/**
* Upsert a schemaComponent record for a given component within a schema.
* Creates a new record if none exists, updates existing one if found.
* Sets locked=true so this position survives future auto-layouts.
*/
async upsertSchemaComponent(schemaId, componentId, { x, y, rotation, flipX, componentTypeId, pinCount, overrides } = {}) {
const existing = await this._findSchemaComponent(schemaId, componentId);
const sc = existing || {
id: uuid(),
schemaId,
componentId,
componentTypeId: null,
pinCount: null,
overrides: null,
};
// Always update position/rotation/lock
sc.x = x;
sc.y = y;
sc.rotation = rotation; // degrees
sc.flipX = flipX;
sc.locked = true;
if (componentTypeId !== undefined) sc.componentTypeId = componentTypeId;
if (pinCount !== undefined) sc.pinCount = pinCount;
if (overrides !== undefined) sc.overrides = overrides;
return this._tx('schemaComponents', 'readwrite', s => s.put(sc));
}
/** Find a schemaComponent record for a specific component within a schema. */
async findSchemaComponent(schemaId, componentId) {
const all = await this._ix('schemaComponents', 'schemaId', schemaId);
return all.find(sc => sc.componentId === componentId) || null;
}
async createMissingComponent(boardId, label, value) {
const id = uuid();
await this._tx('components', 'readwrite', s => s.put(
{ id, boardId, label, value, desc: '' }
));
return id;
}
// Keep private alias for internal use
_findSchemaComponent(schemaId, componentId) {
return this.findSchemaComponent(schemaId, componentId);
}
// ── KiCad Symbols ─────────────────────────────────────────────
async getKicadSymbols(library) {
return this._ix('kicadSymbols', 'library', library);
}
async getKicadSymbolsByPinCount(pinCount) {
return this._ix('kicadSymbols', 'pinCount', pinCount);
}
async _migratePinCounts() {
const syms = await this._getAll('kicadSymbols');
const needsMigration = syms.filter(s => s.pinCount === undefined);
if (needsMigration.length === 0) return;
console.log(`[schema/db] Migrating ${needsMigration.length} KiCad symbols to add pinCount...`);
const tx = this._db.transaction('kicadSymbols', 'readwrite');
const st = tx.objectStore('kicadSymbols');
for (const s of needsMigration) {
try {
const parsed = JSON.parse(s.parsedData);
s.pinCount = parsed.pins ? parsed.pins.length : 0;
st.put(s);
} catch (e) {
console.warn('Failed to migrate symbol', s.id);
}
}
await this._txDone(tx);
console.log('[schema/db] Migration complete.');
}
async saveKicadSymbol(sym) {
return this._tx('kicadSymbols', 'readwrite', s => s.put(sym));
}
async saveKicadSymbolsBatch(syms) {
const tx = this._db.transaction('kicadSymbols', 'readwrite');
const st = tx.objectStore('kicadSymbols');
syms.forEach(sym => st.put(sym));
return this._txDone(tx);
}
async saveComponentType(ct) {
return this._tx('componentTypes', 'readwrite', s => s.put(ct));
}
async saveComponentTypeKicadSymbol(ctks) {
return this._tx('componentTypeKicadSymbols', 'readwrite', s => s.put(ctks));
}
async getKicadSymbolById(id) {
return this._tx('kicadSymbols', 'readonly', s => s.get(id));
}
async getComponentTypeForKicadSymbol(kicadSymbolId) {
const links = await this._ix('componentTypeKicadSymbols', 'kicadSymbolId', kicadSymbolId);
if (links && links.length > 0) return this.getComponentType(links[0].componentTypeId);
return null;
}
async getResolvedKicadDataForComponentType(componentTypeId) {
const links = await this._ix('componentTypeKicadSymbols', 'componentTypeId', componentTypeId);
if (!links || links.length === 0) return null;
return this.getKicadSymbolById(links[0].kicadSymbolId);
}
// ── Component type catalogue ──────────────────────────────────
getComponentTypes() {
return this._getAll('componentTypes');
}
getComponentType(id) {
return this._tx('componentTypes', 'readonly', s => s.get(id));
}
getComponentTypePins(componentTypeId) {
return this._ix('componentTypePins', 'componentTypeId', componentTypeId);
}
getPinDirections() {
return this._getAll('pinDirections');
}
getSymbolTypes() {
return this._getAll('symbolTypes');
}
// ── Standalone netlist import ─────────────────────────────────
/**
* Creates a device + board + components + nets in one logical operation.
* The netlist is in Schema ReTrace's internal parsed format:
* { components: [{ref, value, type}], nets: [{name, nodes:[{ref,pin}]}] }
* Returns { deviceId, boardId }.
*/
async importNetlist(name, parsed) {
const deviceId = uuid();
const boardId = uuid();
const ts = NOW();
// Build component records — map ref → id for net node resolution
const refToId = {};
const compRecords = parsed.components.map(c => {
const id = uuid();
refToId[c.ref] = id;
return { id, boardId, label: c.ref, value: c.value || '', desc: '' };
});
// Build net records using Studio's format (nodes as "ref.pin" label strings)
const netRecords = parsed.nets.map(n => ({
id: uuid(),
projectId: boardId,
name: n.name,
nodes: (n.nodes || []).map(nd => ({ label: `${nd.ref}.${nd.pin}` })),
}));
// Single transaction for atomicity
const stores = ['devices', 'boards', 'components', 'nets'];
const tx = this._db.transaction(stores, 'readwrite');
tx.objectStore('devices').put({ id: deviceId, name });
tx.objectStore('boards').put({
id: boardId, deviceId,
name: 'Main Board',
section: '', sortMode: 'none', lastModified: ts,
});
compRecords.forEach(c => tx.objectStore('components').put(c));
netRecords.forEach(n => tx.objectStore('nets').put(n));
await this._txDone(tx);
return { deviceId, boardId };
}
}
// ── Module-level singleton ────────────────────────────────────
export const db = new SchemaDatabase();