560 lines
14 KiB
TypeScript
560 lines
14 KiB
TypeScript
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))
|
|
}
|
|
}
|