fixed zomming and added 2 new delayers gate

This commit is contained in:
Matei Adriel 2019-07-29 15:41:37 +03:00
parent 2ce1919649
commit f06fe88df5
14 changed files with 342 additions and 47 deletions

6
src/assets/parallel.svg Normal file
View file

@ -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>

After

Width:  |  Height:  |  Size: 579 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 516 B

View file

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

View file

@ -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)
})
`
}
}
]

View file

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

View file

@ -12,6 +12,7 @@ export interface GateState {
transform: TransformState
id: number
template: string
props: Record<string, unknown>
}
export interface CameraState {

View file

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

View file

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

View file

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

View file

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

View file

@ -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(

View file

@ -42,7 +42,7 @@ export const mouseButtons: Record<
mouseButton
> = {
zoom: 1,
drag: 2,
drag: 0,
pan: 2,
select: 0,
unselect: 0

View file

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

View file

@ -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