From f06fe88df5cbed4bc2f694ec2873f57afb2c9254 Mon Sep 17 00:00:00 2001 From: Matei Adriel 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 @@ + + + + + + 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 @@ + + + + + + + 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 { + output: Subject + execute: () => Promise +} + +/** + * Used to execute a number of async tasks + */ +export class ExecutionQueue { + /** + * An array of all the tasks wich need to be executed + */ + private tasks: Task[] = [] + + /** + * Keeps track of the current task + */ + private current: Promise | 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) { + const executionSubject = new Subject() + 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[] = [ 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 } 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() + + /** + * 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 = {} - // 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 = {}, id?: number) { + /** + * The props used by the activation function (the same as memory but presists) + */ + public props: Record = {} + + /** + * 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 = {}, + id?: number, + props: Record = {} + ) { 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) { + 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 { + 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, Property>> } 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