Merge branch 'prescientmoon:master' into patch-1

This commit is contained in:
Deleted user 2024-06-18 02:52:31 +02:00 committed by GitHub
commit a767a609c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2612 additions and 11659 deletions

41
.github/workflows/build.yaml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Build and deploy
on:
push:
pull_request:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build project
run: nix build .#erratic-gate-github-pages
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: ./result/www
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

3
.gitignore vendored
View file

@ -2,3 +2,6 @@ node_modules
dist dist
coverege coverege
idea idea
.direnv
result
.envrc

View file

@ -1,7 +0,0 @@
module.exports = {
semi: false,
trailingComma: 'none',
singleQuote: true,
printWidth: 80,
tabWidth: 4
}

7
.prettierrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"semi": false,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2
}

View file

@ -1,6 +0,0 @@
{
"editor.formatOnSave": true,
"prettier.eslintIntegration": true,
"explorer.autoReveal": false,
"typescript.tsdk": "node_modules/typescript/lib"
}

View file

@ -1,15 +0,0 @@
{
"Gate template": {
"prefix": "template",
"body": [
"import { PartialTemplate } from '../types/PartialTemplate'",
"",
"/**",
" * The template of the ${1:and} gate",
" */",
"const ${1}Template: PartialTemplate = ${2:[]}",
"",
"export default ${1}Template"
]
}
}

View file

@ -4,10 +4,8 @@ This is a logic gate simulator made for infoeducatie 2019.
## Getting started ## Getting started
Check out the [demo](https://logic-gate-simulator.herokuapp.com/) Check out the [demo](https://mateiadrielrafael.github.io/erratic-gate/)
Or read the [docs](./docs/main.md) Or read the [docs](./docs/main.md).
I've also made a [trello board](https://trello.com/b/LW3XSnGN/logic-gate-simulator) .
## Credits ## Credits
@ -21,30 +19,10 @@ Many thanks to:
- Simple & intuitive UI - Simple & intuitive UI
- Multiple simulations support - Multiple simulations support
- Integrated circuits - (Configurable) integrated circuits
- Multiple bits per pin - Multiple bits per pin
- Multiple language support: Romanian, English, Dutch, Turkish, Chinese & more in the future - Multiple language support: Romanian, English, Dutch, Turkish, Chinese & more in the future
## Playing with the source ## Playing with the source
To run locally clone this repo, and then install all dependencies by rrunning: This repo provides a nix flake for deployment.
```sh
npm install
```
Start the development server by running:
```sh
npm run dev
```
Or bundle the source with:
```sh
npm run build
```
## Assets & code wich i didn't make myself:
You can read a full list with what i don't own [here](./docs/assets-i-dont-own.md)

View file

@ -1,18 +0,0 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }]
],
env: {
test: {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
}
}
}

53
build.js Normal file
View file

@ -0,0 +1,53 @@
import * as esbuild from 'esbuild'
import { htmlPlugin } from '@craftamap/esbuild-plugin-html'
import { sassPlugin } from 'esbuild-sass-plugin'
import * as fs from 'fs'
const serve = process.env.ESBUILD_SERVE === '1'
const baseurl = process.env.ESBUILD_BASEURL || ''
console.log(`Building with baseurl ${baseurl}`)
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
minify: true,
bundle: true,
metafile: true,
splitting: true,
outdir: 'dist',
format: 'esm',
target: ['es2020'],
assetNames: 'assets/[name]',
chunkNames: 'chunks/[name]',
loader: {
'.svg': 'file'
},
define: {
'process.env.BASEURL': JSON.stringify(baseurl)
},
plugins: [
htmlPlugin({
files: [
{
filename: 'index.html',
entryPoints: ['src/index.ts'],
favicon: 'public/favicon.ico',
htmlTemplate: 'public/index.html',
scriptLoading: 'module'
}
]
}),
sassPlugin({})
],
publicPath: baseurl
})
if (serve) {
const { port, host } = await ctx.serve({ servedir: 'dist' })
console.log(`Serving on ${host}:${port}`)
} else {
await ctx.rebuild()
await ctx.dispose()
fs.cpSync('./dist/index.html', './dist/404.html') // Needed to please github pages
console.log(`Project bundled successfully`)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
docs/assets/components.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/assets/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

BIN
docs/assets/gates.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,33 @@
# Basic opeartions
To demonstrate the basic usage of the simulator we will construct one of the simplest logic gates: the `buffer` gate.
First of all we need to create something called an `integrated circuit`. You can think of integrated circuits like a box containing some components. You only put the components there once and can then reuse that box as many times as you want.
To create an integrated circuit, click on `Create simulation`, then on `integrated circuit` and type in the name of the circuit - in this case `buffer`.
You should see something like this:
![empty IC](../assets/empty.png)
Now we need to actually add some logic gates. To do that you need to:
1. click on `Logic gates`, which should bring up a screen which looks similar to this:
![gates](../assets/gates.png)
2. Click on the logic gates you want to add to your integrated circuit! In our case we need to add a `button` and a `light bulb`
> After you add any logic gate you should get a confirmation message which looks something like this: ![notification](../assets/notification.png)
3. To go back to the simulation view click the `Back to simulation` button. Now you should see something like this:
![components](../assets/components.png)
4. As you probably noticed, the components are on top of eachother. You can use your right mouse button to drag them around!
> Note 1: while your mouse is outside of gate you can start a selection. After you selected some gates you can use your mouse to drag all of them.
> Note 2: this isn't limited to movement. In the future you'll learn about many actions you can perform which also work on multiple gates :D
> Note 3: While we're on the topic of selection: You can use `ctrl + a` (or `command + a` on macs) to select everything

58
flake.lock Normal file
View file

@ -0,0 +1,58 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1695360818,
"narHash": "sha256-JlkN3R/SSoMTa+CasbxS1gq+GpGxXQlNZRUh9+LIy/0=",
"path": "/nix/store/09yvj6yyxspzfivv91bcxwrjxawpk1g2-source",
"rev": "e35dcc04a3853da485a396bdd332217d0ac9054f",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

60
flake.nix Normal file
View file

@ -0,0 +1,60 @@
{
description = "Logic gate simulator";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system};
in
rec {
packages.erratic-gate = pkgs.buildNpmPackage {
name = "erratic-gate";
buildInputs = [ pkgs.nodejs_18 ];
src = pkgs.lib.cleanSource ./.;
npmDepsHash = "sha256-f5mw6IjkhZgsIuzCz9d7DvoAdceY1y+yWXn1BOonsVI=";
ESBUILD_BASEURL = "";
installPhase = ''
mkdir $out
cp -r dist $out/www
'';
};
packages.erratic-gate-github-pages = packages.erratic-gate;
packages.default = packages.erratic-gate;
devShells.default =
pkgs.mkShell {
buildInputs = with pkgs;
with nodePackages_latest; [
nodejs
];
};
apps.compute-npm-dep-hash = {
type = "app";
program = pkgs.lib.getExe (pkgs.writeShellApplication {
name = "generate-layout-previes";
runtimeInputs = [ pkgs.prefetch-npm-deps ];
text = "prefetch-npm-deps ./package-lock.json";
});
};
}
);
# {{{ Caching and whatnot
nixConfig = {
# extra-substituters = [
# "erratic-gate.cachix.org-1:Ijiu/v//aVpKO4xBqV+2AM2s2uQYOnGCfoj9fYRXxtk" # I think I need this for neovim-nightly?
# ];
#
# extra-trusted-public-keys = [
# "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
# ];
};
# }}}
}

