import { Transform } from '../../../common/math/classes/Transform' import { Pin } from './Pin' import { GateTemplate, PinCount } from '../types/GateTemplate' import { idStore } from '../stores/idStore' import { Context, InitialisationContext } from '../../activation/types/Context' import { toFunction } from '../../activation/helpers/toFunction' import { Subscription, BehaviorSubject } from 'rxjs' import { SimulationError } from '../../errors/classes/SimulationError' import { getGateTimePipes } from '../helpers/getGateTimePipes' import { ImageStore } from '../../simulationRenderer/stores/imageStore' import { completeTemplate } from '../../logic-gates/helpers/completeTemplate' import { Simulation, SimulationEnv } from './Simulation' import { fromSimulationState } from '../../saving/helpers/fromState' import { saveStore } from '../../saving/stores/saveStore' import { Wire } from './Wire' import { cleanSimulation } from '../../simulation-actions/helpers/clean' import { ExecutionQueue } from '../../activation/classes/ExecutionQueue' import { tap } from 'rxjs/operators' /** * The interface for the pins of a gate */ export interface GatePins { inputs: Pin[] outputs: Pin[] } /** * Wrapper around a pin so it can be rendered at the right place */ export interface PinWrapper { total: number index: number value: Pin } /** * A function wich can be run with an activation context */ export type GateFunction = null | ((ctx: Context) => void) /** * All functions a gate must remember */ export interface GateFunctions { activation: GateFunction onClick: GateFunction } export class Gate { /** * The transform of the gate */ public transform = new Transform() /** * The object holding all the pins the gate curently has */ public _pins: GatePins = { inputs: [], outputs: [] } /** * The id of the gate */ public id: number /** * The template the gate needs to follow */ public template: GateTemplate /** * All the functions created from the template strings */ private functions: GateFunctions = { activation: null, onClick: null } /** * Used only if the gate is async */ private executionQueue = new ExecutionQueue<void>() /** * All rxjs subscriptions the gate created * (if they are not manually cleared it can lead to memory leaks) */ private subscriptions: Subscription[] = [] /** * The state the activation functions have aces to */ private memory: Record<string, unknown> = {} /** * The inner simulaton used by integrated circuits */ private ghostSimulation: Simulation /** * The wires connecting the outer simulation to the inner one */ private ghostWires: Wire[] = [] /** * Boolean keeping track if the component is an ic */ private isIntegrated = false /** * Used to know if the component runs in the global scope (rendered) * or insie an integrated circuit */ public env: SimulationEnv = 'global' /** * Holds all the gate-related text */ public text = { inner: new BehaviorSubject('text goes here') } /** * The props used by the activation function (the same as memory but presists) */ public props: Record< string, BehaviorSubject<string | number | boolean> > = {} /** * The main logic gate class * * @param template The template the gate needs to follow * @param id The id of the gate */ public constructor( template: DeepPartial<GateTemplate> = {}, id?: number, props: Record<string, string | number | boolean> = {} ) { this.template = completeTemplate(template) this.transform.scale = this.template.shape.scale if (this.template.material.type === 'color') { this.template.material.colors.main = this.template.material.fill } this.functions.activation = toFunction( this.template.code.activation, 'context' ) this.functions.onClick = toFunction( this.template.code.onClick, 'context' ) this._pins.inputs = Gate.generatePins( this.template.pins.inputs, 1, this ) this._pins.outputs = Gate.generatePins( this.template.pins.outputs, 2, this ) if (this.template.material.type === 'image') { ImageStore.set(this.template.material.fill) } this.id = id !== undefined ? id : idStore.generate() for (const pin of this._pins.inputs) { const pipes = getGateTimePipes(this.template) const subscription = pin.state.pipe(...pipes).subscribe(() => { if (this.template.code.async) { this.executionQueue.push(async () => { return await this.update() }) } else { this.update() } }) this.subscriptions.push(subscription) } this.init() if (this.template.tags.includes('integrated')) { this.isIntegrated = true } if (this.isIntegrated) { const state = saveStore.get(this.template.metadata.name) if (!state) { throw new SimulationError( `Cannot run ic ${ this.template.metadata.name } - save not found` ) } this.ghostSimulation = fromSimulationState(state.simulation, 'gate') cleanSimulation(this.ghostSimulation) const sortByPosition = (x: Gate, y: Gate) => x.transform.position[1] - y.transform.position[1] const gates = Array.from(this.ghostSimulation.gates) const inputs = gates .filter(gate => gate.template.integration.input) .sort(sortByPosition) .map(gate => gate.wrapPins(gate._pins.outputs)) .flat() const outputs = gates .filter(gate => gate.template.integration.output) .sort(sortByPosition) .map(gate => gate.wrapPins(gate._pins.inputs)) .flat() if (inputs.length !== this._pins.inputs.length) { throw new SimulationError( `Input count needs to match with the container gate: ${ inputs.length } !== ${this._pins.inputs.length}` ) } if (outputs.length !== this._pins.outputs.length) { throw new SimulationError( `Output count needs to match with the container gate: ${ outputs.length } !== ${this._pins.outputs.length}` ) } const wrappedInputs = this.wrapPins(this._pins.inputs) const wrappedOutputs = this.wrapPins(this._pins.outputs) for (let index = 0; index < inputs.length; index++) { this.ghostWires.push( new Wire(wrappedInputs[index], inputs[index], true) ) } for (let index = 0; index < outputs.length; index++) { this.ghostWires.push( new Wire(outputs[index], wrappedOutputs[index], true) ) } this.ghostSimulation.wires.push(...this.ghostWires) } this.assignProps(props) } /** * Assign the props passed to the gate and mere them with the base ones */ private assignProps(props: Record<string, string | boolean | number>) { let shouldUpdate = false if (this.template.properties.enabled) { for (const { base, name, needsUpdate } of this.template.properties .data) { this.props[name] = new BehaviorSubject( props.hasOwnProperty(name) ? props[name] : base ) if (!shouldUpdate && needsUpdate) { shouldUpdate = true } } } if (shouldUpdate) { this.update() } } /** * Runs the init function from the template */ private init() { toFunction<[InitialisationContext]>( this.template.code.initialisation, 'context' )({ memory: this.memory }) } /** * Runs the onClick function from the template */ public onClick() { if (this.functions.onClick) { this.functions.onClick(this.getContext()) } } /** * Used to get the props as an object */ public getProps() { const props: Record<string, string | boolean | number> = {} for (const key in this.props) { props[key] = this.props[key].value } return props } /** * Clears subscriptions to prevent memory leaks */ public dispose() { for (const pin of this.pins) { pin.value.dispose() } for (const subscription of this.subscriptions) { subscription.unsubscribe() } if (this.isIntegrated) { this.ghostSimulation.dispose() } } /** * Runs the activation function from the template */ public update() { if (!this.template.tags.includes('integrated')) { const context = this.getContext() if (!this.functions.activation) throw new SimulationError('Activation function is missing') return this.functions.activation(context) } } /** * Generates the activation context */ public getContext(): Context { return { get: (index: number) => { return this._pins.inputs[index].state.value }, set: (index: number, state: boolean = false) => { return this._pins.outputs[index].state.next(state) }, color: (color: string) => { if (this.template.material.type === 'color') { this.template.material.fill = color } }, getProperty: (name: string) => { return this.props[name].value }, setProperty: (name: string, value: string | number | boolean) => { this.props[name].next(value) }, innerText: (value: string) => { this.text.inner.next(value) }, update: () => { this.update() }, enviroment: this.env, memory: this.memory, colors: { ...this.template.material.colors } } } /** * Generates pin wrappers from an array of pins * * @param pins The pins to wwap */ private wrapPins(pins: Pin[]) { const result: PinWrapper[] = [] const length = pins.length for (let index = 0; index < length; index++) { result.push({ index, total: length, value: pins[index] }) } return result } /** * Returns all pins (input + output) */ public get pins() { const result = [ ...this.wrapPins(this._pins.inputs), ...this.wrapPins(this._pins.outputs) ] return result } /** * Generates empty pins for any gate */ private static generatePins(options: PinCount, type: number, gate: Gate) { return [...Array(options.count)] .fill(true) .map(() => new Pin(type, gate)) } }