import { Transform } from '../../../common/math/classes/Transform' import { Pin } from './Pin' import { GateTemplate, PinCount, isGroup, Property } 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 { PropsSave } from '../../saving/types/SimulationSave' import { ValueOf } from '../../../common/lang/record/types/ValueOf' /** * 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 type GateProps = { [K in keyof PropsSave]: BehaviorSubject<PropsSave[K]> | GateProps } & { external: BehaviorSubject<boolean> label: BehaviorSubject<string> } 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<string | null>(null) } /** * The props used by the activation function (the same as memory but presists) */ public props: GateProps = {} as GateProps /** * 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: PropsSave = {} ) { 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 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) } private updateNestedProp( path: string[], value: ValueOf<PropsSave>, gate: Gate = this ) { if (!path.length) { return } if (path.length === 1) { const subject = gate.props[path[0]] if (subject instanceof BehaviorSubject) { subject.next(value) } return } const nextGates = [...gate.ghostSimulation.gates].filter( (gate) => gate.props?.label?.value === path[0] ) for (const nextGate of nextGates) { this.updateNestedProp(path.slice(1), value, nextGate) } } /** * Assign the props passed to the gate and merge them with the base ones */ private assignProps( source: PropsSave, props: Property[] = this.template.properties.data, target: GateProps = this.props, path: string[] = [] ) { // We don't want to update until every prop has been created let lockUpdates = true if (this.template.properties.enabled) { for (const prop of props) { if (isGroup(prop)) { const { groupName } = prop target[groupName] = {} as GateProps this.assignProps( typeof source[groupName] === 'object' ? (source[groupName] as PropsSave) : {}, prop.props, target[groupName] as GateProps, [...path, groupName] ) continue } const { name, base, needsUpdate } = prop const subject = new BehaviorSubject( source.hasOwnProperty(name) ? source[name] : base ) target[name] = subject this.subscriptions.push( subject.subscribe((value) => { if (!lockUpdates && needsUpdate && path.length === 0) { return this.update() } if (path.length === 0) { return } this.updateNestedProp([...path, name], value) }) ) } } lockUpdates = false 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(target = this.props) { const props: PropsSave = {} for (const [key, value] of Object.entries(target)) { if (value instanceof BehaviorSubject) { props[key] = value.value } else { props[key] = this.getProps(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 { const maxLength = Math.max( ...this._pins.inputs.map((pin) => pin.state.value.length) ) const toLength = ( original: string | number, length: number = maxLength, paddingChar = '0' ) => { const value = original.toString(2) if (value.length === length) { return value } else if (value.length > length) { const difference = value.length - length return value.slice(difference) } else { return `${paddingChar.repeat(length - value.length)}${value}` } } const context: Context = { printBinary: (value: number, bits: number = maxLength) => toLength(value.toString(2), bits), printHex: (value: number, bits: number = maxLength) => toLength(value.toString(16), bits), displayBinary: (value: number) => { const length = value.toString(2).length const text = length > 10 ? '0x' + context.printHex(value, Math.ceil(length / 4)) : context.printBinary(value, length) context.innerText(text) }, get: (index: number) => { return this._pins.inputs[index].state.value }, set: (index: number, state) => { return this._pins.outputs[index].state.next(state) }, getOutput: (index: number) => { return this._pins.outputs[index].state.value }, getBinary: (index: number) => { return parseInt(this._pins.inputs[index].state.value, 2) }, setBinary: (index: number, value: number, bits: number = maxLength) => { return this._pins.outputs[index].state.next( toLength(value.toString(2), bits) ) }, getOutputBinary: (index: number) => { return parseInt(this._pins.outputs[index].state.value, 2) }, invertBinary: (value: number) => { return value ^ ((1 << maxLength) - 1) }, color: (color: string) => { if (this.template.material.type === 'color') { this.template.material.fill = color } }, getProperty: (name: string) => { if (this.props[name] === undefined) { throw new Error( [ `Cannot find property ${name} on gate ${this.template.metadata.name}.`, `Current values: ${Object.keys(this.props)}` ].join('\n') ) } else { return this.props[name].value } }, setProperty: (name: string, value: string | number | boolean) => { const subject = this.props[name] if (subject instanceof BehaviorSubject) { subject.next(value) } }, innerText: (value: string) => { this.text.inner.next(value) }, update: () => { this.update() }, toLength, maxLength, enviroment: this.env, memory: this.memory, colors: { ...this.template.material.colors } } return context } /** * 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((_v, _index) => new Pin(type, gate)) } }