erratic-gate/src/modules/simulation/classes/Gate.ts
2019-07-30 23:58:21 +03:00

435 lines
12 KiB
TypeScript

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))
}
}