12837
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,61 +1,30 @@
{ {
"name": "logic-gate-simulator", "name": "erratic-gate",
"version": "1.0.0", "version": "1.0.0",
"main": "./src/index.ts", "type": "module",
"scripts": { "scripts": {
"dev": "webpack-dev-server --open --mode development", "build": "node ./build.js",
"build": "cross-env NODE_ENV=production webpack", "check": "tsc"
"build:server": "cross-env NODE_ENV=server webpack",
"show": "gource -f --start-date \"2019-07-01 12:00\" --key --hide dirnames,filenames,bloom -s 0.3",
"start": "node ./dist/server"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.5", "@craftamap/esbuild-plugin-html": "^0.6.1",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@types/deepmerge": "^2.2.0", "@types/deepmerge": "^2.2.0",
"@types/express": "^4.17.0",
"@types/gh-pages": "^2.0.0",
"@types/mainloop.js": "^1.0.5", "@types/mainloop.js": "^1.0.5",
"@types/react-helmet": "^5.0.8", "@types/node": "^20.8.9",
"@types/react-dom": "^18.2.14",
"@types/react-router-dom": "^4.3.4", "@types/react-router-dom": "^4.3.4",
"babel-loader": "^8.0.6", "esbuild": "^0.19.5",
"babel-plugin-transform-runtime": "^6.23.0", "esbuild-sass-plugin": "^2.16.0",
"babel-regenerator-runtime": "^6.5.0", "typescript": "^5.0.2"
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^5.2.0",
"css-loader": "^3.0.0",
"file-loader": "^4.1.0",
"gh-pages": "^2.0.1",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"typescript": "^3.8.3",
"webpack": "^4.36.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@eix-js/utils": "0.0.6", "@eix-js/utils": "0.0.6",
"@material-ui/core": "^4.2.1", "@material-ui/core": "^4.2.1",
"deepmerge": "^4.0.0", "deepmerge": "^4.0.0",
"express": "^4.17.1",
"keycode": "^2.2.0", "keycode": "^2.2.0",
"mainloop.js": "^1.0.4", "mainloop.js": "^1.0.4",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-helmet": "^5.2.1",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.0.1",
"react-toastify": "^5.3.2", "react-toastify": "^5.3.2",
"rxjs": "^6.5.2", "rxjs": "^6.5.2",

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

66
public/index.html Normal file
View file

@ -0,0 +1,66 @@
<!doctype html>
<html lang="en">
<head>
<title>Erratic gate</title>
<meta name="pinterest" content="nopin" />
<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/css?family=Righteous&display=swap"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<meta property="og:title" content="Erratic gate" />
<meta
property="og:description"
content="A logic gate simulator made for infoeducatie 2019"
/>
<meta
property="og:url"
content="https://mateiadrielrafael.github.io/erratic-gate/"
/>
<meta
name="Description"
content="A logic gate simulator made for infoeducatie 2019"
/>
<meta charset="UTF-8" />
</head>
<body
ondragstart="return false;"
ondrop="return false;"
oncontextmenu="return false"
>
<!--The react app is rendered here-->
<div id="app"></div>
<!--The splash screen structure-->
<div class="Splash">
<!--Loading animation-->
<div class="loading">
<div class="lds-ripple">
<div></div>
<div></div>
</div>
</div>
<!--In case someone tries to laod this with js disabled-->
<noscript> JavaScript must be enabled to run this app. </noscript>
</div>
</body>
</html>

View file

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- This doesnt work inside react helmetv-->
<meta name="pinterest" content="nopin" />
</head>
<body
ondragstart="return false;"
ondrop="return false;"
oncontextmenu="return false"
>
<!--The react app is rendered here-->
<div id="app"></div>
<!--The splash screen structure-->
<div class="Splash">
<!--Loading animation-->
<div class="loading">
<div class="lds-ripple">
<div></div>
<div></div>
</div>
</div>
<!--In case someone tries to laod this with js disabled-->
<noscript> JavaScript must be enabled to run this app. </noscript>
</div>
</body>
</html>

View file

@ -1,4 +1,3 @@
import React from 'react'
import App from './modules/core/components/App' import App from './modules/core/components/App'
import { render } from 'react-dom' import { render } from 'react-dom'
@ -10,6 +9,7 @@ import { take, filter } from 'rxjs/operators'
import { logWelcome } from './modules/core/helpers/logWelcome' import { logWelcome } from './modules/core/helpers/logWelcome'
import { initRenderer } from './modules/simulationRenderer/helpers/initRenderer' import { initRenderer } from './modules/simulationRenderer/helpers/initRenderer'
import { updateLogicGateList } from './modules/logic-gates/subjects/LogicGateList' import { updateLogicGateList } from './modules/logic-gates/subjects/LogicGateList'
import { initAutoSave } from './modules/simulation-actions/helpers/initAutoSave'
/** /**
* The function wich does the heavy lifting for starting the app * The function wich does the heavy lifting for starting the app
@ -18,7 +18,7 @@ export const start = async () => {
// This will resolve at the first render // This will resolve at the first render
const result = loadSubject const result = loadSubject
.pipe( .pipe(
filter(a => a), filter((a) => a),
take(1) take(1)
) )
.toPromise() .toPromise()
@ -41,6 +41,9 @@ export const start = async () => {
// Update the logic gates in local storage // Update the logic gates in local storage
updateLogicGateList() updateLogicGateList()
// start the autosaving stuff
initAutoSave()
// Render app component // Render app component
render(<App />, document.getElementById('app')) render(<App />, document.getElementById('app'))

View file

@ -1,55 +1,52 @@
import '../styles/reset' import '../styles/reset.scss'
import './App.scss' import './App.scss'
import './Scrollbars.scss' import './Scrollbars.scss'
import 'react-toastify/dist/ReactToastify.css' import 'react-toastify/dist/ReactToastify.css'
import { ToastContainer } from 'react-toastify' import { ToastContainer } from 'react-toastify'
import { theme as muiTheme } from '../constants' import { theme as muiTheme } from '../constants'
import { Route } from 'react-router-dom' import { BrowserRouter, Route } from 'react-router-dom'
import React, { useEffect } from 'react' import { useEffect } from 'react'
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import Theme from '@material-ui/styles/ThemeProvider' import Theme from '@material-ui/styles/ThemeProvider'
import Sidebar from './Sidebar' import Sidebar from './Sidebar'
import Head from './Head'
import Root from './Root' import Root from './Root'
import LogicGatePage from '../../logic-gates/components/LogicGatesPage' import LogicGatePage from '../../logic-gates/components/LogicGatesPage'
import { loadSubject } from '../subjects/loadedSubject' import { loadSubject } from '../subjects/loadedSubject'
import { CustomRouter } from './CustomRouter'
import LogicGateInfoPage from '../../logic-gate-info/components/LogicGateInfoPage' import LogicGateInfoPage from '../../logic-gate-info/components/LogicGateInfoPage'
const App = () => { const App = () => {
useEffect(() => { useEffect(() => {
loadSubject.next(true) loadSubject.next(true)
}) })
return ( return (
<> <>
<Head /> <CssBaseline />
<CssBaseline />
<Theme theme={muiTheme}> <Theme theme={muiTheme}>
<CustomRouter> <BrowserRouter basename={process.env.BASEURL}>
<Sidebar /> <Sidebar />
<Route path="/" component={Root} exact /> <Route path="/" component={Root} exact />
<Route path="/gates" component={LogicGatePage} /> <Route path="/gates" component={LogicGatePage} />
<Route path="/info/:name" component={LogicGateInfoPage} /> <Route path="/info/:name" component={LogicGateInfoPage} />
</CustomRouter> </BrowserRouter>
</Theme> </Theme>
<ToastContainer <ToastContainer
position="top-left" position="top-left"
autoClose={5000} autoClose={5000}
hideProgressBar={false} hideProgressBar={false}
newestOnTop={false} newestOnTop={false}
closeOnClick closeOnClick
rtl={false} rtl={false}
draggable draggable
pauseOnHover pauseOnHover
/> />
</> </>
) )
} }
export default App export default App

View file

@ -1,6 +0,0 @@
import { BrowserRouter } from 'react-router-dom'
import { history } from '../constants'
export class CustomRouter extends BrowserRouter {
public history = history
}

View file

@ -1,45 +0,0 @@
import Helmet from 'react-helmet'
import React from 'react'
const title = 'Logic gate simulator'
const description = 'A logic gate simulator made for infoeducatie 2019'
const url = 'https://logic-gate-simulator.herokuapp.com/'
const Head = () => {
return (
<Helmet>
<title>{title}</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="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<link
href="https://fonts.googleapis.com/css?family=Righteous&display=swap"
rel="stylesheet"
/>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta
name="Description"
content="A logic gate simulator made for infoeducatie 2019"
/>
<link rel="icon" href={require('../../../assets/favicon.ico')} />
</Helmet>
)
}
export default Head

View file

@ -1,9 +1,8 @@
import { createMuiTheme } from '@material-ui/core/styles' import { createTheme } from '@material-ui/core/styles'
import { red, deepPurple } from '@material-ui/core/colors' import { red, deepPurple } from '@material-ui/core/colors'
import { simulationMode } from '../saving/types/SimulationSave' import { simulationMode } from '../saving/types/SimulationSave'
import { createBrowserHistory } from 'history'
export const theme = createMuiTheme({ export const theme = createTheme({
palette: { palette: {
type: 'dark', type: 'dark',
primary: deepPurple, primary: deepPurple,
@ -32,8 +31,3 @@ export const icons: IconInterface = {
* The width of the sidebar * The width of the sidebar
*/ */
export const sidebarWidth = 240 export const sidebarWidth = 240
/**
* The history to be used by the router
*/
export const history = createBrowserHistory()

View file

@ -1,23 +1,22 @@
/** /**
* In case the guys who look at my projet open the console -_- * In case the people who look at my projet open the console -_-
*/ */
export const logWelcome = () => { export const logWelcome = () => {
const commonStyles = 'padding: 3px' const commonStyles = 'padding: 3px'
const titleStyles = `font-size: 3em;` const titleStyles = `font-size: 3em;`
// console.log('%c Hello!',` `${titleStyles}) console.log(
console.log( `%c Hello
`%c Hello
%c I don't know if you see this, %c I don't know if you see this,
but if you do than you are probably wondering... but if you do than you are probably wondering...
Why did I include this? The answer is - I don't know. Why did I include this? The answer is - I don't know.
At first, it seemed like a good idea to include a welcome message in the console At first, it seemed like a good idea to include a welcome message in the console
(in case someone randomly openes it), but now i don't even know what i'm doing :) (in case someone randomly openes it), but now i don't even know what i'm doing :)
Anyways, I hope you are having a good time in my simulator!!!` Anyways, I hope you are having a good time in my simulator!!!`
.split('\n') .split('\n')
.map(s => s.trim()) .map((s) => s.trim())
.join(' '), .join(' '),
`${titleStyles}`, `${titleStyles}`,
`${commonStyles}` `${commonStyles}`
) )
} }

View file

@ -169,8 +169,6 @@ audio,
canvas, canvas,
video { video {
display: inline-block; display: inline-block;
*display: inline;
*zoom: 1;
max-width: 100%; max-width: 100%;
} }
@ -269,7 +267,7 @@ legend {
border: 0; /* 1 */ border: 0; /* 1 */
padding: 0; padding: 0;
white-space: normal; /* 2 */ white-space: normal; /* 2 */
*margin-left: -7px; /* 3 */ margin-left: -7px; /* 3 */
} }
/** /**
@ -286,7 +284,6 @@ textarea {
font-size: 100%; /* 1 */ font-size: 100%; /* 1 */
margin: 0; /* 2 */ margin: 0; /* 2 */
vertical-align: baseline; /* 3 */ vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
} }
/** /**
@ -327,7 +324,7 @@ input[type="reset"],
input[type="submit"] { input[type="submit"] {
-webkit-appearance: button; /* 2 */ -webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */ cursor: pointer; /* 3 */
*overflow: visible; /* 4 */ overflow: visible; /* 4 */
} }
/** /**
@ -350,8 +347,8 @@ input[type='checkbox'],
input[type='radio'] { input[type='radio'] {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
padding: 0; /* 2 */ padding: 0; /* 2 */
*height: 13px; /* 3 */ height: 13px; /* 3 */
*width: 13px; /* 3 */ width: 13px; /* 3 */
} }
/** /**

View file

@ -2,7 +2,6 @@ import { SimulationState } from '../../saving/types/SimulationSave'
import { SimulationError } from '../../errors/classes/SimulationError' import { SimulationError } from '../../errors/classes/SimulationError'
import { import {
GateTemplate, GateTemplate,
Property,
PropGroup, PropGroup,
isGroup isGroup
} from '../../simulation/types/GateTemplate' } from '../../simulation/types/GateTemplate'
@ -24,9 +23,10 @@ import { getTemplateSafely } from '../../logic-gates/helpers/getTemplateSafely'
* Compiles a simulation into a logicGate * Compiles a simulation into a logicGate
* *
* @param simulaton The simulation to compile * @param simulaton The simulation to compile
* @param log Allow disabling logging
*/ */
export const compileIc = (state: SimulationState) => { export const compileIc = (state: SimulationState, log = false) => {
const { mode, name, gates } = state const { mode, name } = state
if (mode === 'project') { if (mode === 'project') {
throw new SimulationError('Cannot compile project') throw new SimulationError('Cannot compile project')
@ -59,7 +59,7 @@ export const compileIc = (state: SimulationState) => {
category: categories.ic, category: categories.ic,
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/ic') fill: require('../../../assets/ic.svg')
}, },
properties: { properties: {
enabled: !!props.length, enabled: !!props.length,
@ -82,10 +82,13 @@ export const compileIc = (state: SimulationState) => {
} }
templateStore.set(name, result) templateStore.set(name, result)
toast(
...createToastArguments( if (log) {
translation.messages.compiledIc(name), toast(
'markunread_mailbox' ...createToastArguments(
translation.messages.compiledIc(name),
'markunread_mailbox'
)
) )
) }
} }

View file

@ -1,7 +1,4 @@
import { import { CurrentLanguage } from '../stores/currentLanguage'
CurrentLanguage,
CurrentLanguageLocalStore
} from '../stores/currentLanguage'
import { allSupportedLanguages } from '../constants' import { allSupportedLanguages } from '../constants'
/** /**

View file

@ -7,12 +7,12 @@ export const DeutschTranslation: Translation = {
language: 'deutsch', language: 'deutsch',
sidebar: { sidebar: {
createSimulation: 'Simulation erstellen', createSimulation: 'Simulation erstellen',
logicGates: ' Logische Tore', logicGates: 'Logikgatter',
openSimulation: ' Simulationen öffnen ', openSimulation: 'Simulationen öffnen',
simulation: 'Simulation', simulation: 'Simulation',
language: ' Sprache ', language: 'Sprache',
backToSimulation: ' Zurück zur Simulation', backToSimulation: 'Zurück zur Simulation',
backToGates: ' Zurück zur Logische Tore' backToGates: 'Zurück zu den Logikgattern'
}, },
createSimulation: { createSimulation: {
mode: { mode: {
@ -23,33 +23,33 @@ export const DeutschTranslation: Translation = {
} }
}, },
name: { name: {
question: 'Was wollen Sie Ihre Simulation genannt werden?' question: 'Wie soll Ihre Simulation genannt werden?'
} }
}, },
actions: { actions: {
save: 'Speichern', save: 'Speichern',
clean: 'Reinigen', clean: 'Bereinigen',
refresh: 'Aktualisierung', refresh: 'Aktualisieren',
undo: 'Rückgängig machen', undo: 'Rückgängig machen',
paste: 'Einfügen', paste: 'Einfügen',
copy: 'Kopieren', copy: 'Kopieren',
duplicate: 'Duplizieren', duplicate: 'Duplizieren',
cut: 'Schnitt', cut: 'Ausschneiden',
'select all': 'Wählen Sie Alle', 'select all': 'Alles auswählen',
'delete selection': 'Auswahl löschen', 'delete selection': 'Auswahl löschen',
'delete simulation': 'Simulation löschen' 'delete simulation': 'Simulation löschen'
}, },
messages: { messages: {
createdSimulation: name => `Erfolgreich erstellte Simulation '${name}'`, createdSimulation: name => `Simulation '${name}' erfolgreich erstellt`,
switchedToSimulation: name => switchedToSimulation: name =>
`Erfolgreich auf Simulation umgestellt '${name}'`, `Erfolgreich zu Simulation '${name}' gewechselt`,
savedSimulation: name => savedSimulation: name =>
`Erfolgreich gespeicherte Simulation '${name}'`, `Simulation '${name}' erfolgreich gespeichert`,
compiledIc: name => `Erfolgreich kompilierte Schaltung '${name}'`, compiledIc: name => `Integrierter Schaltkreis '${name}' erfolgreich kompiliert`,
cleaned: name => `Erfolgreich bereinigte Simulation '${name}'`, cleaned: name => `Simulation '${name}' erfolgreich bereinigt`,
refreshed: name => `Erfolgreich aktualisierte Simulation '${name}'`, refreshed: name => `Simulation '${name}' erfolgreich aktualisiert`,
undone: name => `Simulation erfolgreich rückgängig gemacht ' ${name}'`, undone: name => `Simulation '${name}' erfolgreich Rückgängig gemacht`,
deletedSimulation: name => `Simulation erfolgreich gelöscht ' ${name}'`, deletedSimulation: name => `Simulation '${name}' erfolgreich gelöscht`,
addedGate: name => `Tor erfolgreich hinzugefügt` addedGate: name => `Logikgatter erfolgreich hinzugefügt`
} }
} }

View file

@ -40,15 +40,16 @@ export const EnglishTranslation: Translation = {
'delete simulation': 'Delete simulation' 'delete simulation': 'Delete simulation'
}, },
messages: { messages: {
createdSimulation: name => `Succesfully created simulation '${name}'`, autoSave: 'The sim saves your work automatically :D',
switchedToSimulation: name => createdSimulation: (name) => `Succesfully created simulation '${name}'`,
switchedToSimulation: (name) =>
`Succesfully switched to simulation '${name}'`, `Succesfully switched to simulation '${name}'`,
savedSimulation: name => `Succesfully saved simulation '${name}'`, savedSimulation: (name) => `Succesfully saved simulation '${name}'`,
compiledIc: name => `Succesfully compiled circuit '${name}'`, compiledIc: (name) => `Succesfully compiled circuit '${name}'`,
cleaned: name => `Succesfully cleaned simulation '${name}'`, cleaned: (name) => `Succesfully cleaned simulation '${name}'`,
refreshed: name => `Succesfully refreshed simulation '${name}'`, refreshed: (name) => `Succesfully refreshed simulation '${name}'`,
undone: name => `Succesfully undone simulation '${name}'`, undone: (name) => `Succesfully undone simulation '${name}'`,
deletedSimulation: name => `Succesfully deleted simulation '${name}'`, deletedSimulation: (name) => `Succesfully deleted simulation '${name}'`,
addedGate: name => `Succesfully added gate '${name}'` addedGate: (name) => `Succesfully added gate '${name}'`
} }
} }

View file

@ -29,6 +29,7 @@ export interface Translation {
} }
} }
messages: { messages: {
autoSave?: string
createdSimulation: NameSentence createdSimulation: NameSentence
switchedToSimulation: NameSentence switchedToSimulation: NameSentence
savedSimulation: NameSentence savedSimulation: NameSentence

View file

@ -117,7 +117,7 @@ const GateRawProperty = ({
raw.type === 'text' || raw.type === 'text' ||
raw.type === 'string' raw.type === 'string'
) { ) {
return (input = ( return (
<TextField <TextField
onChange={handleChange} onChange={handleChange}
label={displayableName} label={displayableName}
@ -126,9 +126,9 @@ const GateRawProperty = ({
multiline={raw.type === 'string'} multiline={raw.type === 'string'}
rowsMax={7} rowsMax={7}
/> />
)) )
} else if (raw.type === 'boolean') { } else if (raw.type === 'boolean') {
return (input = ( return (
<> <>
<span className="checkbox-label">{displayableName}</span> <span className="checkbox-label">{displayableName}</span>
<CheckBox <CheckBox
@ -139,7 +139,7 @@ const GateRawProperty = ({
checked={!!outputSnapshot} checked={!!outputSnapshot}
/>{' '} />{' '}
</> </>
)) )
} }
})() })()

View file

@ -10,30 +10,29 @@ import LogicGate from './LogicGate'
* The component containing the info / actions about all logic gates * The component containing the info / actions about all logic gates
*/ */
const LogicGatePage = () => { const LogicGatePage = () => {
const gates = useObservable(() => LogicGateList, []) const gates = useObservable(() => LogicGateList, [])
const renderer = getRendererSafely() const renderer = getRendererSafely()
return ( return (
<main> <main>
<div className="page" id="gates-page"> <div className="page" id="gates-page">
<div className="gate-grid"> <div className="gate-grid">
{gates {gates
.map(getTemplateSafely) .map(getTemplateSafely)
.sort((a, b) => a.category - b.category) .sort((a, b) => a.category - b.category)
.filter(template => { .filter((template) => {
return ( return (
renderer.simulation.mode === 'project' || renderer.simulation.mode === 'project' ||
template.metadata.name !== template.metadata.name !== renderer.simulation.name
renderer.simulation.name )
) })
}) .map((template, index) => {
.map((template, index) => { return <LogicGate key={index} template={template} />
return <LogicGate key={index} template={template} /> })}
})} </div>
</div> </div>
</div> </main>
</main> )
)
} }
export default LogicGatePage export default LogicGatePage

View file

@ -1,8 +1,8 @@
@import '../../core/styles/indexes.scss'; @import '../../../core/styles/indexes.scss';
@import '../../core/styles/colors.scss'; @import '../../../core/styles/colors.scss';
@import '../../core/styles/mixins/flex.scss'; @import '../../../core/styles/mixins/flex.scss';
@import '../../core/styles/mixins/full-screen.scss'; @import '../../../core/styles/mixins/full-screen.scss';
@import '../../core/styles/mixins/visibility.scss'; @import '../../../core/styles/mixins/visibility.scss';
@mixin modal-container { @mixin modal-container {
@include flex(); @include flex();

View file

@ -7,6 +7,20 @@ import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments' import { createToastArguments } from '../../toasts/helpers/createToastArguments'
import { CurrentLanguage } from '../../internalisation/stores/currentLanguage' import { CurrentLanguage } from '../../internalisation/stores/currentLanguage'
import { compileIc } from '../../integrated-circuits/helpers/compileIc' import { compileIc } from '../../integrated-circuits/helpers/compileIc'
import { EnglishTranslation } from '../../internalisation/translations/english'
export const notifyAboutAutosave = () => {
const translation = CurrentLanguage.getTranslation()
toast(
...createToastArguments(
translation.messages.autoSave ||
EnglishTranslation.messages.autoSave ||
'this sentence was not translated yet',
'save'
)
)
}
/** /**
* Saves the state from a renderer in localStorage, * Saves the state from a renderer in localStorage,
@ -22,20 +36,11 @@ export const save = (renderer: SimulationRenderer) => {
if (current) { if (current) {
const state = getRendererState(renderer) const state = getRendererState(renderer)
const translation = CurrentLanguage.getTranslation()
saveStore.set(current, state) saveStore.set(current, state)
if (state.simulation.mode === 'ic') { if (state.simulation.mode === 'ic') {
compileIc(state.simulation) compileIc(state.simulation)
} }
toast(
...createToastArguments(
translation.messages.savedSimulation(current),
'save'
)
)
} else { } else {
throw new SimulationError( throw new SimulationError(
'Cannot save without knowing the name of the active simulation' 'Cannot save without knowing the name of the active simulation'

View file

@ -5,6 +5,8 @@ import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments' import { createToastArguments } from '../../toasts/helpers/createToastArguments'
import { dumpSimulation } from './dumpSimulation' import { dumpSimulation } from './dumpSimulation'
import { CurrentLanguage } from '../../internalisation/stores/currentLanguage' import { CurrentLanguage } from '../../internalisation/stores/currentLanguage'
import { compileIc } from '../../integrated-circuits/helpers/compileIc'
import { getRendererState } from './getState'
/** /**
* Used to switch to a simulation * Used to switch to a simulation
@ -22,6 +24,12 @@ export const switchTo = (simulationName: string = 'default') => {
if (rendererSubject.value) { if (rendererSubject.value) {
const renderer = rendererSubject.value const renderer = rendererSubject.value
const translation = CurrentLanguage.getTranslation() const translation = CurrentLanguage.getTranslation()
const state = getRendererState(renderer)
// compile the ic just in case
if (state.simulation.mode === 'ic') {
compileIc(state.simulation, true)
}
dumpSimulation(renderer) dumpSimulation(renderer)

View file

@ -25,7 +25,7 @@ const _4bitDecoderTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/4decoder') fill: require('../../../assets/4decoder.svg')
}, },
shape: { shape: {
scale: _4BitScale scale: _4BitScale

View file

@ -24,7 +24,7 @@ const _4bitEncoderTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/4encoder') fill: require('../../../assets/4encoder.svg')
}, },
shape: { shape: {
scale: _4BitScale scale: _4BitScale

View file

@ -9,7 +9,7 @@ const andTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/and') fill: require('../../../assets/and.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -24,7 +24,7 @@ const bitMergerTemplate: PartialTemplate = {
category: categories.compressing, category: categories.compressing,
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/merger') fill: require('../../../assets/merger.svg')
} }
} }

View file

@ -27,7 +27,7 @@ const bitSplitterTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/splitter') fill: require('../../../assets/splitter.svg')
}, },
category: categories.compressing, category: categories.compressing,
pins: { pins: {

View file

@ -11,7 +11,7 @@ const fullAdderTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/full-adder') fill: require('../../../assets/full-adder.svg')
}, },
code: { code: {
activation: adderActivation(true) activation: adderActivation(true)

View file

@ -11,7 +11,7 @@ const halfAdderTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/half-adder') fill: require('../../../assets/half-adder.svg')
}, },
code: { code: {
activation: adderActivation(false) activation: adderActivation(false)

View file

@ -9,7 +9,7 @@ const nandTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/nand') fill: require('../../../assets/nand.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -9,7 +9,7 @@ const norTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/nor') fill: require('../../../assets/nor.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -9,7 +9,7 @@ const notTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/not') fill: require('../../../assets/not.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -9,7 +9,7 @@ const orTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/or') fill: require('../../../assets/or.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -11,7 +11,7 @@ const parallelDelayerTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/parallel') fill: require('../../../assets/parallel.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -11,7 +11,7 @@ const sequentialDelayerTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/sequential') fill: require('../../../assets/sequential.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -9,7 +9,7 @@ const xnorTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/xnor') fill: require('../../../assets/xnor.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -9,7 +9,7 @@ const xorTemplate: PartialTemplate = {
}, },
material: { material: {
type: 'image', type: 'image',
fill: require('../../../assets/xor') fill: require('../../../assets/xor.svg')
}, },
code: { code: {
activation: ` activation: `

View file

@ -1,6 +1,6 @@
import { SidebarAction } from './types/SidebarAction' import { SidebarAction } from './types/SidebarAction'
import { possibleAction } from './types/possibleAction' import { possibleAction } from './types/possibleAction'
import { save } from '../saving/helpers/save' import { save, notifyAboutAutosave } from '../saving/helpers/save'
import { refresh } from './helpers/refresh' import { refresh } from './helpers/refresh'
import { undo } from './helpers/undo' import { undo } from './helpers/undo'
import { createActionConfig } from './helpers/createActionConfig' import { createActionConfig } from './helpers/createActionConfig'
@ -30,7 +30,7 @@ export const actionIcons: Record<possibleAction, string> = {
* Array with all the actions for the SimulationAction component to render * Array with all the actions for the SimulationAction component to render
*/ */
export const SidebarActions: Record<possibleAction, SidebarAction> = { export const SidebarActions: Record<possibleAction, SidebarAction> = {
...createActionConfig('save', save, ['control', 's']), ...createActionConfig('save', notifyAboutAutosave, ['control', 's']),
...createActionConfig( ...createActionConfig(
'refresh', 'refresh',
{ {

View file

@ -0,0 +1,12 @@
import { interval } from 'rxjs'
import { save } from '../../saving/helpers/save'
import { getRendererSafely } from '../../logic-gates/helpers/getRendererSafely'
const everySecond = interval(1000)
export const initAutoSave = () => {
everySecond.subscribe(() => {
const renderer = getRendererSafely()
save(renderer)
})
}

View file

@ -3,35 +3,30 @@ import { PinWrapper } from './Gate'
import { SimulationError } from '../../errors/classes/SimulationError' import { SimulationError } from '../../errors/classes/SimulationError'
export class Wire { export class Wire {
public id: number public id: number
public active = true public active = true
public constructor( public constructor(
public start: PinWrapper, public start: PinWrapper,
public end: PinWrapper, public end: PinWrapper,
ic: boolean = false, ic: boolean = false,
id?: number id?: number
) { ) {
if (!ic && end.value.pairs.size !== 0) { if (!ic && end.value.pairs.size !== 0) {
throw new SimulationError('An input pin can only have 1 pair') throw new SimulationError('An input pin can only have 1 pair')
}
end.value.addPair(start.value, true, !ic)
start.value.addPair(end.value, false, !ic)
// if (ic) {
// start.value.state.subscribe(console.log)
// end.value.state.subscribe(console.log)
// }
this.id = id !== undefined ? id : idStore.generate()
} }
public dispose() { end.value.addPair(start.value, true, !ic)
this.end.value.removePair(this.start.value) start.value.addPair(end.value, false, !ic)
this.start.value.removePair(this.end.value)
this.end.value.state.next("0")
this.active = false this.id = id !== undefined ? id : idStore.generate()
} }
public dispose() {
this.end.value.removePair(this.start.value)
this.start.value.removePair(this.end.value)
this.end.value.state.next('0')
this.active = false
}
} }

View file

@ -70,7 +70,8 @@ export const DefaultGateTemplate: GateTemplate = {
enabled: false, enabled: false,
color: 'white' color: 'white'
}, },
category: categories.basic category: categories.basic,
info: []
} }
/** /**

View file

@ -92,4 +92,5 @@ export interface GateTemplate {
enabled: boolean enabled: boolean
} }
category: number // for better sorting category: number // for better sorting
info: string[]
} }

View file

@ -9,8 +9,8 @@ import { SelectedPins } from '../types/SelectedPins'
import { currentStore } from '../../saving/stores/currentStore' import { currentStore } from '../../saving/stores/currentStore'
import { saveStore } from '../../saving/stores/saveStore' import { saveStore } from '../../saving/stores/saveStore'
import { import {
fromSimulationState, fromSimulationState,
fromCameraState fromCameraState
} from '../../saving/helpers/fromState' } from '../../saving/helpers/fromState'
import merge from 'deepmerge' import merge from 'deepmerge'
import { handleScroll } from '../helpers/scaleCanvas' import { handleScroll } from '../helpers/scaleCanvas'
@ -29,134 +29,132 @@ import { handleMouseMove } from '../helpers/handleMouseMove'
import { Gate } from '../../simulation/classes/Gate' import { Gate } from '../../simulation/classes/Gate'
export class SimulationRenderer { export class SimulationRenderer {
public mouseDownOutput = new Subject<MouseEventInfo>() public mouseDownOutput = new Subject<MouseEventInfo>()
public mouseUpOutput = new Subject<MouseEventInfo>() public mouseUpOutput = new Subject<MouseEventInfo>()
public mouseMoveOutput = new Subject<MouseEventInfo>() public mouseMoveOutput = new Subject<MouseEventInfo>()
public wheelOutput = new Subject<unknown>() public wheelOutput = new Subject<unknown>()
public selectedGates: Record<selectionType, Set<number>> = { public selectedGates: Record<selectionType, Set<number>> = {
temporary: new Set<number>(), temporary: new Set<number>(),
permanent: new Set<number>() permanent: new Set<number>()
} }
public options: SimulationRendererOptions public options: SimulationRendererOptions
public camera = new Camera() public camera = new Camera()
public selectedArea = new Transform() public selectedArea = new Transform()
public clipboard: GateInitter[] = [] public clipboard: GateInitter[] = []
public wireClipboard: WireState[] = [] public wireClipboard: WireState[] = []
// first bit = dragging // first bit = dragging
// second bit = panning // second bit = panning
// third bit = selecting // third bit = selecting
public mouseState = 0b000 public mouseState = 0b000
public lastMousePosition: vector2 = [0, 0] public lastMousePosition: vector2 = [0, 0]
// this is used for spawning gates // this is used for spawning gates
public spawnCount = 0 public spawnCount = 0
public selectedPins: SelectedPins = { public selectedPins: SelectedPins = {
start: null, start: null,
end: null end: null
} }
public constructor( public constructor(
options: Partial<SimulationRendererOptions> = {}, options: Partial<SimulationRendererOptions> = {},
public simulation = new Simulation('project', 'default') public simulation = new Simulation('project', 'default')
) { ) {
this.options = merge(defaultSimulationRendererOptions, options) this.options = merge(defaultSimulationRendererOptions, options)
this.init() this.init()
} }
public init() { public init() {
this.mouseDownOutput.subscribe(handleMouseDown(this)) this.mouseDownOutput.subscribe(handleMouseDown(this))
this.mouseUpOutput.subscribe(handleMouseUp(this)) this.mouseUpOutput.subscribe(handleMouseUp(this))
this.mouseMoveOutput.subscribe(handleMouseMove(this)) this.mouseMoveOutput.subscribe(handleMouseMove(this))
this.reloadSave() this.reloadSave()
} }
public updateWheelListener(ref: RefObject<HTMLCanvasElement>) { public updateWheelListener(ref: RefObject<HTMLCanvasElement>) {
if (ref.current) { if (ref.current) {
ref.current.addEventListener('wheel', (event) => { ref.current.addEventListener('wheel', (event) => {
if (!modalIsOpen() && location.pathname === '/') { if (!modalIsOpen() && location.pathname === `${process.env.BASEURL}/`) {
event.preventDefault() event.preventDefault()
handleScroll(event, this.camera) handleScroll(event, this.camera)
}
})
} }
})
} }
}
/** /**
* Loads a simulation state * Loads a simulation state
* *
* @param save LThe state to load * @param save LThe state to load
*/ */
public loadSave(save: RendererState) { public loadSave(save: RendererState) {
this.simulation = fromSimulationState(save.simulation) this.simulation = fromSimulationState(save.simulation)
this.camera = fromCameraState(save.camera) this.camera = fromCameraState(save.camera)
} }
public reloadSave() { public reloadSave() {
dumpSimulation(this) dumpSimulation(this)
const current = currentStore.get() const current = currentStore.get()
const save = saveStore.get(current) const save = saveStore.get(current)
if (!save) return if (!save) return
if (!(save.simulation || save.camera)) return if (!(save.simulation || save.camera)) return
this.loadSave(save) this.loadSave(save)
} }
/** /**
* Gets all selected gates in the simulation * Gets all selected gates in the simulation
* *
* @throws SimulationError if an id isnt valid * @throws SimulationError if an id isnt valid
* @throws SimulationError if the id doesnt have a data prop * @throws SimulationError if the id doesnt have a data prop
*/ */
public getSelected(): Gate[] { public getSelected(): Gate[] {
return setToArray(this.allSelectedIds()).map((id) => { return setToArray(this.allSelectedIds()).map((id) => {
const gate = this.simulation.gates.get(id) const gate = this.simulation.gates.get(id)
if (!gate) { if (!gate) {
throw new SimulationError(`Cannot find gate with id ${id}`) throw new SimulationError(`Cannot find gate with id ${id}`)
} else if (!gate.data) { } else if (!gate.data) {
throw new SimulationError( throw new SimulationError(`Cannot find data of gate with id ${id}`)
`Cannot find data of gate with id ${id}` }
)
}
return gate.data return gate.data
}) })
} }
/** /**
* helper to merge the temporary and permanent selection * helper to merge the temporary and permanent selection
*/ */
public allSelectedIds() { public allSelectedIds() {
return new Set([ return new Set([
...setToArray(this.selectedGates.permanent), ...setToArray(this.selectedGates.permanent),
...setToArray(this.selectedGates.temporary) ...setToArray(this.selectedGates.temporary)
]) ])
} }
/** /**
* Helper to clear all selected sets * Helper to clear all selected sets
*/ */
public clearSelection() { public clearSelection() {
this.selectedGates.permanent.clear() this.selectedGates.permanent.clear()
this.selectedGates.temporary.clear() this.selectedGates.temporary.clear()
} }
/** /**
* Clears the selected pins of the renderer * Clears the selected pins of the renderer
*/ */
public clearPinSelection() { public clearPinSelection() {
this.selectedPins.end = null this.selectedPins.end = null
this.selectedPins.start = null this.selectedPins.start = null
} }
} }

View file

@ -3,28 +3,26 @@ import { SimulationRenderer } from '../classes/SimulationRenderer'
import { fromHexColorString, fromChunks } from '../../colors/helpers/fromHex' import { fromHexColorString, fromChunks } from '../../colors/helpers/fromHex'
export const pinFill = (renderer: SimulationRenderer, pin: Pin) => { export const pinFill = (renderer: SimulationRenderer, pin: Pin) => {
let color = 'rgba(0,0,0,0)' let color = 'rgba(0,0,0,0)'
if (pin.pairs.size) { if (pin.pairs.size) {
const { open, closed } = renderer.options.gates.pinFill const { open, closed } = renderer.options.gates.pinFill
const digits = Array.from(pin.state.value).map(Number) const digits = Array.from(pin.state.value).map(Number)
const colors = digits.map(digit => (digit ? open : closed)) const colors = digits.map((digit) => (digit ? open : closed))
const chunked = colors.map(fromHexColorString) const chunked = colors.map(fromHexColorString)
const summed = [0, 1, 2] const summed = [0, 1, 2]
.map(key => .map((key) =>
chunked chunked
.flat() .flat()
.filter((v, index) => index % 3 === key) .filter((v, index) => index % 3 === key)
.reduce((acc, curr) => acc + curr, 0) .reduce((acc, curr) => acc + curr, 0)
) )
.map(value => Math.floor(value / digits.length)) .map((value) => Math.floor(value / digits.length))
color = fromChunks(summed) color = fromChunks(summed)
}
// console.log(color) return color
}
return color
} }

View file

@ -20,28 +20,28 @@ b * (a - x0) = a - x1
x1 = a - b * (a - x0) x1 = a - b * (a - x0)
*/ */
export const handleScroll = (e: WheelEvent, camera: Camera) => { export const handleScroll = (e: WheelEvent, camera: Camera) => {
const sign = -e.deltaY / Math.abs(e.deltaY) const sign = -e.deltaY / Math.abs(e.deltaY)
const zoom = scrollStep ** sign const zoom = scrollStep ** sign
if (!e.deltaY) { if (!e.deltaY) {
return return
} }
const { position, scale } = camera.transform const { position, scale } = camera.transform
const mousePosition = [e.clientX, e.clientY] const mousePosition = [e.clientX, e.clientY]
const oldPosition = [...mousePosition] as vector2 const oldPosition = [...mousePosition] as vector2
const oldScale = scale[0] const oldScale = scale[0]
const newScale = clamp(zoomLimits[0], zoomLimits[1], oldScale * zoom) const newScale = clamp(zoomLimits[0], zoomLimits[1], oldScale * zoom)
camera.transform.scale = repeat(newScale, 2) as vector2 camera.transform.scale = repeat(newScale, 2) as vector2
const scaleFraction = newScale / oldScale const scaleFraction = newScale / oldScale
const newPosition = substract( const newPosition = substract(
oldPosition, oldPosition,
multiply(substract(oldPosition, position), scaleFraction) multiply(substract(oldPosition, position), scaleFraction)
) )
camera.transform.position = newPosition camera.transform.position = newPosition
} }

View file

@ -1,6 +1,3 @@
import { CacheInstancesByKey } from '@eix-js/utils'
@CacheInstancesByKey(Infinity)
export class LocalStore<T> { export class LocalStore<T> {
public constructor(public name: string) { public constructor(public name: string) {
if (!localStorage.getItem(name)) { if (!localStorage.getItem(name)) {
@ -13,9 +10,7 @@ export class LocalStore<T> {
if (!raw) if (!raw)
throw new Error( throw new Error(
`An error occured when accesing ${ `An error occured when accesing ${this.name} in the local storage!`
this.name
} in the local storage!`
) )
else { else {
return JSON.parse(raw) return JSON.parse(raw)

View file

@ -1,4 +0,0 @@
User-agent: *
Disallow: /server.js
Disallow: /js
Disallow: /css

View file

@ -1,16 +0,0 @@
import express, { static as _static } from 'express'
import { resolve } from 'path'
// create express app
const app = express()
// serve static assets
app.use(_static(__dirname))
// serve single page application
app.get('*', (rex, res) => {
res.sendFile(resolve(__dirname, 'index.html'))
})
// listen to the port from .env (default to 8080)
app.listen(process.env.PORT || 8080)

View file

@ -1,14 +1,26 @@
{ {
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node", "target": "ESNext",
"esModuleInterop": true, "lib": [
"jsx": "preserve", "DOM",
"experimentalDecorators": true, "DOM.Iterable",
"target": "ESNext", "ESNext"
"downlevelIteration": true, ],
"strictNullChecks": true, "module": "ESNext",
"module": "esnext" "skipLibCheck": true,
}, /* Bundler mode */
"exclude": ["node_modules"], "moduleResolution": "bundler",
"include": ["src"] "allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Features */
"experimentalDecorators": true,
"downlevelIteration": true,
"strictNullChecks": true
},
"include": [
"src"
],
} }

View file

@ -1,171 +0,0 @@
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const webpackMerge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const CopyPlugin = require('copy-webpack-plugin')
const isProduction = process.env.NODE_ENV === 'production'
const projectRoot = resolve(__dirname)
const sourceFolder = resolve(projectRoot, 'src')
const buildFolder = resolve(projectRoot, 'dist')
const htmlTemplateFile = resolve(sourceFolder, 'index.html')
const babelRule = {
test: /\.(js|tsx?)$/,
use: 'babel-loader'
}
const fileRule = {
test: /\.(png|svg|jpg|gif|ico)$/,
use: ['file-loader']
}
const cssAndSass = [
isProduction
? MiniCssExtractPlugin.loader
: {
loader: 'style-loader',
options: {
singleton: true
}
},
'css-loader'
]
const cssRule = {
test: /\.css$/,
use: cssAndSass
}
const sassRule = {
test: /\.scss$/,
use: [
...cssAndSass,
{
loader: 'sass-loader',
options: {
includePaths: [sourceFolder]
}
}
]
}
const serverConfig = {
mode: 'production',
target: 'node',
externals: [nodeExternals()],
module: {
rules: [babelRule]
},
resolve: {
extensions: ['.ts', '.js', '.json']
},
entry: [resolve(sourceFolder, 'server')],
output: {
filename: 'server.js',
path: buildFolder,
publicPath: '/'
},
node: {
__dirname: false
},
plugins: [
new CopyPlugin([
{
from: resolve(sourceFolder, 'public'),
to: buildFolder
}
])
]
}
const baseConfig = {
mode: 'none',
entry: ['babel-regenerator-runtime', resolve(sourceFolder, 'index')],
output: {
filename: 'js/[name].js',
path: buildFolder,
publicPath: '/'
},
module: {
rules: [babelRule, sassRule, cssRule, fileRule]
},
resolve: {
extensions: [
'.js',
'.ts',
'.tsx',
'.scss',
'.png',
'.svg',
'.jpg',
'.gif'
]
}
}
const devConfig = {
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
template: htmlTemplateFile,
chunksSortMode: 'dependency'
})
],
devtool: 'inline-source-map',
devServer: {
historyApiFallback: true
}
}
const prodConfig = {
mode: 'production',
optimization: {
minimize: true,
nodeEnv: 'production'
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].min.css'
}),
new OptimizeCssAssetsWebpackPlugin(),
new HtmlWebpackPlugin({
template: htmlTemplateFile,
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
},
inject: true
// favicon: faviconPath
}),
new HtmlWebpackInlineSourcePlugin()
],
devtool: 'source-map'
}
function getFinalConfig() {
if (process.env.NODE_ENV === 'production') {
console.info('Running production config')
return [webpackMerge(baseConfig, prodConfig), serverConfig]
} else if (process.env.NODE_ENV === 'server') {
console.info('Running server config')
return [serverConfig]
}
console.info('Running development config')
return webpackMerge(baseConfig, devConfig)
}
module.exports = getFinalConfig()