velxio/frontend/src/services/ComponentRegistry.ts

278 lines
10 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 {
// `cache: 'no-store'` so adding a new component (or rebuilding the JSON)
// shows up after a single page refresh — without this, the browser keeps
// serving the stale copy until you do a hard reload.
const response = await fetch('/components-metadata.json', { cache: 'no-store' });
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: 'velxio-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'],
});
// Inject SPICE probe instruments — these are Velxio-specific React
// components (not wokwi web elements), so they have no auto-generated
// metadata but still need a registry entry so the picker can offer
// them and the canvas can resolve them by id.
data.components.push({
id: 'instr-voltmeter',
tagName: 'velxio-instr-voltmeter',
name: 'Voltmeter',
category: 'analog',
description:
'SPICE probe — displays the voltage between V+ and V-. Used in electrical-mode circuits.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="6" fill="#1f1f1f" stroke="#ffa500" stroke-width="2"/><text x="50%" y="42%" text-anchor="middle" font-family="monospace" font-size="9" fill="#ffa500">V METER</text><text x="50%" y="68%" text-anchor="middle" font-family="monospace" font-size="11" fill="#ffa500" font-weight="bold">3.30 V</text></svg>',
properties: [],
defaultValues: {},
pinCount: 2,
tags: ['voltmeter', 'meter', 'probe', 'instrument', 'spice', 'multimeter', 'dmm'],
});
data.components.push({
id: 'instr-ammeter',
tagName: 'velxio-instr-ammeter',
name: 'Ammeter',
category: 'analog',
description:
'SPICE probe — measures the current through its body (connect in series). Used in electrical-mode circuits.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="6" fill="#1f1f1f" stroke="#4dd0e1" stroke-width="2"/><text x="50%" y="42%" text-anchor="middle" font-family="monospace" font-size="9" fill="#4dd0e1">A METER</text><text x="50%" y="68%" text-anchor="middle" font-family="monospace" font-size="11" fill="#4dd0e1" font-weight="bold">12.4 mA</text></svg>',
properties: [],
defaultValues: {},
pinCount: 2,
tags: ['ammeter', 'meter', 'probe', 'instrument', 'spice', 'current', 'multimeter', 'dmm'],
});
// Custom Chip — user-supplied WASM compiled from C. Pin layout is
// dynamic (read from the per-instance chip.json properties), so
// pinCount=0 is just a placeholder for the picker grid.
data.components.push({
id: 'custom-chip',
tagName: 'velxio-custom-chip',
name: 'Custom Chip',
category: 'logic',
description:
'Write your own chip in C and compile to WebAssembly. Includes a gallery of examples (EEPROM, RTC, shift register, ADC, UART, …).',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect x="6" y="14" width="52" height="36" rx="3" fill="#1a1a1a" stroke="#888" stroke-width="2"/><rect x="2" y="20" width="6" height="3" fill="#c0c0c0"/><rect x="2" y="28" width="6" height="3" fill="#c0c0c0"/><rect x="2" y="36" width="6" height="3" fill="#c0c0c0"/><rect x="2" y="44" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="20" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="28" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="36" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="44" width="6" height="3" fill="#c0c0c0"/><text x="32" y="36" text-anchor="middle" font-family="monospace" font-size="9" font-weight="bold" fill="#e0e0e0">CHIP</text></svg>',
properties: [
{ name: 'chipName', type: 'string', defaultValue: 'My Chip' },
{ name: 'sourceC', type: 'string', defaultValue: '' },
{ name: 'chipJson', type: 'string', defaultValue: '{"name":"My Chip","pins":["IN","OUT","GND","VCC"]}' },
{ name: 'wasmBase64', type: 'string', defaultValue: '' },
],
defaultValues: {
chipName: 'My Chip',
sourceC: '',
chipJson: '{"name":"My Chip","pins":["IN","OUT","GND","VCC"]}',
wasmBase64: '',
},
pinCount: 0,
tags: ['custom', 'chip', 'wasm', 'c', 'wokwi', 'eeprom', 'rtc', 'logic'],
});
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];
}
/**
* Merge additional components into the registry from an external source.
*
* Used by private overlays (e.g. the velxio.dev pro overlay) to add
* premium components after the default `/components-metadata.json` has
* loaded. Components with an existing `id` are replaced; new ones are
* appended. Categories and search index are rebuilt.
*/
mergeComponents(extras: ComponentMetadata[]): void {
if (!extras || extras.length === 0) return;
const byId = new Map(this.allComponents.map((c) => [c.id, c]));
for (const extra of extras) {
byId.set(extra.id, extra);
}
this.processMetadata(Array.from(byId.values()));
}
/**
* 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;