// AutomationHub: excerpt, not a standalone build. /* eslint-disable @typescript-eslint/no-explicit-any */ import { BLOCK_CATALOG, getBlockDefinition } from './automation/catalog.ts'; import type { BlockDefinition, CircuitBlock, CircuitConnection, CircuitDocument, ParameterValue, RuntimeBlock, Signal, SignalKind, } from './automation/types.ts'; export type { BlockType, CircuitBlock, CircuitConnection, CircuitDocument, RuntimeBlock, Signal } from './automation/types.ts'; const readers = new Set(['INPUT', 'NC_INPUT', 'ANALOG_INPUT']); const writers = new Set(['OUTPUT', 'FLAG', 'ANALOG_OUTPUT', 'ANALOG_FLAG']); const defaultSignal = (kind: SignalKind): Signal => kind === 'analog' ? 0 : kind === 'text' ? '' : false; const isSignal = (value: Signal, kind: SignalKind) => kind === 'digital' ? typeof value === 'boolean' : kind === 'analog' ? typeof value === 'number' && Number.isFinite(value) : typeof value === 'string'; /** * Deterministic educational simulator. A tick represents an elapsed interval. * Blocks run in dependency order, including tag links; memory updates once per * tick. Feedback must cross SCAN_DELAY, whose output is the preceding scan's * sampled input. Editing a circuit never advances its simulation time. */ export class LogicEngine { public blocks = new Map(); public connections: CircuitConnection[] = []; public timeMs = 0; public diagnostics: string[] = []; public history: import('./automation/types.ts').EngineSnapshot[] = []; private order: string[] = []; private incoming = new Map(); private tagWriters = new Map(); addBlock(block: CircuitBlock): void { if (!block.id.trim()) throw new Error('A block needs an ID.'); if (this.blocks.has(block.id)) throw new Error(`Block ID "${block.id}" already exists.`); const definition = getBlockDefinition(block.type); if (!definition || definition.status !== 'ready') throw new Error(`Block ${block.type} is not available for simulation yet.`); if (!Number.isFinite(block.position.x) || !Number.isFinite(block.position.y)) throw new Error('Block coordinates must be finite numbers.'); const params: Record = {}; for (const parameter of definition.parameters) params[parameter.key] = parameter.defaultValue; for (const [key, value] of Object.entries(block.params ?? {})) { this.validateParameter(definition, key, value); params[key] = value; } if (block.value !== undefined && 'value' in params) { params.value = block.value; } let initialValue = block.value; if (initialValue === undefined && params.value !== undefined) { initialValue = params.value as Signal; } const valueKind = definition.outputs[0]?.kind; if (initialValue !== undefined && valueKind && !isSignal(initialValue, valueKind)) throw new Error(`${definition.name} requires a ${valueKind} value.`); if (block.tag?.trim() && !definition.tagged) throw new Error(`${definition.name} does not support tags.`); const runtime: RuntimeBlock = { ...block, position: { ...block.position }, tag: block.tag?.trim() || undefined, value: initialValue, params, inputs: {}, outputs: {}, state: {}, }; this.initialize(runtime); this.blocks.set(block.id, runtime); try { this.rebuildGraph(); } catch (error) { this.blocks.delete(block.id); this.rebuildGraph(); throw error; } this.refresh(); } removeBlock(id: string): void { this.blocks.delete(id); this.connections = this.connections.filter(edge => edge.fromBlockId !== id && edge.toBlockId !== id); this.rebuildGraph(); this.refresh(); } validateConnection(connection: CircuitConnection): string | null { if (!connection.id.trim()) return 'A connection needs an ID.'; if (this.connections.some(edge => edge.id === connection.id)) return 'This connection ID already exists.'; const source = this.blocks.get(connection.fromBlockId);