From 057c2268acfa060135d50a3dd9063a60cf6e6084 Mon Sep 17 00:00:00 2001 From: Matei Adriel Date: Fri, 19 Jul 2019 15:25:01 +0300 Subject: [PATCH] Added the basics of keybindings + zooming --- .vscode/settings.json | 9 +- package-lock.json | 251 +++++++++++++++++- package.json | 5 +- src/common/canvas/helpers/clearCanvas.ts | 10 +- src/common/lang/arrays/removeDuplicates.ts | 2 + src/index.html | 10 +- src/main.tsx | 10 +- src/modules/activation/helpers/toFunction.ts | 8 + src/modules/activation/types/Context.ts | 5 + src/modules/core/QuestionModalSubjects.ts | 11 + src/modules/core/components/App.tsx | 19 +- src/modules/core/components/Canvas.tsx | 26 +- src/modules/core/components/FluidCanvas.tsx | 2 +- .../core/components/QuestionModal.scss | 7 + src/modules/core/components/QuestionModal.tsx | 14 + src/modules/core/components/Sidebar.tsx | 53 ++++ src/modules/core/constants.ts | 10 + .../{handlErrors.ts => handleErrors.ts} | 2 - .../keybindings/classes/KeyboardInput.ts | 94 +++++++ src/modules/keybindings/constants.ts | 4 + .../helpers/initialiseKeyBindings.ts | 43 +++ .../keybindings/types/KeyBindingMap.ts | 6 + src/modules/saving/constants.ts | 27 ++ src/modules/saving/helpers/fromState.ts | 68 +++++ src/modules/saving/helpers/getState.ts | 3 +- .../saving/helpers/initBaseTemplates.ts | 10 + src/modules/saving/helpers/save.ts | 23 ++ src/modules/saving/stores/currentStore.ts | 10 + src/modules/saving/stores/saveStore.ts | 4 + src/modules/saving/stores/templateStore.ts | 6 + src/modules/saving/types/SimulationSave.ts | 1 + src/modules/simulation/classes/Gate.ts | 69 +++++ src/modules/simulation/classes/GateStorage.ts | 2 +- src/modules/simulation/classes/Simulation.ts | 6 + src/modules/simulation/classes/Wire.ts | 3 + src/modules/simulation/constants.ts | 17 +- src/modules/simulation/helpers/addGate.ts | 13 + .../simulation/helpers/getGateTimePipes.ts | 19 ++ src/modules/simulation/types/GateTemplate.ts | 24 ++ .../simulationRenderer/classes/Camera.ts | 21 +- .../classes/SimulationRenderer.ts | 128 ++++++++- src/modules/simulationRenderer/constants.ts | 5 + .../simulationRenderer/helpers/renderGate.ts | 9 + .../helpers/renderSimulation.ts | 13 +- .../simulationRenderer/helpers/scaleCanvas.ts | 39 +++ .../helpers/wireConnectedToGate.ts | 5 + .../types/SimulationRendererOptions.ts | 5 + src/modules/storage/classes/LocalStore.ts | 2 +- src/modules/vector2/helpers/basic.ts | 2 + 49 files changed, 1059 insertions(+), 76 deletions(-) create mode 100644 src/common/lang/arrays/removeDuplicates.ts create mode 100644 src/modules/activation/helpers/toFunction.ts create mode 100644 src/modules/activation/types/Context.ts create mode 100644 src/modules/core/QuestionModalSubjects.ts create mode 100644 src/modules/core/components/QuestionModal.scss create mode 100644 src/modules/core/components/QuestionModal.tsx create mode 100644 src/modules/core/components/Sidebar.tsx create mode 100644 src/modules/core/constants.ts rename src/modules/errors/helpers/{handlErrors.ts => handleErrors.ts} (93%) create mode 100644 src/modules/keybindings/classes/KeyboardInput.ts create mode 100644 src/modules/keybindings/constants.ts create mode 100644 src/modules/keybindings/helpers/initialiseKeyBindings.ts create mode 100644 src/modules/keybindings/types/KeyBindingMap.ts create mode 100644 src/modules/saving/constants.ts create mode 100644 src/modules/saving/helpers/fromState.ts create mode 100644 src/modules/saving/helpers/initBaseTemplates.ts create mode 100644 src/modules/saving/helpers/save.ts create mode 100644 src/modules/saving/stores/currentStore.ts create mode 100644 src/modules/saving/stores/saveStore.ts create mode 100644 src/modules/saving/stores/templateStore.ts create mode 100644 src/modules/simulation/helpers/addGate.ts create mode 100644 src/modules/simulation/helpers/getGateTimePipes.ts create mode 100644 src/modules/simulationRenderer/helpers/scaleCanvas.ts create mode 100644 src/modules/simulationRenderer/helpers/wireConnectedToGate.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 106395f..d0015c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,5 @@ { - "eslint.enable": true, - "editor.formatOnSave": true, - "prettier.eslintIntegration": true, - "eslint.validate": ["typescript", "typescriptreact"] -} \ No newline at end of file + "editor.formatOnSave": true, + "prettier.eslintIntegration": true, + "explorer.autoReveal": false +} diff --git a/package-lock.json b/package-lock.json index 23d8ae3..d8632ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 9ed9a03..583eae2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/canvas/helpers/clearCanvas.ts b/src/common/canvas/helpers/clearCanvas.ts index b707e06..d39ec16 100644 --- a/src/common/canvas/helpers/clearCanvas.ts +++ b/src/common/canvas/helpers/clearCanvas.ts @@ -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) } diff --git a/src/common/lang/arrays/removeDuplicates.ts b/src/common/lang/arrays/removeDuplicates.ts new file mode 100644 index 0000000..1f7c2d5 --- /dev/null +++ b/src/common/lang/arrays/removeDuplicates.ts @@ -0,0 +1,2 @@ +export const removeDuplicates = (array: T[]): T[] => + Array.from(new Set(array).values()) diff --git a/src/index.html b/src/index.html index c580d67..daaf4ee 100644 --- a/src/index.html +++ b/src/index.html @@ -3,9 +3,17 @@ Logic gate simulator + + diff --git a/src/main.tsx b/src/main.tsx index 812be3d..008d7fc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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(, 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(, document.getElementById('app')) diff --git a/src/modules/activation/helpers/toFunction.ts b/src/modules/activation/helpers/toFunction.ts new file mode 100644 index 0000000..034cd70 --- /dev/null +++ b/src/modules/activation/helpers/toFunction.ts @@ -0,0 +1,8 @@ +export const toFunction = ( + source: string, + ...args: string[] +): ((...args: T) => void) => { + return new Function(`return (${args.join(',')}) => { + ${source} + }`)() +} diff --git a/src/modules/activation/types/Context.ts b/src/modules/activation/types/Context.ts new file mode 100644 index 0000000..f418d27 --- /dev/null +++ b/src/modules/activation/types/Context.ts @@ -0,0 +1,5 @@ +export interface Context { + memory: Record + get: (index: number) => boolean + set: (index: number, state: boolean) => void +} diff --git a/src/modules/core/QuestionModalSubjects.ts b/src/modules/core/QuestionModalSubjects.ts new file mode 100644 index 0000000..c60a83d --- /dev/null +++ b/src/modules/core/QuestionModalSubjects.ts @@ -0,0 +1,11 @@ +import { BehaviorSubject } from 'rxjs' + +export type Question = null | { + text: string + options: { + text: string + icon: string + }[] +} + +export const QuestionSubject = new BehaviorSubject(null) diff --git a/src/modules/core/components/App.tsx b/src/modules/core/components/App.tsx index 301cb28..1abafd6 100644 --- a/src/modules/core/components/App.tsx +++ b/src/modules/core/components/App.tsx @@ -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 ( <> + + + + + + + { draggable pauseOnHover /> - ) } diff --git a/src/modules/core/components/Canvas.tsx b/src/modules/core/components/Canvas.tsx index 3ed4daf..d8573bc 100644 --- a/src/modules/core/components/Canvas.tsx +++ b/src/modules/core/components/Canvas.tsx @@ -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 = 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() } diff --git a/src/modules/core/components/FluidCanvas.tsx b/src/modules/core/components/FluidCanvas.tsx index cb01886..ad18cee 100644 --- a/src/modules/core/components/FluidCanvas.tsx +++ b/src/modules/core/components/FluidCanvas.tsx @@ -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' diff --git a/src/modules/core/components/QuestionModal.scss b/src/modules/core/components/QuestionModal.scss new file mode 100644 index 0000000..bea18a2 --- /dev/null +++ b/src/modules/core/components/QuestionModal.scss @@ -0,0 +1,7 @@ +.questionModal { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} diff --git a/src/modules/core/components/QuestionModal.tsx b/src/modules/core/components/QuestionModal.tsx new file mode 100644 index 0000000..af53651 --- /dev/null +++ b/src/modules/core/components/QuestionModal.tsx @@ -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
{question.text}
+} + +export default QuestionModal diff --git a/src/modules/core/components/Sidebar.tsx b/src/modules/core/components/Sidebar.tsx new file mode 100644 index 0000000..240a941 --- /dev/null +++ b/src/modules/core/components/Sidebar.tsx @@ -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 ( +
+ + + +
+ ) +} + +export default Sidebar diff --git a/src/modules/core/constants.ts b/src/modules/core/constants.ts new file mode 100644 index 0000000..c7c9eb1 --- /dev/null +++ b/src/modules/core/constants.ts @@ -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 + } +}) diff --git a/src/modules/errors/helpers/handlErrors.ts b/src/modules/errors/helpers/handleErrors.ts similarity index 93% rename from src/modules/errors/helpers/handlErrors.ts rename to src/modules/errors/helpers/handleErrors.ts index 8d3f0f5..98a72e0 100644 --- a/src/modules/errors/helpers/handlErrors.ts +++ b/src/modules/errors/helpers/handleErrors.ts @@ -8,7 +8,5 @@ export const handleErrors = () => { toast.error(...args) } - - console.log(a) } } diff --git a/src/modules/keybindings/classes/KeyboardInput.ts b/src/modules/keybindings/classes/KeyboardInput.ts new file mode 100644 index 0000000..bb8a73b --- /dev/null +++ b/src/modules/keybindings/classes/KeyboardInput.ts @@ -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 = [] + + /** + * the keys to listen for events to + */ + private keys: Array + + /** + * an observable of the state o the event + */ + public valueChanges = new Subject() + + /** + * keeps track of the subscriptions for disposing + */ + private subscription: Array = [] + + /** + * 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() + } +} diff --git a/src/modules/keybindings/constants.ts b/src/modules/keybindings/constants.ts new file mode 100644 index 0000000..500ed41 --- /dev/null +++ b/src/modules/keybindings/constants.ts @@ -0,0 +1,4 @@ +import { KeyBindingMap } from './types/KeyBindingMap' +import { save } from '../saving/helpers/save' + +export const keyBindings: KeyBindingMap = [] diff --git a/src/modules/keybindings/helpers/initialiseKeyBindings.ts b/src/modules/keybindings/helpers/initialiseKeyBindings.ts new file mode 100644 index 0000000..85c2e18 --- /dev/null +++ b/src/modules/keybindings/helpers/initialiseKeyBindings.ts @@ -0,0 +1,43 @@ +import { keyBindings } from '../constants' +import { KeyboardInput } from '../classes/KeyboardInput' +import { KeyBindingMap } from '../types/KeyBindingMap' + +export const listeners: Record = {} + +export const initKeyBindings = (bindings: KeyBindingMap = keyBindings) => { + const allKeys = new Set() + + 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() + } + }) +} diff --git a/src/modules/keybindings/types/KeyBindingMap.ts b/src/modules/keybindings/types/KeyBindingMap.ts new file mode 100644 index 0000000..b9eba18 --- /dev/null +++ b/src/modules/keybindings/types/KeyBindingMap.ts @@ -0,0 +1,6 @@ +export interface KeyBinding { + keys: string[] + actions: Function[] +} + +export type KeyBindingMap = KeyBinding[] diff --git a/src/modules/saving/constants.ts b/src/modules/saving/constants.ts new file mode 100644 index 0000000..3b83afa --- /dev/null +++ b/src/modules/saving/constants.ts @@ -0,0 +1,27 @@ +import { GateTemplate } from '../simulation/types/GateTemplate' + +export const defaultSimulationName = 'default' +export const baseTemplates: DeepPartial[] = [ + { + 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 + } + } + } +] diff --git a/src/modules/saving/helpers/fromState.ts b/src/modules/saving/helpers/fromState.ts new file mode 100644 index 0000000..81d1837 --- /dev/null +++ b/src/modules/saving/helpers/fromState.ts @@ -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 +} diff --git a/src/modules/saving/helpers/getState.ts b/src/modules/saving/helpers/getState.ts index 582f2d0..75a5cb0 100644 --- a/src/modules/saving/helpers/getState.ts +++ b/src/modules/saving/helpers/getState.ts @@ -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 } } diff --git a/src/modules/saving/helpers/initBaseTemplates.ts b/src/modules/saving/helpers/initBaseTemplates.ts new file mode 100644 index 0000000..f961702 --- /dev/null +++ b/src/modules/saving/helpers/initBaseTemplates.ts @@ -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) + } + } +} diff --git a/src/modules/saving/helpers/save.ts b/src/modules/saving/helpers/save.ts new file mode 100644 index 0000000..e5657d0 --- /dev/null +++ b/src/modules/saving/helpers/save.ts @@ -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' + ) + } +} diff --git a/src/modules/saving/stores/currentStore.ts b/src/modules/saving/stores/currentStore.ts new file mode 100644 index 0000000..6f7920c --- /dev/null +++ b/src/modules/saving/stores/currentStore.ts @@ -0,0 +1,10 @@ +import { LocalStore } from '../../storage/classes/LocalStore' +import { defaultSimulationName } from '../constants' + +const currentStore = new LocalStore('currentSave') + +if (!currentStore.get()) { + currentStore.set(defaultSimulationName) +} + +export { currentStore } diff --git a/src/modules/saving/stores/saveStore.ts b/src/modules/saving/stores/saveStore.ts new file mode 100644 index 0000000..1656424 --- /dev/null +++ b/src/modules/saving/stores/saveStore.ts @@ -0,0 +1,4 @@ +import { LocalStore } from '../../storage/classes/LocalStore' +import { RendererState } from '../types/SimulationSave' + +export const saveStore = new LocalStore('saves') diff --git a/src/modules/saving/stores/templateStore.ts b/src/modules/saving/stores/templateStore.ts new file mode 100644 index 0000000..74dcf60 --- /dev/null +++ b/src/modules/saving/stores/templateStore.ts @@ -0,0 +1,6 @@ +import { LocalStore } from '../../storage/classes/LocalStore' +import { GateTemplate } from '../../simulation/types/GateTemplate' + +export const templateStore = new LocalStore>( + 'templates' +) diff --git a/src/modules/saving/types/SimulationSave.ts b/src/modules/saving/types/SimulationSave.ts index 2d88e5f..3cb0f9d 100644 --- a/src/modules/saving/types/SimulationSave.ts +++ b/src/modules/saving/types/SimulationSave.ts @@ -21,6 +21,7 @@ export interface CameraState { export interface WireLimit { id: number index: number + total: number } export interface WireState { diff --git a/src/modules/simulation/classes/Gate.ts b/src/modules/simulation/classes/Gate.ts index f23d4dc..4049238 100644 --- a/src/modules/simulation/classes/Gate.ts +++ b/src/modules/simulation/classes/Gate.ts @@ -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 = {} + public constructor(template: DeepPartial = {}, 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[]) { diff --git a/src/modules/simulation/classes/GateStorage.ts b/src/modules/simulation/classes/GateStorage.ts index 9783e8b..795d543 100644 --- a/src/modules/simulation/classes/GateStorage.ts +++ b/src/modules/simulation/classes/GateStorage.ts @@ -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 diff --git a/src/modules/simulation/classes/Simulation.ts b/src/modules/simulation/classes/Simulation.ts index 543d6c1..9c1a961 100644 --- a/src/modules/simulation/classes/Simulation.ts +++ b/src/modules/simulation/classes/Simulation.ts @@ -17,4 +17,10 @@ export class Simulation { this.gates.set(gate.id, node) } } + + public dispose() { + for (const gate of this.gates) { + gate.dispose() + } + } } diff --git a/src/modules/simulation/classes/Wire.ts b/src/modules/simulation/classes/Wire.ts index 5a008fb..5abcffe 100644 --- a/src/modules/simulation/classes/Wire.ts +++ b/src/modules/simulation/classes/Wire.ts @@ -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 } } diff --git a/src/modules/simulation/constants.ts b/src/modules/simulation/constants.ts index 6e928a2..3b7d92b 100644 --- a/src/modules/simulation/constants.ts +++ b/src/modules/simulation/constants.ts @@ -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 + } } } diff --git a/src/modules/simulation/helpers/addGate.ts b/src/modules/simulation/helpers/addGate.ts new file mode 100644 index 0000000..2d84637 --- /dev/null +++ b/src/modules/simulation/helpers/addGate.ts @@ -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)) +} diff --git a/src/modules/simulation/helpers/getGateTimePipes.ts b/src/modules/simulation/helpers/getGateTimePipes.ts new file mode 100644 index 0000000..6c8e7a1 --- /dev/null +++ b/src/modules/simulation/helpers/getGateTimePipes.ts @@ -0,0 +1,19 @@ +import { GateTemplate } from '../types/GateTemplate' +import { debounceTime, throttleTime } from 'rxjs/operators' +import { MonoTypeOperatorFunction, pipe } from 'rxjs' + +export type TimePipe = MonoTypeOperatorFunction + +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] +} diff --git a/src/modules/simulation/types/GateTemplate.ts b/src/modules/simulation/types/GateTemplate.ts index 6716f7b..ed2b0cc 100644 --- a/src/modules/simulation/types/GateTemplate.ts +++ b/src/modules/simulation/types/GateTemplate.ts @@ -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 = + | { + 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 + } } diff --git a/src/modules/simulationRenderer/classes/Camera.ts b/src/modules/simulationRenderer/classes/Camera.ts index 0d7af61..5e0c8d7 100644 --- a/src/modules/simulationRenderer/classes/Camera.ts +++ b/src/modules/simulationRenderer/classes/Camera.ts @@ -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 } } diff --git a/src/modules/simulationRenderer/classes/SimulationRenderer.ts b/src/modules/simulationRenderer/classes/SimulationRenderer.ts index 088a83d..b8910fd 100644 --- a/src/modules/simulationRenderer/classes/SimulationRenderer.ts +++ b/src/modules/simulationRenderer/classes/SimulationRenderer.ts @@ -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() public mouseUpOutput = new Subject() public mouseMoveOutput = new Subject() + public wheelOutput = new Subject() - // 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, options: Partial = {}, 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) { diff --git a/src/modules/simulationRenderer/constants.ts b/src/modules/simulationRenderer/constants.ts index cc1359b..6133bdf 100644 --- a/src/modules/simulationRenderer/constants.ts +++ b/src/modules/simulationRenderer/constants.ts @@ -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: { diff --git a/src/modules/simulationRenderer/helpers/renderGate.ts b/src/modules/simulationRenderer/helpers/renderGate.ts index 338561d..7749d95 100644 --- a/src/modules/simulationRenderer/helpers/renderGate.ts +++ b/src/modules/simulationRenderer/helpers/renderGate.ts @@ -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() } } diff --git a/src/modules/simulationRenderer/helpers/renderSimulation.ts b/src/modules/simulationRenderer/helpers/renderSimulation.ts index f73c092..b77ae65 100644 --- a/src/modules/simulationRenderer/helpers/renderSimulation.ts +++ b/src/modules/simulationRenderer/helpers/renderSimulation.ts @@ -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)) } diff --git a/src/modules/simulationRenderer/helpers/scaleCanvas.ts b/src/modules/simulationRenderer/helpers/scaleCanvas.ts new file mode 100644 index 0000000..f6028bd --- /dev/null +++ b/src/modules/simulationRenderer/helpers/scaleCanvas.ts @@ -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 +} diff --git a/src/modules/simulationRenderer/helpers/wireConnectedToGate.ts b/src/modules/simulationRenderer/helpers/wireConnectedToGate.ts new file mode 100644 index 0000000..c8cf252 --- /dev/null +++ b/src/modules/simulationRenderer/helpers/wireConnectedToGate.ts @@ -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 diff --git a/src/modules/simulationRenderer/types/SimulationRendererOptions.ts b/src/modules/simulationRenderer/types/SimulationRendererOptions.ts index 1df581a..5920aaf 100644 --- a/src/modules/simulationRenderer/types/SimulationRendererOptions.ts +++ b/src/modules/simulationRenderer/types/SimulationRendererOptions.ts @@ -11,6 +11,11 @@ export interface SimulationRendererOptions { open: string closed: string } + gateStroke: { + active: string + normal: string + width: number + } } wires: { temporaryWireColor: string diff --git a/src/modules/storage/classes/LocalStore.ts b/src/modules/storage/classes/LocalStore.ts index 942ad60..d87cd55 100644 --- a/src/modules/storage/classes/LocalStore.ts +++ b/src/modules/storage/classes/LocalStore.ts @@ -32,7 +32,7 @@ export class LocalStore { } } - public get(key = 'index') { + public get(key = 'index'): T | undefined { return this.getAll()[key] } diff --git a/src/modules/vector2/helpers/basic.ts b/src/modules/vector2/helpers/basic.ts index e9ef333..d291b76 100644 --- a/src/modules/vector2/helpers/basic.ts +++ b/src/modules/vector2/helpers/basic.ts @@ -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