From f06fe88df5cbed4bc2f694ec2873f57afb2c9254 Mon Sep 17 00:00:00 2001
From: Matei Adriel <rafaeladriel11@gmail.com>
Date: Mon, 29 Jul 2019 15:41:37 +0300
Subject: [PATCH] fixed zomming and added 2 new delayers gate

---
 src/assets/parallel.svg                       |   6 +
 src/assets/sequential.svg                     |   7 +
 .../activation/classes/ExecutionQueue.ts      |  72 ++++++++++
 src/modules/saving/constants.ts               |  43 ++++++
 src/modules/saving/helpers/getState.ts        |   3 +-
 src/modules/saving/types/SimulationSave.ts    |   1 +
 .../simulation-actions/helpers/duplicate.ts   |   5 -
 src/modules/simulation/classes/Gate.ts        | 135 +++++++++++++++++-
 src/modules/simulation/constants.ts           |   6 +-
 src/modules/simulation/types/GateTemplate.ts  |   9 ++
 .../classes/SimulationRenderer.ts             |  23 +--
 src/modules/simulationRenderer/constants.ts   |   2 +-
 .../simulationRenderer/helpers/scaleCanvas.ts |  52 ++++---
 src/modules/vector2/helpers/basic.ts          |  25 +++-
 14 files changed, 342 insertions(+), 47 deletions(-)
 create mode 100644 src/assets/parallel.svg
 create mode 100644 src/assets/sequential.svg
 create mode 100644 src/modules/activation/classes/ExecutionQueue.ts

diff --git a/src/assets/parallel.svg b/src/assets/parallel.svg
new file mode 100644
index 0000000..131a519
--- /dev/null
+++ b/src/assets/parallel.svg
@@ -0,0 +1,6 @@
+<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="800" height="800" fill="#FF7700"/>
+<path d="M181 284H273.673L342.933 367H424.875L503.241 284H619" stroke="white" stroke-width="10"/>
+<path d="M181 516H273.461L342.563 438H424.318L502.505 516H618" stroke="white" stroke-width="10"/>
+<path d="M432.5 400C432.5 429.426 410.144 452.5 383.5 452.5C356.856 452.5 334.5 429.426 334.5 400C334.5 370.574 356.856 347.5 383.5 347.5C410.144 347.5 432.5 370.574 432.5 400Z" fill="#FF7700" stroke="white" stroke-width="15"/>
+</svg>
diff --git a/src/assets/sequential.svg b/src/assets/sequential.svg
new file mode 100644
index 0000000..8a85582
--- /dev/null
+++ b/src/assets/sequential.svg
@@ -0,0 +1,7 @@
+<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="800" height="800" fill="#FF7700"/>
+<path d="M171 274H274.688L352.182 362.006H443.864L533 274V404" stroke="white" stroke-width="10"/>
+<path d="M171 526H274.674L352.157 444.355H443.827L582 526V409" stroke="white" stroke-width="10"/>
+<circle cx="398.5" cy="403.5" r="56" fill="#FF7700" stroke="white" stroke-width="15"/>
+<path d="M457.693 407.304L629.998 411.373" stroke="white" stroke-width="10"/>
+</svg>
diff --git a/src/modules/activation/classes/ExecutionQueue.ts b/src/modules/activation/classes/ExecutionQueue.ts
new file mode 100644
index 0000000..45a519c
--- /dev/null
+++ b/src/modules/activation/classes/ExecutionQueue.ts
@@ -0,0 +1,72 @@
+import { Subject } from 'rxjs'
+import { filter, take } from 'rxjs/operators'
+
+/**
+ * Keeps track of what a task should do and where the output should be delivered
+ */
+export interface Task<T> {
+    output: Subject<T>
+    execute: () => Promise<T>
+}
+
+/**
+ * Used to execute a number of async tasks
+ */
+export class ExecutionQueue<T> {
+    /**
+     * An array of all the tasks wich need to be executed
+     */
+    private tasks: Task<T>[] = []
+
+    /**
+     * Keeps track of the current task
+     */
+    private current: Promise<T> | null
+
+    /**
+     * Wheather the tasks should continue beeing executed
+     */
+    public active = true
+
+    /**
+     * Adds a new task to the queue
+     *
+     * @param task The task to add
+     */
+    public push(task: () => Promise<T>) {
+        const executionSubject = new Subject<T>()
+        const executionPromise = executionSubject.pipe(take(1)).toPromise()
+
+        this.tasks.push({
+            output: executionSubject,
+            execute: task
+        })
+
+        if (!this.current) {
+            this.next()
+        }
+
+        return executionPromise
+    }
+
+    /**
+     * Executes the next task in the queue
+     */
+    private next() {
+        const task = this.tasks.shift()
+
+        if (task) {
+            this.current = task.execute()
+
+            this.current.then(result => {
+                task.output.next(result)
+
+                if (this.active) {
+                    this.next()
+                }
+            })
+        } else {
+            this.current = null
+        }
+    }
+}
diff --git a/src/modules/saving/constants.ts b/src/modules/saving/constants.ts
index 126d674..4c5d5c5 100644
--- a/src/modules/saving/constants.ts
+++ b/src/modules/saving/constants.ts
@@ -243,6 +243,49 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
                 count: 3
             }
         }
