fixed zomming and added 2 new delayers gate
This commit is contained in:
parent
2ce1919649
commit
f06fe88df5
6
src/assets/parallel.svg
Normal file
6
src/assets/parallel.svg
Normal 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 |
7
src/assets/sequential.svg
Normal file
7
src/assets/sequential.svg
Normal 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 |
72
src/modules/activation/classes/ExecutionQueue.ts
Normal file
72
src/modules/activation/classes/ExecutionQueue.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ export interface GateState {
|
|||
transform: TransformState
|
||||
id: number
|
||||
template: string
|
||||
props: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface CameraState {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>>
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -42,7 +42,7 @@ export const mouseButtons: Record<
|
|||
mouseButton
|
||||
> = {
|
||||
zoom: 1,
|
||||
drag: 2,
|
||||
drag: 0,
|
||||
pan: 2,
|
||||
select: 0,
|
||||
unselect: 0
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue