@typecad/typecad
Version:
🤖programmatically 💥create 🛰️hardware
183 lines (182 loc) • 6.7 kB
JavaScript
import { randomUUID } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import chalk from 'chalk';
/**
* Registry for managing component UUIDs and reusing them across builds
*/
export class ComponentRegistry {
/**
* Get the number of components in the registry
*/
getComponentCount() {
return this.components.size;
}
/**
* Get the number of UUID to hash mappings in the registry
*/
getUuidToHashCount() {
return this.uuidToHash.size;
}
constructor() {
this.components = new Map(); // Map of component hash to UUID
this.uuidToHash = new Map(); // Reverse lookup (UUID to hash)
this.cachePath = path.join(process.cwd(), 'build', 'cache', 'component_registry.json');
this.loadFromCache();
}
/**
* Get the singleton instance of the registry
*/
static getInstance() {
if (!ComponentRegistry.instance) {
ComponentRegistry.instance = new ComponentRegistry();
}
return ComponentRegistry.instance;
}
/**
* Load existing component registry from cache
*/
loadFromCache() {
try {
if (fs.existsSync(this.cachePath)) {
const data = JSON.parse(fs.readFileSync(this.cachePath, 'utf8'));
// Process each entry and build both maps
if (data.components) {
Object.entries(data.components).forEach(([hash, uuid]) => {
this.components.set(hash, uuid);
this.uuidToHash.set(uuid, hash);
});
console.log(chalk.green(`đź“‹ Loaded ${this.components.size} component UUIDs from cache`));
}
}
}
catch (error) {
console.warn(chalk.yellow('⚠️ Failed to load component registry from cache'), error);
// Create a new registry if loading fails
this.components = new Map();
this.uuidToHash = new Map();
}
}
/**
* Save the current registry to cache
*/
saveToCache() {
try {
// Ensure directory exists
const dir = path.dirname(this.cachePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Save the cache
const data = {
components: Object.fromEntries(this.components),
meta: {
lastUpdated: new Date().toISOString(),
componentCount: this.components.size
}
};
fs.writeFileSync(this.cachePath, JSON.stringify(data, null, 2));
}
catch (error) {
console.warn(chalk.yellow('⚠️ Failed to save component registry to cache'), error);
}
}
/**
* Generate a positional hash for PCB coordinates with tolerance
* This ensures components in roughly the same position get the same hash
* even with minor position adjustments
*/
getPCBPositionHash(pcb) {
if (!pcb || pcb.x === undefined || pcb.y === undefined) {
return 'no-position';
}
// Round to nearest millimeter to provide some tolerance for small changes
// Adjust tolerance as needed for your application
const tolerance = 1; // 1mm tolerance
const x = Math.round(pcb.x / tolerance) * tolerance;
const y = Math.round(pcb.y / tolerance) * tolerance;
const rotation = pcb.rotation !== undefined ?
Math.round(pcb.rotation / 45) * 45 : // Round to nearest 45 degrees
0;
const side = pcb.side || 'front'; // Default to 'front' if side is not specified
return `pos:${x},${y},${rotation},${side}`;
}
/**
* Generate a unique hash for a component based on its properties
* Prioritizes stable properties that won't change between builds
* Avoids using reference designator which might change
*
* @param component The component to generate a hash for
* @returns A string hash representing the component's unique properties
*/
getComponentHash(component) {
// Create a hash based on stable component properties that identify it uniquely
// Using component as any to avoid circular dependencies with property access
// Explicitly NOT using reference designator since it can change
const hashParts = [
// Type info
component.symbol || 'no-symbol',
component.value || 'no-value',
component.footprint || 'no-footprint',
// Manufacturer info (if available)
component.mpn || 'no-mpn',
// Physical positioning - crucial for identifying components
this.getPCBPositionHash(component.pcb),
// Additional identifiers
component.voltage || '',
component.wattage || '',
component.description?.substring(0, 50) || '' // Truncate long descriptions
];
return hashParts.filter(part => part !== '').join('::');
}
/**
* Get or create a UUID for the given component
* Will reuse existing UUID if a similar component is found
*
* @param component The component to get a UUID for
* @returns A UUID string
*/
getUUID(component) {
// Check early if component already has a uuid to avoid recursion
if (component._uuid && component._uuid !== '') {
return component._uuid;
}
const hash = this.getComponentHash(component);
// If we already have a UUID for this component hash, return it
if (this.components.has(hash)) {
return this.components.get(hash);
}
// Create a new UUID for this component
const newUuid = randomUUID();
this.components.set(hash, newUuid);
this.uuidToHash.set(newUuid, hash);
this.saveToCache();
return newUuid;
}
/**
* Find a component by UUID
* Useful for cross-referencing components
*
* @param uuid UUID to lookup
* @returns The component hash or undefined if not found
*/
findComponentHashByUUID(uuid) {
return this.uuidToHash.get(uuid);
}
/**
* Get all registered UUIDs
* @returns Array of UUIDs
*/
getAllUUIDs() {
return Array.from(this.uuidToHash.keys());
}
/**
* Clear the registry
* Use with caution as this will invalidate all component UUIDs
*/
clear() {
this.components.clear();
this.uuidToHash.clear();
this.saveToCache();
}
}