+    },
+    {
+        metadata: {
+            name: 'Sequential delayer'
+        },
+        material: {
+            type: 'image',
+            fill: require('../../assets/sequential')
+        },
+        code: {
+            activation: `
+                const i = context.get(0)
+                return new Promise((res, rej) => {
+                    setTimeout(() => {
+                        res()
+                    },1000)
+                }).then(() => {
+                    context.set(0,i)
+                })
+            `,
+            async: true
+        }
+    },
+    {
+        metadata: {
+            name: 'Parallel delayer'
+        },
+        material: {
+            type: 'image',
+            fill: require('../../assets/parallel')
+        },
+        code: {
+            activation: `
+                const i = context.get(0)
+                return new Promise((res, rej) => {
+                    setTimeout(() => {
+                        res()
+                    },1000)
+                }).then(() => {
+                    context.set(0,i)
+                })
+            `
+        }
     }
 ]
 
diff --git a/src/modules/saving/helpers/getState.ts b/src/modules/saving/helpers/getState.ts
index 1cc1892..22af919 100644
--- a/src/modules/saving/helpers/getState.ts
+++ b/src/modules/saving/helpers/getState.ts
@@ -61,7 +61,8 @@ export const getGateState = (gate: Gate): GateState => {
     return {
         id: gate.id,
         template: gate.template.metadata.name,
-        transform: getTransformState(gate.transform)
+        transform: getTransformState(gate.transform),
+        props: gate.props
     }
 }
 
