velxio/frontend/src/services/ComponentRegistry.ts

197 lines
5.2 KiB
TypeScript
Raw Normal View History

/**
* Component Registry
*
* Singleton service that loads and provides access to component metadata.
* Loads from components-metadata.json generated at build time.
*/
import type {
ComponentMetadata,
ComponentCategory,
ComponentMetadataCollection,
} from '../types/component-metadata';
export class ComponentRegistry {
private static instance: ComponentRegistry;
private metadata: Map<string, ComponentMetadata> = new Map();
private categories: Map<ComponentCategory, ComponentMetadata[]> = new Map();
private allComponents: ComponentMetadata[] = [];
private loaded = false;
private _loadPromise: Promise<void> | null = null;
private constructor() {}
/**
* Get singleton instance
*/
static getInstance(): ComponentRegistry {
if (!ComponentRegistry.instance) {
ComponentRegistry.instance = new ComponentRegistry();
}
return ComponentRegistry.instance;
}
/**
* Load metadata from JSON file
*/
async load(): Promise<void> {
if (this.loaded) return;
if (this._loadPromise) return this._loadPromise;
this._loadPromise = this._doLoad();
return this._loadPromise;
}
/**
* Returns the load promise so consumers can await registry readiness
*/
get loadPromise(): Promise<void> {
return this._loadPromise ?? this.load();
}
get isLoaded(): boolean {
return this.loaded;
}
private async _doLoad(): Promise<void> {
try {
const response = await fetch('/components-metadata.json');
if (!response.ok) {
throw new Error(`Failed to load metadata: ${response.statusText}`);
}
const data: ComponentMetadataCollection = await response.json();
// Inject Raspberry Pi 3 metadata
data.components.push({
id: 'raspberry-pi-3',
tagName: 'wokwi-raspberry-pi-3',
name: 'Raspberry Pi 3',
category: 'boards',
description: 'Raspberry Pi 3 Model B with 40-pin GPIO. Connects to backend QEMU simulator.',
thumbnail: '<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#E60049" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi3</text></svg>',
properties: [],
defaultValues: {},
pinCount: 40,
tags: ['raspberry', 'pi', 'rp3', 'board', 'qemu', 'linux']
});
this.processMetadata(data.components);
this.loaded = true;
console.log(`Loaded ${this.allComponents.length} components from metadata`);
} catch (error) {
console.error('Failed to load component metadata:', error);
// Continue with empty registry - app should still work with manual component addition
}
}
/**
* Process and index metadata
*/
private processMetadata(components: ComponentMetadata[]): void {
this.allComponents = components;
this.metadata.clear();
this.categories.clear();
// Index by ID
components.forEach(component => {
this.metadata.set(component.id, component);
// Group by category
const categoryComponents = this.categories.get(component.category) || [];
categoryComponents.push(component);
this.categories.set(component.category, categoryComponents);
});
}
/**
* Get all components
*/
getAllComponents(): ComponentMetadata[] {
return [...this.allComponents];
}
/**
* Get components by category
*/
getByCategory(category: ComponentCategory): ComponentMetadata[] {
return this.categories.get(category) || [];
}
/**
* Get component by ID
*/
getById(id: string): ComponentMetadata | undefined {
return this.metadata.get(id);
}
/**
* Search components by query (name, description, tags)
*/
search(query: string): ComponentMetadata[] {
if (!query.trim()) {
return this.getAllComponents();
}
const lowerQuery = query.toLowerCase();
return this.allComponents.filter(component => {
return (
component.name.toLowerCase().includes(lowerQuery) ||
component.id.toLowerCase().includes(lowerQuery) ||
component.description?.toLowerCase().includes(lowerQuery) ||
component.tags.some(tag => tag.toLowerCase().includes(lowerQuery))
);
});
}
/**
* Get all available categories
*/
getCategories(): ComponentCategory[] {
return Array.from(this.categories.keys());
}
/**
* Reload metadata (for hot-reload in dev mode)
*/
async reload(): Promise<void> {
this.loaded = false;
await this.load();
}
/**
* Get component count
*/
getComponentCount(): number {
return this.allComponents.length;
}
/**
* Get category display name
*/
static getCategoryDisplayName(category: ComponentCategory): string {
const displayNames: Record<ComponentCategory, string> = {
boards: 'Boards',
sensors: 'Sensors',
displays: 'Displays',
input: 'Input',
output: 'Output',
motors: 'Motors',
communication: 'Communication',
passive: 'Passive',
feat: expand SPICE component catalog (fases 9 + 10) Adds 44 SPICE mappers, 58 custom metadata entries, and 12 visual Web Components covering logic gates, transistors, op-amps, regulators, sources, electromechanical parts and integrated-circuit packaging. Fase 9 — component catalog expansion ------------------------------------ - 7 logic gates (AND/OR/NAND/NOR/XOR/XNOR + NOT) as SPICE B-sources - 8 multi-input gates (AND/OR/NAND/NOR with 3 and 4 inputs) - 9 transistors: 5 BJTs (incl. PNP 2N3906/BC557) + 4 MOSFETs (incl. P-channel IRF9540/FQP27P06). NMOS refactored from Level=3 W=0.1 (hangs ngspice) to Level=1 with sane W/L - 5 op-amps: LM358, LM741, TL072, LM324 with per-chip saturation rails + opamp-ideal - 4 linear regulators (7805, 7812, 7905, LM317) with dropout - 3 batteries (9V, AA, coin-cell) with realistic ESR - Signal generator (sine / square / DC) - 2 Schottky diodes (1N5817, 1N5819) + photodiode (lux-driven current source) Fase 10 — electromechanical + ICs --------------------------------- - Relay (SPDT): coil + L + S-switch with native hysteresis + flyback diode, inverted-control trick for the NC contact - Optocouplers 4N25 and PC817 (LED + CCCS with CTR=0.5 / 1.0) - 7 74HC ICs as DIP-14 packages emitting 4 or 6 B-sources per component (first mapper pattern emitting multiple device cards) - 3 flip-flops (D, T, JK) — digital-sim only (edge detection is not representable in ngspice .op) - L293D dual H-bridge motor driver Infrastructure -------------- - scripts/component-overrides.json gains a _customComponents[] array that lets new Velxio-only parts survive metadata regeneration (previously applyOverrides() could only patch wokwi-elements components that had already been scanned) - scripts/generate-component-metadata.ts injects custom entries before the patch loop - New ComponentCategory values: 'logic', 'analog', 'electromech' - frontend/src/components/DynamicComponent.tsx PASSIVE tracing extended from just ['resistor','resistor-us'] to 9 two-terminal passives with per-part pin name maps - New CI workflow test-circuit.yml runs the sandbox on push/PR - frontend-tests.yml regenerates metadata and fails if committed JSON is stale - Documented 2 new ngspice gotchas in circuit-emulation-gotchas.md: unicode in netlist titles silently hangs the parser, and MOSFET Level=3 + W=0.1m causes .op to hang - 164/164 sandbox tests passing in ~9 s (was 88 pre-fase-9) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 06:44:18 +07:00
logic: 'Logic Gates',
analog: 'Analog',
electromech: 'Electromechanical',
other: 'Other',
};
return displayNames[category] || category;
}
}
// Auto-load on module import
const registry = ComponentRegistry.getInstance();
registry.load();
export default registry;