Added the basics of keybindings + zooming

This commit is contained in:
Matei Adriel 2019-07-19 15:25:01 +03:00
parent 970d43187d
commit 057c2268ac
49 changed files with 1059 additions and 76 deletions

View file

@ -1,6 +1,5 @@
{
"eslint.enable": true,
"editor.formatOnSave": true,
"prettier.eslintIntegration": true,
"eslint.validate": ["typescript", "typescriptreact"]
}
"editor.formatOnSave": true,
"prettier.eslintIntegration": true,
"explorer.autoReveal": false
}

251
package-lock.json generated
View file

@ -1463,6 +1463,114 @@
"resolved": "https://registry.npmjs.org/@eix-js/utils/-/utils-0.0.6.tgz",
"integrity": "sha512-VyxwQAN5bNKmSzafo9Ma9nNDdVqxrN+ikp9SqC/OyvbAyihfZm17R8yjexXnIyfGeZstRAuUvSIw1bUzrL+RqA=="
},
"@emotion/hash": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.2.tgz",
"integrity": "sha512-RMtr1i6E8MXaBWwhXL3yeOU8JXRnz8GNxHvaUfVvwxokvayUY0zoBeWbKw1S9XkufmGEEdQd228pSZXFkAln8Q=="
},
"@material-ui/core": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.2.1.tgz",
"integrity": "sha512-hasPQUFAb9OxKng7UX2+SjUWtVZbnkVJ/jHZWXTivVcU+UzvNIpA9AyRRQvZ8SPV6swP/HD2VzUBzoMEeRR6wg==",
"requires": {
"@babel/runtime": "^7.2.0",
"@material-ui/styles": "^4.2.0",
"@material-ui/system": "^4.3.0",
"@material-ui/types": "^4.1.1",
"@material-ui/utils": "^4.1.0",
"@types/react-transition-group": "^2.0.16",
"clsx": "^1.0.2",
"convert-css-length": "^2.0.1",
"deepmerge": "^4.0.0",
"hoist-non-react-statics": "^3.2.1",
"is-plain-object": "^3.0.0",
"normalize-scroll-left": "^0.2.0",
"popper.js": "^1.14.1",
"prop-types": "^15.7.2",
"react-transition-group": "^4.0.0",
"warning": "^4.0.1"
},
"dependencies": {
"is-plain-object": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
"requires": {
"isobject": "^4.0.0"
}
},
"isobject": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
},
"react-transition-group": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.2.1.tgz",
"integrity": "sha512-IXrPr93VzCPupwm2O6n6C2kJIofJ/Rp5Ltihhm9UfE8lkuVX2ng/SUUl/oWjblybK9Fq2Io7LGa6maVqPB762Q==",
"requires": {
"@babel/runtime": "^7.4.5",
"dom-helpers": "^3.4.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
}
}
},
"@material-ui/styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.2.1.tgz",
"integrity": "sha512-1KSOZ17LBWBqIyPRsEpyb4snT/wRIfQTPi0x66UvSzznVK9MPAfJx3/s5lVT4vrGFObs/nj6Pet6Nhrdl2WCrg==",
"requires": {
"@babel/runtime": "^7.2.0",
"@emotion/hash": "^0.7.1",
"@material-ui/types": "^4.1.1",
"@material-ui/utils": "^4.1.0",
"clsx": "^1.0.2",
"csstype": "^2.5.2",
"deepmerge": "^4.0.0",
"hoist-non-react-statics": "^3.2.1",
"jss": "10.0.0-alpha.17",
"jss-plugin-camel-case": "10.0.0-alpha.17",
"jss-plugin-default-unit": "10.0.0-alpha.17",
"jss-plugin-global": "10.0.0-alpha.17",
"jss-plugin-nested": "10.0.0-alpha.17",
"jss-plugin-props-sort": "10.0.0-alpha.17",
"jss-plugin-rule-value-function": "10.0.0-alpha.17",
"jss-plugin-vendor-prefixer": "10.0.0-alpha.17",
"prop-types": "^15.7.2",
"warning": "^4.0.1"
}
},
"@material-ui/system": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.3.1.tgz",
"integrity": "sha512-Krrc/p/A3rod4M3FYcsWSqE5KxpoyMzYuUHhs0Pns3KH+5kcFyBU+aYbIzMfUz58rhbHkqrShf1fjj7EKcgY0g==",
"requires": {
"@babel/runtime": "^7.2.0",
"deepmerge": "^4.0.0",
"prop-types": "^15.7.2",
"warning": "^4.0.1"
}
},
"@material-ui/types": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-4.1.1.tgz",
"integrity": "sha512-AN+GZNXytX9yxGi0JOfxHrRTbhFybjUJ05rnsBVjcB+16e466Z0Xe5IxawuOayVZgTBNDxmPKo5j4V6OnMtaSQ==",
"requires": {
"@types/react": "*"
}
},
"@material-ui/utils": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.1.0.tgz",
"integrity": "sha512-muwmVU799tzPjzb+Q5E/CTDle0rXwkCAdvMVyU0BfbJhenkUsFmuYiCmbvMVOU1m6F1S5HWfXz8EP4pXwwAvrw==",
"requires": {
"@babel/runtime": "^7.2.0",
"prop-types": "^15.7.2",
"react-is": "^16.8.0"
}
},
"@types/deepmerge": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/deepmerge/-/deepmerge-2.2.0.tgz",
@ -1522,8 +1630,7 @@
"@types/prop-types": {
"version": "15.7.1",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz",
"integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==",
"dev": true
"integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg=="
},
"@types/q": {
"version": "1.5.2",
@ -1535,7 +1642,6 @@
"version": "16.8.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.23.tgz",
"integrity": "sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"csstype": "^2.2.0"
@ -1562,6 +1668,14 @@
"@types/react-router": "*"
}
},
"@types/react-transition-group": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.9.2.tgz",
"integrity": "sha512-5Fv2DQNO+GpdPZcxp2x/OQG/H19A01WlmpjVD9cKvVFmoVLOZ9LvBgSWG6pSXIU4og5fgbvGPaCV5+VGkWAEHA==",
"requires": {
"@types/react": "*"
}
},
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -2723,6 +2837,11 @@
"shallow-clone": "^1.0.0"
}
},
"clsx": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.0.4.tgz",
"integrity": "sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg=="
},
"coa": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
@ -2939,6 +3058,11 @@
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"dev": true
},
"convert-css-length": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/convert-css-length/-/convert-css-length-2.0.1.tgz",
"integrity": "sha512-iGpbcvhLPRKUbBc0Quxx7w/bV14AC3ItuBEGMahA5WTYqB8lq9jH0kTXFheCBASsYnqeMFZhiTruNxr1N59Axg=="
},
"convert-source-map": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
@ -3191,6 +3315,15 @@
"integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=",
"dev": true
},
"css-vendor": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.5.tgz",
"integrity": "sha512-36w+4Cg0zqFIt5TAkaM3proB6XWh5kSGmbddRCPdrRLQiYNfHPTgaWPOlCrcuZIO0iAtrG+5wsHJZ6jj8AUULA==",
"requires": {
"@babel/runtime": "^7.3.1",
"is-in-browser": "^1.0.2"
}
},
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
@ -3310,8 +3443,7 @@
"csstype": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz",
"integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==",
"dev": true
"integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg=="
},
"currently-unhandled": {
"version": "0.4.1",
@ -5528,6 +5660,11 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"hyphenate-style-name": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz",
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -5833,6 +5970,11 @@
"is-extglob": "^2.1.1"
}
},
"is-in-browser": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
},
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@ -6083,6 +6225,87 @@
"verror": "1.10.0"
}
},
"jss": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss/-/jss-10.0.0-alpha.17.tgz",
"integrity": "sha512-egGIUg+YRu0+U+XXlD0gmVtU/gW5sn7+qmDv7opwK5s8emZBE/VoN55X6CaMrAa0kLeGMldnI43KOWea6M9/mA==",
"requires": {
"@babel/runtime": "^7.3.1",
"is-in-browser": "^1.1.3",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-camel-case": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.0-alpha.17.tgz",
"integrity": "sha512-aPY4kr6MwliH7KToLRzeSk1NxXUo9n7MQsAa0Hghwj01x9UnMkDkGAKENMKUtPjGkQZfiJpB9tTLFrSJ/6VrIQ==",
"requires": {
"@babel/runtime": "^7.3.1",
"hyphenate-style-name": "^1.0.3",
"jss": "10.0.0-alpha.17"
}
},
"jss-plugin-default-unit": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.0.0-alpha.17.tgz",
"integrity": "sha512-KQgiXczvzJ9AlFdD8NS7FZLub0NSctSrCA9Yi/GqdsfJg4ZCriU4DzIybCZBHCi/INFGJmLIESYWSxnuhAzgSQ==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.0.0-alpha.17"
}
},
"jss-plugin-global": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.0.0-alpha.17.tgz",
"integrity": "sha512-WYxiwwI+CLk0ozW8loeceqXBAZXBMsLBEZeRwVf9WX+FljdJkGwVZpRCk6LBX4aXnqAGyKqCxIAIJ3KP2yBdEg==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.0.0-alpha.17"
}
},
"jss-plugin-nested": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.0.0-alpha.17.tgz",
"integrity": "sha512-onpFqv904KCujryf2t6IIV1/QoB7cSF7ojrd4UujcN5TPvYOvXF5bchi7jnHG5U0SLlRSDGMLJ9fhtoCknhEbw==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.0.0-alpha.17",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-props-sort": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.0.0-alpha.17.tgz",
"integrity": "sha512-KnbyrxCbtQTqpDx2mSZU/r/E5QnDPIVfIxRi8K+W/q4gZpomBvqWC+xgvAk9hbpmA6QBoQaOilV8o12w2IZ6fg==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.0.0-alpha.17"
}
},
"jss-plugin-rule-value-function": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.0.0-alpha.17.tgz",
"integrity": "sha512-8AuJB44Q+ehfkWVRi2XlRbUf6SrLmrHTa5EXd6dgQRCCRuvGmqX8Dl4fZvNeKRFjTLPZgzg9+31rqeOMhKa2vA==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.0.0-alpha.17"
}
},
"jss-plugin-vendor-prefixer": {
"version": "10.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.0.0-alpha.17.tgz",
"integrity": "sha512-wDq9EL0QaoMGSGifPEBb+/SA9LBcqPEW0jpL9ht+Z2t+lV7NNz0j7uCEOuE6FvNWqHzUKTsiATs1rTHPkzNBEQ==",
"requires": {
"@babel/runtime": "^7.3.1",
"css-vendor": "^2.0.1",
"jss": "10.0.0-alpha.17"
}
},
"keycode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
"integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ="
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -6847,6 +7070,11 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"normalize-scroll-left": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/normalize-scroll-left/-/normalize-scroll-left-0.2.0.tgz",
"integrity": "sha512-t5oCENZJl8TGusJKoCJm7+asaSsPuNmK6+iEjrZ5TyBj2f02brCRsd4c83hwtu+e5d4LCSBZ0uoDlMjBo+A8yA=="
},
"normalize-url": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
@ -7362,6 +7590,11 @@
}
}
},
"popper.js": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz",
"integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA=="
},
"portfinder": {
"version": "1.0.21",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.21.tgz",
@ -10234,6 +10467,14 @@
"integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==",
"dev": true
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",

View file

@ -5,7 +5,8 @@
"scripts": {
"dev": "webpack-dev-server --open --mode development",
"build": "cross-env NODE_ENV=production webpack",
"deploy": "ts-node deploy"
"deploy": "ts-node deploy",
"show": "gource -f --start-date \"2019-07-01 12:00\" --key --hide dirnames,filenames,bloom"
},
"devDependencies": {
"@babel/core": "^7.5.5",
@ -39,7 +40,9 @@
},
"dependencies": {
"@eix-js/utils": "0.0.6",
"@material-ui/core": "^4.2.1",
"deepmerge": "^4.0.0",
"keycode": "^2.2.0",
"mainloop.js": "^1.0.4",
"react": "^16.8.6",
"react-dom": "^16.8.6",

View file

@ -1,8 +1,8 @@
import { SimulationRenderer } from '../../../modules/simulationRenderer/classes/SimulationRenderer'
import { Screen } from '../../../modules/core/classes/Screen'
export const clearCanvas = (
ctx: CanvasRenderingContext2D,
renderer: SimulationRenderer
) => {
ctx.clearRect(0, 0, ...renderer.camera.transform.scale)
const screen = new Screen()
export const clearCanvas = (ctx: CanvasRenderingContext2D) => {
ctx.clearRect(0, 0, screen.x, screen.y)
}

View file

@ -0,0 +1,2 @@
export const removeDuplicates = <T>(array: T[]): T[] =>
Array.from(new Set<T>(array).values())

View file

@ -3,9 +3,17 @@
<head>
<title>Logic gate simulator</title>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
</head>

View file

@ -2,8 +2,12 @@ import React from 'react'
import App from './modules/core/components/App'
import { render } from 'react-dom'
import { handleErrors } from './modules/errors/helpers/handlErrors'
render(<App />, document.getElementById('app'))
import { handleErrors } from './modules/errors/helpers/handleErrors'
import { initKeyBindings } from './modules/keybindings/helpers/initialiseKeyBindings'
import { initBaseTemplates } from './modules/saving/helpers/initBaseTemplates'
handleErrors()
initKeyBindings()
initBaseTemplates()
render(<App />, document.getElementById('app'))

View file

@ -0,0 +1,8 @@
export const toFunction = <T extends unknown[]>(
source: string,
...args: string[]
): ((...args: T) => void) => {
return new Function(`return (${args.join(',')}) => {
${source}
}`)()
}

View file

@ -0,0 +1,5 @@
export interface Context {
memory: Record<string, unknown>
get: (index: number) => boolean
set: (index: number, state: boolean) => void
}

View file

@ -0,0 +1,11 @@
import { BehaviorSubject } from 'rxjs'
export type Question = null | {
text: string
options: {
text: string
icon: string
}[]
}
export const QuestionSubject = new BehaviorSubject<Question>(null)

View file

@ -1,13 +1,27 @@
import React from 'react'
import '../styles/reset'
import './App.scss'
import 'react-toastify/dist/ReactToastify.css'
import Canvas from './Canvas'
import { ToastContainer } from 'react-toastify'
import { theme as muiTheme } from '../constants'
import React from 'react'
import Canvas from './Canvas'
import CssBaseline from '@material-ui/core/CssBaseline'
import Theme from '@material-ui/styles/ThemeProvider'
import Sidebar from './Sidebar'
import QuestionModal from './QuestionModal'
const App = () => {
return (
<>
<Theme theme={muiTheme}>
<CssBaseline />
<Canvas />
<Sidebar />
<QuestionModal />
</Theme>
<CssBaseline />
<ToastContainer
position="top-left"
autoClose={5000}
@ -18,7 +32,6 @@ const App = () => {
draggable
pauseOnHover
/>
<Canvas />
</>
)
}

View file

@ -5,33 +5,17 @@ import { Gate } from '../../simulation/classes/Gate'
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
import { renderSimulation } from '../../simulationRenderer/helpers/renderSimulation'
import { updateSimulation } from '../../simulationRenderer/helpers/updateSimulation'
import { addGate } from '../../simulation/helpers/addGate'
class Canvas extends Component {
private canvasRef: RefObject<HTMLCanvasElement> = createRef()
private renderingContext: CanvasRenderingContext2D | null
private renderer = new SimulationRenderer()
private renderer = new SimulationRenderer(this.canvasRef)
public constructor(props: {}) {
super(props)
const foo = new Gate({
material: {
value: 'blue'
}
})
const bar = new Gate({
material: {
value: 'green'
}
})
foo.transform.position = [100, 100]
foo.transform.scale = [100, 100]
bar.transform.position = [400, 200]
bar.transform.scale = [100, 100]
this.renderer.simulation.push(foo, bar)
addGate(this.renderer.simulation, 'not')
loop.setDraw(() => {
if (this.renderingContext) {
@ -41,8 +25,10 @@ class Canvas extends Component {
}
public componentDidMount() {
if (this.canvasRef.current)
if (this.canvasRef.current) {
this.renderingContext = this.canvasRef.current.getContext('2d')
this.renderer.updateWheelListener()
}
loop.start()
}

View file

@ -1,4 +1,4 @@
import React, { RefObject, forwardRef, MouseEvent } from 'react'
import React, { RefObject, forwardRef, MouseEvent, WheelEvent } from 'react'
import { useObservable } from 'rxjs-hooks'
import { Screen } from '../classes/Screen'
import { Subject } from 'rxjs'

View file

@ -0,0 +1,7 @@
.questionModal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

View file

@ -0,0 +1,14 @@
import './QuestionModal.scss'
import { useObservable } from 'rxjs-hooks'
import { QuestionSubject } from '../QuestionModalSubjects'
import React from 'react'
const QuestionModal = () => {
const question = useObservable(() => QuestionSubject)
if (!question) return <></>
return <div className="questionModal">{question.text}</div>
}
export default QuestionModal

View file

@ -0,0 +1,53 @@
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import React from 'react'
import Drawer from '@material-ui/core/Drawer'
import Button from '@material-ui/core/Button'
const drawerWidth = 240
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex'
},
drawer: {
width: drawerWidth,
flexShrink: 0
},
drawerPaper: {
padding: '4px',
width: drawerWidth,
background: `#111111`
},
drawerHeader: {
display: 'flex',
alignItems: 'center',
padding: '0 8px',
...theme.mixins.toolbar,
justifyContent: 'flex-start'
}
})
)
const Sidebar = () => {
const classes = useStyles()
return (
<div className={classes.root}>
<Drawer
className={classes.drawer}
variant="persistent"
anchor="right"
open={true}
classes={{
paper: classes.drawerPaper
}}
>
<Button variant={'contained'} color="primary">
New Simulation
</Button>
</Drawer>
</div>
)
}
export default Sidebar

View file

@ -0,0 +1,10 @@
import { createMuiTheme } from '@material-ui/core/styles'
import { red, deepPurple } from '@material-ui/core/colors'
export const theme = createMuiTheme({
palette: {
type: 'dark',
primary: deepPurple,
secondary: red
}
})

View file

@ -8,7 +8,5 @@ export const handleErrors = () => {
toast.error(...args)
}
console.log(a)
}
}

View file

@ -0,0 +1,94 @@
import { fromEvent, Subject, Subscription } from 'rxjs'
import keycode from 'keycode'
export class KeyboardInput {
/**
* boolean showing the state of the event
*/
public value = false
/**
* array with all the pressed keys
*/
private pressed: Array<string> = []
/**
* the keys to listen for events to
*/
private keys: Array<string>
/**
* an observable of the state o the event
*/
public valueChanges = new Subject<boolean>()
/**
* keeps track of the subscriptions for disposing
*/
private subscription: Array<Subscription> = []
/**
* use for keyboard events
* @param params the keys to listen to
*/
public constructor(...params: string[]) {
//save the keys
this.keys = params
//push a new subscription to the subscriptions array
this.subscription.push(
fromEvent(document, 'keydown').subscribe(e => {
//remember the length of the pressed array
//used to see if anything changed
const last = this.pressed.length
//iterate over the keys it listens to
//if the key is pressed and it isnt already pressed,
//then add it to the pressed array
for (let i of this.keys)
if (i == keycode(e) && this.pressed.indexOf(i) == -1)
this.pressed.push(i)
//if there was no key pressd before, and now there is
//then change the state of the event and emit it
if (last == 0 && this.pressed.length != 0) {
this.value = true
this.valueChanges.next(this.value)
}
})
)
//push a new subscription to the subscriptions array
this.subscription.push(
fromEvent(document, 'keyup').subscribe(e => {
//remember the length of the pressed array
//used to see if anything changed
const last = this.pressed.length
//iterate over the keys it listens to
//if the key is released and it was pressed,
//then remove it from the pressed array
for (let i of this.keys)
if (i == keycode(e) && this.pressed.indexOf(i) != -1)
this.pressed.splice(this.pressed.indexOf(i), 1)
//if there was at least a key pressd before, and now there isnt
//also, if the state was true
//then change the state of the event and emit it
if (this.value && last > 0 && this.pressed.length == 0) {
this.value = false
this.valueChanges.next(this.value)
}
})
)
}
/**
* ends the listening
*/
public dispose() {
this.subscription.forEach(e => e.unsubscribe())
this.value = false
this.valueChanges.next(false)
this.valueChanges.complete()
}
}

View file

@ -0,0 +1,4 @@
import { KeyBindingMap } from './types/KeyBindingMap'
import { save } from '../saving/helpers/save'
export const keyBindings: KeyBindingMap = []

View file

@ -0,0 +1,43 @@
import { keyBindings } from '../constants'
import { KeyboardInput } from '../classes/KeyboardInput'
import { KeyBindingMap } from '../types/KeyBindingMap'
export const listeners: Record<string, KeyboardInput> = {}
export const initKeyBindings = (bindings: KeyBindingMap = keyBindings) => {
const allKeys = new Set<string>()
for (const binding of bindings) {
for (const key of binding.keys) {
allKeys.add(key)
}
}
for (const key of allKeys.values()) {
listeners[key] = new KeyboardInput(key)
}
window.addEventListener('keydown', e => {
for (const keyBinding of bindings) {
let done = false
for (const key of keyBinding.keys) {
if (!(done || listeners[key].value)) {
done = true
break
}
}
if (done) {
continue
}
for (const action of keyBinding.actions) {
action()
}
e.preventDefault()
e.stopPropagation()
}
})
}

View file

@ -0,0 +1,6 @@
export interface KeyBinding {
keys: string[]
actions: Function[]
}
export type KeyBindingMap = KeyBinding[]

View file

@ -0,0 +1,27 @@
import { GateTemplate } from '../simulation/types/GateTemplate'
export const defaultSimulationName = 'default'
export const baseTemplates: DeepPartial<GateTemplate>[] = [
{
metadata: {
name: 'not'
},
material: {
value: 'red',
type: 'color'
},
code: {
activation: `context.set(0, !context.get(0))`
},
pins: {
inputs: {
count: 1,
variable: false
},
outputs: {
count: 1,
variable: false
}
}
}
]

View file

@ -0,0 +1,68 @@
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
import { Gate, PinWrapper } from '../../simulation/classes/Gate'
import {
TransformState,
RendererState,
CameraState,
SimulationState
} from '../types/SimulationSave'
import { Transform } from '../../../common/math/classes/Transform'
import { Camera } from '../../simulationRenderer/classes/Camera'
import { Simulation } from '../../simulation/classes/Simulation'
import { Wire } from '../../simulation/classes/Wire'
import { templateStore } from '../stores/templateStore'
export const fromTransformState = (state: TransformState): Transform => {
return new Transform(state.position, state.scale, state.rotation)
}
export const fromCameraState = (state: CameraState): Camera => {
const camera = new Camera()
camera.transform = fromTransformState(state.transform)
return camera
}
export const fromSimulationState = (state: SimulationState): Simulation => {
const simulation = new Simulation(state.mode)
for (const gateState of state.gates) {
const gate = new Gate(
templateStore.get(gateState.template),
gateState.id
)
gate.transform = fromTransformState(gateState.transform)
simulation.push(gate)
}
for (const wireState of state.wires) {
const startGateNode = simulation.gates.get(wireState.from.id)
const endGateNode = simulation.gates.get(wireState.to.id)
if (
startGateNode &&
endGateNode &&
startGateNode.data &&
endGateNode.data
) {
const start: PinWrapper = {
index: wireState.from.index,
total: wireState.from.total,
value: startGateNode.data._pins.outputs[wireState.from.index]
}
const end: PinWrapper = {
index: wireState.to.index,
total: wireState.to.total,
value: endGateNode.data._pins.inputs[wireState.to.index]
}
const wire = new Wire(start, end, wireState.id)
simulation.wires.push(wire)
}
}
return simulation
}

View file

@ -31,7 +31,8 @@ export const getCameraState = (camera: Camera): CameraState => {
export const getWireLimit = (pin: PinWrapper): WireLimit => {
return {
id: pin.value.gate.id,
index: pin.index
index: pin.index,
total: pin.total
}
}

View file

@ -0,0 +1,10 @@
import { baseTemplates } from '../constants'
import { templateStore } from '../stores/templateStore'
export const initBaseTemplates = () => {
for (const template of baseTemplates) {
if (template.metadata && template.metadata.name) {
templateStore.set(template.metadata.name, template)
}
}
}

View file

@ -0,0 +1,23 @@
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
import { currentStore } from '../stores/currentStore'
import { SimulationError } from '../../errors/classes/SimulationError'
import { getRendererState } from './getState'
import { saveStore } from '../stores/saveStore'
import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments'
export const save = (renderer: SimulationRenderer) => {
const current = currentStore.get()
if (current) {
const state = getRendererState(renderer)
saveStore.set(current, state)
toast.info(...createToastArguments(`Succesfully saved ${current}`))
} else {
throw new SimulationError(
'Cannot save without knowing the name of the active simulation'
)
}
}

View file

@ -0,0 +1,10 @@
import { LocalStore } from '../../storage/classes/LocalStore'
import { defaultSimulationName } from '../constants'
const currentStore = new LocalStore<string>('currentSave')
if (!currentStore.get()) {
currentStore.set(defaultSimulationName)
}
export { currentStore }

View file

@ -0,0 +1,4 @@
import { LocalStore } from '../../storage/classes/LocalStore'
import { RendererState } from '../types/SimulationSave'
export const saveStore = new LocalStore<RendererState>('saves')

View file

@ -0,0 +1,6 @@
import { LocalStore } from '../../storage/classes/LocalStore'
import { GateTemplate } from '../../simulation/types/GateTemplate'
export const templateStore = new LocalStore<DeepPartial<GateTemplate>>(
'templates'
)

View file

@ -21,6 +21,7 @@ export interface CameraState {
export interface WireLimit {
id: number
index: number
total: number
}
export interface WireState {

View file

@ -4,6 +4,12 @@ import merge from 'deepmerge'
import { GateTemplate, PinCount } from '../types/GateTemplate'
import { DefaultGateTemplate } from '../constants'
import { idStore } from '../stores/idStore'
import { Context } from '../../activation/types/Context'
import { toFunction } from '../../activation/helpers/toFunction'
import { Subscription, combineLatest } from 'rxjs'
import { SimulationError } from '../../errors/classes/SimulationError'
import { throttleTime, debounce, debounceTime } from 'rxjs/operators'
import { getGateTimePipes } from '../helpers/getGateTimePipes'
export interface GatePins {
inputs: Pin[]
@ -16,6 +22,10 @@ export interface PinWrapper {
value: Pin
}
export interface GateFunctions {
activation: null | ((ctx: Context) => void)
}
export class Gate {
public transform = new Transform()
public _pins: GatePins = {
@ -26,9 +36,23 @@ export class Gate {
public id: number
public template: GateTemplate
private functions: GateFunctions = {
activation: null
}
private subscriptions: Subscription[] = []
private memory: Record<string, unknown> = {}
public constructor(template: DeepPartial<GateTemplate> = {}, id?: number) {
this.template = merge(DefaultGateTemplate, template) as GateTemplate
this.transform.scale = this.template.shape.scale
this.functions.activation = toFunction(
this.template.code.activation,
'context'
)
this._pins.inputs = Gate.generatePins(
this.template.pins.inputs,
1,
@ -41,6 +65,51 @@ export class Gate {
)
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(() => {
this.update()
})
this.subscriptions.push(subscription)
}
}
public dispose() {
for (const pin of this.pins) {
pin.value.dispose()
}
for (const subscription of this.subscriptions) {
subscription.unsubscribe()
}
}
public update() {
const context = this.getContext()
if (!this.functions.activation)
throw new SimulationError('Activation function is missing')
this.functions.activation(context)
}
public getContext(): Context {
return {
get: (index: number) => {
return this._pins.inputs[index].state.value
},
set: (index: number, state: boolean = false) => {
return this._pins.outputs[index].state.next(state)
},
memory: this.memory
}
}
private getInputsStates() {
return this._pins.inputs.map(pin => pin.state)
}
private wrapPins(pins: Pin[]) {

View file

@ -13,7 +13,7 @@ export class GateStorage {
this.tail.previous = this.head
}
private delete(node: GateNode) {
public delete(node: GateNode) {
node.previous.next = node.next
node.next.previous = node.previous

View file

@ -17,4 +17,10 @@ export class Simulation {
this.gates.set(gate.id, node)
}
}
public dispose() {
for (const gate of this.gates) {
gate.dispose()
}
}
}

View file

@ -4,6 +4,7 @@ import { SimulationError } from '../../errors/classes/SimulationError'
export class Wire {
public id: number
public active = true
public constructor(
public start: PinWrapper,
@ -23,5 +24,7 @@ export class Wire {
public dispose() {
this.end.value.removePair(this.start.value)
this.start.value.removePair(this.end.value)
this.active = false
}
}

View file

@ -20,6 +20,21 @@ export const DefaultGateTemplate: GateTemplate = {
},
shape: {
radius: 10,
rounded: true
rounded: true,
scale: [100, 100]
},
code: {
activation: 'context.set(0,true)',
start: '',
stop: ''
},
simulation: {
debounce: {
enabled: true,
time: 1000 / 60
},
throttle: {
enabled: false
}
}
}

View file

@ -0,0 +1,13 @@
import { templateStore } from '../../saving/stores/templateStore'
import { SimulationError } from '../../errors/classes/SimulationError'
import { Simulation } from '../classes/Simulation'
import { Gate } from '../classes/Gate'
export const addGate = (simulation: Simulation, templateName: string) => {
const template = templateStore.get(templateName)
if (!template)
throw new SimulationError(`Cannot find template ${templateName}`)
simulation.push(new Gate(template))
}

View file

@ -0,0 +1,19 @@
import { GateTemplate } from '../types/GateTemplate'
import { debounceTime, throttleTime } from 'rxjs/operators'
import { MonoTypeOperatorFunction, pipe } from 'rxjs'
export type TimePipe = MonoTypeOperatorFunction<boolean>
export const getGateTimePipes = (template: GateTemplate) => {
const pipes: TimePipe[] = []
if (template.simulation.debounce.enabled) {
pipes.push(debounceTime(template.simulation.debounce.time))
}
if (template.simulation.throttle.enabled) {
pipes.push(throttleTime(template.simulation.throttle.time))
}
return pipes as [TimePipe]
}

View file

@ -1,3 +1,5 @@
import { vector2 } from '../../../common/math/classes/Transform'
export interface PinCount {
variable: boolean
count: number
@ -11,8 +13,21 @@ export interface Material {
export interface Shape {
rounded: boolean
radius: number
scale: vector2
}
export type Enabled<T> =
| {
enabled: false
}
| ({
enabled: true
} & T)
export type TimePipe = Enabled<{
time: number
}>
export interface GateTemplate {
material: Material
shape: Shape
@ -23,4 +38,13 @@ export interface GateTemplate {
metadata: {
name: string
}
code: {
start: string
activation: string
stop: string
}
simulation: {
throttle: TimePipe
debounce: TimePipe
}
}

View file

@ -4,19 +4,22 @@ import { Screen } from '../../core/classes/Screen'
import { relativeTo } from '../../vector2/helpers/basic'
export class Camera {
private screen = new Screen()
public transform = new Transform([0, 0], [this.screen.x, this.screen.y])
public transform = new Transform([0, 0])
public constructor() {
this.screen.height.subscribe(value => {
this.transform.height = value
})
this.screen.width.subscribe(value => {
this.transform.width = value
})
// this.screen.height.subscribe(value => {
// this.transform.height = value
// })
// this.screen.width.subscribe(value => {
// this.transform.width = value
// })
}
public toWordPostition(position: vector2) {
return relativeTo(this.transform.position, position)
return [
(position[0] - this.transform.position[0]) /
this.transform.scale[0],
(position[1] - this.transform.position[1]) / this.transform.scale[1]
] as vector2
}
}

View file

@ -1,6 +1,6 @@
import { Camera } from './Camera'
import { Simulation } from '../../simulation/classes/Simulation'
import { Subject } from 'rxjs'
import { Subject, fromEvent } from 'rxjs'
import { MouseEventInfo } from '../../core/components/FluidCanvas'
import { pointInSquare } from '../../../common/math/helpers/pointInSquare'
import { vector2 } from '../../../common/math/types/vector2'
@ -9,25 +9,34 @@ import { Screen } from '../../core/classes/Screen'
import { relativeTo, add, invert } from '../../vector2/helpers/basic'
import { SimulationRendererOptions } from '../types/SimulationRendererOptions'
import { defaultSimulationRendererOptions } from '../constants'
import merge from 'deepmerge'
import { getPinPosition } from '../helpers/pinPosition'
import { pointInCircle } from '../../../common/math/helpers/pointInCircle'
import { SelectedPins } from '../types/SelectedPins'
import { getRendererState } from '../../saving/helpers/getState'
import { Wire } from '../../simulation/classes/Wire'
import { KeyBindingMap } from '../../keybindings/types/KeyBindingMap'
import { save } from '../../saving/helpers/save'
import { initKeyBindings } from '../../keybindings/helpers/initialiseKeyBindings'
import { currentStore } from '../../saving/stores/currentStore'
import { saveStore } from '../../saving/stores/saveStore'
import { SimulationError } from '../../errors/classes/SimulationError'
import {
fromSimulationState,
fromCameraState
} from '../../saving/helpers/fromState'
import merge from 'deepmerge'
import { wireConnectedToGate } from '../helpers/wireConnectedToGate'
import { updateMouse, handleScroll } from '../helpers/scaleCanvas'
import { RefObject } from 'react'
// import { WheelEvent } from 'react'
export class SimulationRenderer {
public mouseDownOutput = new Subject<MouseEventInfo>()
public mouseUpOutput = new Subject<MouseEventInfo>()
public mouseMoveOutput = new Subject<MouseEventInfo>()
public wheelOutput = new Subject<unknown>()
// first bit = dragging
// second bit = moving around
private mouseState = 0b00
private selectedGate: number | null = null
private gateSelectionOffset: vector2 = [0, 0]
public selectedGate: number | null = null
public lastMousePosition: vector2 = [0, 0]
public movedSelection = false
public options: SimulationRendererOptions
@ -35,12 +44,18 @@ export class SimulationRenderer {
public screen = new Screen()
public camera = new Camera()
// first bit = dragging
// second bit = moving around
private mouseState = 0b00
private gateSelectionOffset: vector2 = [0, 0]
public selectedPins: SelectedPins = {
start: null,
end: null
}
public constructor(
public ref: RefObject<HTMLCanvasElement>,
options: Partial<SimulationRendererOptions> = {},
public simulation = new Simulation()
) {
@ -93,7 +108,17 @@ export class SimulationRenderer {
this.options.gates.pinRadius
)
) {
if ((pin.value.type & 0b10) >> 1) {
if (
this.selectedPins.start &&
pin.value === this.selectedPins.start.wrapper.value
) {
this.selectedPins.start = null
} else if (
this.selectedPins.end &&
pin.value === this.selectedPins.end.wrapper.value
) {
this.selectedPins.end = null
} else if ((pin.value.type & 0b10) >> 1) {
this.selectedPins.start = {
wrapper: pin,
transform
@ -118,8 +143,6 @@ export class SimulationRenderer {
)
this.selectedPins.start = null
this.selectedPins.end = null
console.log(getRendererState(this))
}
}
}
@ -144,6 +167,8 @@ export class SimulationRenderer {
})
this.mouseMoveOutput.subscribe(event => {
updateMouse(event)
const worldPosition = this.camera.toWordPostition(event.position)
if (this.mouseState & 1 && this.selectedGate !== null) {
@ -164,7 +189,9 @@ export class SimulationRenderer {
if ((this.mouseState >> 1) & 1) {
const offset = invert(
relativeTo(this.lastMousePosition, worldPosition)
)
).map(
(value, index) => value * this.camera.transform.scale[index]
) as vector2
this.camera.transform.position = add(
this.camera.transform.position,
@ -174,6 +201,81 @@ export class SimulationRenderer {
this.lastMousePosition = this.camera.toWordPostition(event.position)
})
this.reloadSave()
this.initKeyBindings()
}
public updateWheelListener() {
if (this.ref.current) {
this.ref.current.addEventListener('wheel', event => {
event.preventDefault()
handleScroll(event, this.camera)
})
}
}
public reloadSave() {
try {
const current = currentStore.get()
const save = saveStore.get(current)
if (!save) return
if (!(save.simulation || save.camera)) return
this.simulation.dispose()
this.simulation = fromSimulationState(save.simulation)
this.camera = fromCameraState(save.camera)
} catch (e) {
throw new Error(
`An error occured while loading the save: ${
(e as Error).message
}`
)
}
}
private initKeyBindings() {
const bindings: KeyBindingMap = [
{
keys: ['ctrl', 's'],
actions: [() => save(this)]
},
{
keys: ['delete'],
actions: [
() => {
const selected = this.getSelected()
if (!selected) {
return
}
const node = this.simulation.gates.get(selected.id)
if (!node) {
return
}
for (const wire of this.simulation.wires) {
if (wireConnectedToGate(selected, wire)) {
wire.dispose()
}
}
this.simulation.wires = this.simulation.wires.filter(
wire => wire.active
)
selected.dispose()
this.simulation.gates.delete(node)
}
]
}
]
initKeyBindings(bindings)
}
public getGateById(id: number) {

View file

@ -12,6 +12,11 @@ export const defaultSimulationRendererOptions: SimulationRendererOptions = {
pinFill: {
open: 'rgb(255,216,20)',
closed: 'rgb(90,90,90)'
},
gateStroke: {
active: 'yellow',
normal: 'black',
width: 4
}
},
wires: {

View file

@ -10,8 +10,17 @@ export const renderGate = (
) => {
renderPins(ctx, renderer, gate)
if (renderer.selectedGate === gate.id) {
ctx.strokeStyle = renderer.options.gates.gateStroke.active
} else {
ctx.strokeStyle = renderer.options.gates.gateStroke.normal
}
ctx.lineWidth = renderer.options.gates.gateStroke.width
if (gate.template.material.type === 'color') {
ctx.fillStyle = gate.template.material.value
drawRotatedSquare(ctx, gate.transform, gate.template.shape)
ctx.stroke()
}
}

View file

@ -1,17 +1,21 @@
import { SimulationRenderer } from '../classes/SimulationRenderer'
import { invert } from '../../vector2/helpers/basic'
import { invert, inverse } from '../../vector2/helpers/basic'
import { renderGate } from './renderGate'
import { clearCanvas } from '../../../common/canvas/helpers/clearCanvas'
import { renderClickedPins } from './renderClickedPins'
import { renderWires } from './renderWires'
import { vector2 } from '../../../common/math/classes/Transform'
export const renderSimulation = (
ctx: CanvasRenderingContext2D,
renderer: SimulationRenderer
) => {
clearCanvas(ctx, renderer)
clearCanvas(ctx)
ctx.translate(...renderer.camera.transform.position)
const transform = renderer.camera.transform
ctx.translate(...transform.position)
ctx.scale(...transform.scale)
for (const wire of renderer.simulation.wires) {
renderWires(ctx, renderer, wire)
@ -23,5 +27,6 @@ export const renderSimulation = (
renderClickedPins(ctx, renderer)
ctx.translate(...invert(renderer.camera.transform.position))
ctx.scale(...inverse(transform.scale))
ctx.translate(...invert(transform.position))
}

View file

@ -0,0 +1,39 @@
import { Screen } from '../../core/classes/Screen'
import { clamp } from '../../simulation/helpers/clamp'
import { Camera } from '../classes/Camera'
import { vector2 } from '../../../common/math/classes/Transform'
import { MouseEventInfo } from '../../core/components/FluidCanvas'
// import { WheelEvent } from 'react'
const screen = new Screen()
const scrollStep = 1.3
const zoomLimits = [0.1, 10]
let absoluteMousePosition = [screen.x / 2, screen.y / 2]
export const updateMouse = (e: MouseEventInfo) => {
absoluteMousePosition = e.position
}
export const handleScroll = (e: WheelEvent, camera: Camera) => {
const sign = e.deltaY / Math.abs(e.deltaY)
const zoom = scrollStep ** sign
const size = [screen.width.value, screen.height.value]
const mouseFraction = size.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) =>
size[index] * (newScale[index] - value) * mouseFraction[index]
)
camera.transform.scale = newScale as vector2
camera.transform.position = camera.transform.position.map(
(value, index) => value - delta[index]
) as vector2
}

View file

@ -0,0 +1,5 @@
import { Gate } from '../../simulation/classes/Gate'
import { Wire } from '../../simulation/classes/Wire'
export const wireConnectedToGate = (gate: Gate, wire: Wire) =>
wire.end.value.gate === gate || wire.start.value.gate === gate

View file

@ -11,6 +11,11 @@ export interface SimulationRendererOptions {
open: string
closed: string
}
gateStroke: {
active: string
normal: string
width: number
}
}
wires: {
temporaryWireColor: string

View file

@ -32,7 +32,7 @@ export class LocalStore<T> {
}
}
public get(key = 'index') {
public get(key = 'index'): T | undefined {
return this.getAll()[key]
}

View file

@ -38,3 +38,5 @@ export const ofLength = (vector: vector2, l: number) => {
// This returns a vector relative to the other
export const relativeTo = (vector: vector2, other: vector2) =>
add(other, invert(vector))
export const inverse = (vector: vector2) => vector.map(a => 1 / a) as vector2