diff --git a/src/modules/saving/types/SimulationSave.ts b/src/modules/saving/types/SimulationSave.ts
index fe30e60..9a502c2 100644
--- a/src/modules/saving/types/SimulationSave.ts
+++ b/src/modules/saving/types/SimulationSave.ts
@@ -12,6 +12,7 @@ export interface GateState {
     transform: TransformState
     id: number
     template: string
+    props: Record<string, unknown>
 }
 
 export interface CameraState {
diff --git a/src/modules/simulation-actions/helpers/duplicate.ts b/src/modules/simulation-actions/helpers/duplicate.ts
index cc70d73..f619aff 100644
--- a/src/modules/simulation-actions/helpers/duplicate.ts
+++ b/src/modules/simulation-actions/helpers/duplicate.ts
@@ -3,11 +3,6 @@ import { copy } from './copy'
 import { paste } from './paste'
 
 export const duplicate = (renderer: SimulationRenderer) => {
-    const { clipboard, wireClipboard } = renderer
-
     copy(renderer)
     paste(renderer)
-
-    renderer.clipboard = clipboard
-    renderer.wireClipboard = wireClipboard
 }
diff --git a/src/modules/simulation/classes/Gate.ts b/src/modules/simulation/classes/Gate.ts
index 4b2e9a0..734489e 100644
--- a/src/modules/simulation/classes/Gate.ts
+++ b/src/modules/simulation/classes/Gate.ts
@@ -14,50 +14,123 @@ 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'
 
+/**
+ * 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> = {}
 
-    // Related to integration
+    /**
+     * 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'
 
-    public constructor(template: DeepPartial<GateTemplate> = {}, id?: number) {
+    /**
+     * The props used by the activation function (the same as memory but presists)
+     */
+    public props: Record<string, unknown> = {}
+
+    /**
+     * 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, unknown> = {}
+    ) {
         this.template = completeTemplate(template)
 
         this.transform.scale = this.template.shape.scale
@@ -97,7 +170,13 @@ export class Gate {
             const pipes = getGateTimePipes(this.template)
 
             const subscription = pin.state.pipe(...pipes).subscribe(() => {
-                this.update()
+                if (this.template.code.async) {
+                    this.executionQueue.push(async () => {
+                        return await this.update()
+                    })
+                } else {
+                    this.update()
+                }
             })
 
             this.subscriptions.push(subscription)
@@ -173,8 +252,29 @@ export class Gate {
 
             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, unknown>) {
+        if (this.template.properties.enabled) {
+            for (const prop in this.template.properties) {
+                if (prop !== 'enabled') {
+                    this.props[prop] =
+                        props[prop] !== undefined
+                            ? props[prop]
+                            : this.template.properties[prop].base
+                }
+            }
+        }
+    }
+
+    /**
+     * Runs the init function from the template
+     */
     private init() {
         toFunction<[InitialisationContext]>(
             this.template.code.initialisation,
@@ -184,12 +284,18 @@ export class Gate {
         })
     }
 
+    /**
+     * Runs the onClick function from the template
+     */
     public onClick() {
         if (this.functions.onClick) {
             this.functions.onClick(this.getContext())
         }
     }
 
+    /**
+     * Clears subscriptions to prevent memory leaks
+     */
     public dispose() {
         for (const pin of this.pins) {
             pin.value.dispose()
@@ -204,18 +310,23 @@ export class Gate {
         }
     }
 
+    /**
+     * Runs the activation function from the template
+     */
     public update() {
-        if (this.template.tags.includes('integrated')) {
-        } else {
+        if (!this.template.tags.includes('integrated')) {
             const context = this.getContext()
 
             if (!this.functions.activation)
                 throw new SimulationError('Activation function is missing')
 
-            this.functions.activation(context)
+            return this.functions.activation(context)
         }
     }
 
+    /**
+     * Generates the activation context
+     */
     public getContext(): Context {
         return {
             get: (index: number) => {
@@ -237,7 +348,11 @@ export class Gate {
         }
     }
 
-    private wrapPins(pins: Pin[]) {
+    /**
+     * 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
 
@@ -252,6 +367,9 @@ export class Gate {
         return result
     }
 
+    /**
+     * Returns all pins (input + output)
+     */
     public get pins() {
         const result = [
             ...this.wrapPins(this._pins.inputs),
@@ -261,6 +379,9 @@ export class Gate {
         return result
     }
 
+    /**
+     * Generates empty pins for any gate
+     */
     private static generatePins(options: PinCount, type: number, gate: Gate) {
         return [...Array(options.count)]
             .fill(true)
diff --git a/src/modules/simulation/constants.ts b/src/modules/simulation/constants.ts
index d5db891..39062ab 100644
--- a/src/modules/simulation/constants.ts
+++ b/src/modules/simulation/constants.ts
@@ -29,6 +29,7 @@ export const DefaultGateTemplate: GateTemplate = {
         scale: [100, 100]
     },
     code: {
+        async: false,
         activation: '',
         onClick: '',
         initialisation: ''
@@ -48,5 +49,8 @@ export const DefaultGateTemplate: GateTemplate = {
         output: false
     },
     info: [],
-    tags: ['base']
+    tags: ['base'],
+    properties: {
+        enabled: false
+    }
 }
diff --git a/src/modules/simulation/types/GateTemplate.ts b/src/modules/simulation/types/GateTemplate.ts
index 95f6441..24ceec1 100644
--- a/src/modules/simulation/types/GateTemplate.ts
+++ b/src/modules/simulation/types/GateTemplate.ts
@@ -1,10 +1,17 @@
 import { vector2 } from '../../../common/math/classes/Transform'
+import { InputHTMLAttributes } from 'react'
 
 export interface PinCount {
     variable: boolean
     count: number
 }
 
+export interface Property<T> {
+    type: HTMLInputElement['type']
+    encode: (value: string) => T
+    base: T
+}
+
 export interface Material {
     type: 'color' | 'image'
     fill: string
@@ -46,6 +53,7 @@ export interface GateTemplate {
         name: string
     }
     code: {
+        async: boolean
         initialisation: string
         activation: string
         onClick: string
@@ -61,4 +69,5 @@ export interface GateTemplate {
     }
     info: string[]
     tags: GateTag[]
+    properties: Enabled<Record<Exclude<string, 'enabled'>, Property<unknown>>>
 }
diff --git a/src/modules/simulationRenderer/classes/SimulationRenderer.ts b/src/modules/simulationRenderer/classes/SimulationRenderer.ts
index dd719f6..68c0d0a 100644
--- a/src/modules/simulationRenderer/classes/SimulationRenderer.ts
+++ b/src/modules/simulationRenderer/classes/SimulationRenderer.ts
@@ -22,7 +22,7 @@ import {
     fromCameraState
 } from '../../saving/helpers/fromState'
 import merge from 'deepmerge'
-import { updateMouse, handleScroll } from '../helpers/scaleCanvas'
+import { handleScroll } from '../helpers/scaleCanvas'
 import { RefObject } from 'react'
 import { dumpSimulation } from '../../saving/helpers/dumpSimulation'
 import { modalIsOpen } from '../../modals/helpers/modalIsOpen'
@@ -55,7 +55,7 @@ export class SimulationRenderer {
     public wireClipboard: WireState[] = []
 
     // first bit = dragging
-    // second bit = panning around
+    // second bit = panning
     // third bit = selecting
     public mouseState = 0b000
 
@@ -200,7 +200,7 @@ export class SimulationRenderer {
         })
 
         this.mouseUpOutput.subscribe(event => {
-            if (event.button === mouseButtons.drag) {
+            if (event.button === mouseButtons.drag && this.mouseState & 1) {
                 const selected = this.getSelected()
 
                 for (const gate of selected) {
@@ -209,8 +209,8 @@ export class SimulationRenderer {
 
                 this.selectedGates.temporary.clear()
 
-                // turn first 2 bits to 0
-                this.mouseState &= 1 << 2
+                // turn first bit to 0
+                this.mouseState &= 6
 
                 // for debugging
                 if ((this.mouseState >> 1) & 1 || this.mouseState & 1) {
@@ -221,11 +221,16 @@ export class SimulationRenderer {
             }
 
             if (
-                event.button === mouseButtons.select &&
-                (this.mouseState >> 2) & 1
+                event.button === mouseButtons.pan &&
+                (this.mouseState >> 1) & 1
             ) {
+                // turn second bit to 0
+                this.mouseState &= 5
+            }
+
+            if (event.button === mouseButtons.select && this.mouseState >> 2) {
                 // turn the third bit to 0
-                this.mouseState &= (1 << 2) - 1
+                this.mouseState &= 3
 
                 const selectedGates = gatesInSelection(
                     this.selectedArea,
@@ -249,8 +254,6 @@ export class SimulationRenderer {
         })
 
         this.mouseMoveOutput.subscribe(event => {
-            updateMouse(event)
-
             const worldPosition = this.camera.toWordPostition(event.position)
 
             const offset = invert(
diff --git a/src/modules/simulationRenderer/constants.ts b/src/modules/simulationRenderer/constants.ts
index 6645f8c..cf9b32f 100644
--- a/src/modules/simulationRenderer/constants.ts
+++ b/src/modules/simulationRenderer/constants.ts
@@ -42,7 +42,7 @@ export const mouseButtons: Record<
     mouseButton
 > = {
     zoom: 1,
-    drag: 2,
+    drag: 0,
     pan: 2,
     select: 0,
     unselect: 0
diff --git a/src/modules/simulationRenderer/helpers/scaleCanvas.ts b/src/modules/simulationRenderer/helpers/scaleCanvas.ts
index 2757710..902e21b 100644
--- a/src/modules/simulationRenderer/helpers/scaleCanvas.ts
+++ b/src/modules/simulationRenderer/helpers/scaleCanvas.ts
@@ -1,37 +1,47 @@
 import { clamp } from '../../simulation/helpers/clamp'
 import { Camera } from '../classes/Camera'
 import { vector2 } from '../../../common/math/classes/Transform'
-import { MouseEventInfo } from '../../core/components/MouseEventInfo'
-import { Screen } from '../../screen/helpers/Screen'
+import { multiply, substract } from '../../vector2/helpers/basic'
+import { repeat } from '../../vector2/helpers/repeat'
 
 const scrollStep = 1.3
 const zoomLimits = [0.1, 10]
 
-let absoluteMousePosition = [Screen.width / 2, Screen.height]
+/*
+f(x) = (a - x) / b(x)
 
-export const updateMouse = (e: MouseEventInfo) => {
-    absoluteMousePosition = e.position
-}
+f(x0) = f(x1)
 
+(a - x0) / b(x0) = (a - x1) / b(x1)
+
+b = b(x1) / b(x0)
+
+b * (a - x0) = a - x1
+x1 = a - b * (a - x0)
+*/
 export const handleScroll = (e: WheelEvent, camera: Camera) => {
     const sign = -e.deltaY / Math.abs(e.deltaY)
     const zoom = scrollStep ** sign
 
-    const mouseFraction = Screen.scale.map(
-        (value, index) => absoluteMousePosition[index] / value
-    )
-    const newScale = camera.transform.scale.map(value =>
-        clamp(zoomLimits[0], zoomLimits[1], value * zoom)
-    )
-    const delta = camera.transform.scale.map(
-        (value, index) =>
-            Screen.scale[index] *
-            (newScale[index] - value) *
-            mouseFraction[index]
+    if (!e.deltaY) {
+        return
+    }
+
+    const { position, scale } = camera.transform
+
+    const mousePosition = [e.clientX, e.clientY]
+    const oldPosition = [...mousePosition] as vector2
+
+    const oldScale = scale[0]
+    const newScale = clamp(zoomLimits[0], zoomLimits[1], oldScale * zoom)
+
+    camera.transform.scale = repeat(newScale, 2) as vector2
+
+    const scaleFraction = newScale / oldScale
+    const newPosition = substract(
+        oldPosition,
+        multiply(substract(oldPosition, position), scaleFraction)
     )
 
-    camera.transform.scale = newScale as vector2
-    camera.transform.position = camera.transform.position.map(
-        (value, index) => value - delta[index]
-    ) as vector2
+    camera.transform.position = newPosition
 }
diff --git a/src/modules/vector2/helpers/basic.ts b/src/modules/vector2/helpers/basic.ts
index d291b76..c273977 100644
--- a/src/modules/vector2/helpers/basic.ts
+++ b/src/modules/vector2/helpers/basic.ts
@@ -23,6 +23,15 @@ export const length = (vector: vector2) =>
 export const multiply = (vector: vector2, scalar: number) =>
     vector.map(val => val * scalar) as vector2
 
+/**
+ * Multiplies 2 vectors
+ *
+ * @param vector The first vector to multiply
+ * @param other The second vector to multiply
+ */
+export const multiplyVectors = (vector: vector2, other: vector2) =>
+    vector.map((position, index) => position * other[index]) as vector2
+
 // This makese the length of the vector 1
 export const normalise = (vector: vector2) => {
     const size = length(vector)
@@ -35,8 +44,22 @@ export const ofLength = (vector: vector2, l: number) => {
     return multiply(vector, l / length(vector))
 }
 
-// This returns a vector relative to the other
+/**
+ * Moves a vector relative to another
+ *
+ * @param vector The vector t move relative to something
+ * @param other The vector for the first one to be mvoed relative to
+ */
 export const relativeTo = (vector: vector2, other: vector2) =>
     add(other, invert(vector))
 
+/**
+ * Subtracts a vector from another
+ *
+ * @param vector Hhe vector to substruact from
+ * @param other The vector to subtract
+ */
+export const substract = (vector: vector2, other: vector2) =>
+    relativeTo(other, vector)
+
 export const inverse = (vector: vector2) => vector.map(a => 1 / a) as vector2