curves for wires & importing logic gates

This commit is contained in:
Matei Adriel 2019-07-11 20:51:14 +03:00
parent 443d049db6
commit b06a4c7441
22 changed files with 1117 additions and 588 deletions

7
.prettierrc.js Normal file
View file

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

6
.vscode/settings.json vendored Normal file
View file

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

2
README.txt Normal file
View file

@ -0,0 +1,2 @@
Pentru a rula aplicatia, deschideti dist/index.html.
Codul sursa se afla in folderul src.

View file

@ -1,53 +1,47 @@
const { publish } = require("gh-pages") const { publish } = require('gh-pages')
const { exec } = require("child_process") const { exec } = require('child_process')
const { random } = require("random-emoji") const { random } = require('random-emoji')
// const { publish } = require("gh-pages") // const { publish } = require("gh-pages")
const args = process.argv.splice(2) const args = process.argv.splice(2)
const randomEmoji = () => random({ count: 1 })[0].character const randomEmoji = () => random({ count: 1 })[0].character
const mFlag = ((args.indexOf("--message") + 1) || (args.indexOf("-m") + 1)) - 1 const mFlag = (args.indexOf('--message') + 1 || args.indexOf('-m') + 1) - 1
const message = `${randomEmoji()} ${(mFlag >= 0) ? args[mFlag + 1] : "automated update"} ${randomEmoji()}` const message = `${mFlag >= 0 ? args[mFlag + 1] : 'automated update'}`
console.log("Deploying..."); console.log('Deploying...')
const run = (command: string): Promise<string> => { const run = (command: string): Promise<string> => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
console.log(`🏃 Running: '${command}'`) console.log(`🏃 Running: '${command}'`)
//@ts-ignore //@ts-ignore
exec(command, (err, stdout, stderr) => { exec(command, (err, stdout, stderr) => {
if (err != null) if (err != null) rej(err)
rej(err) else if (typeof stderr != 'string') rej(new Error(stderr))
else if (typeof (stderr) != "string") else res(stdout)
rej(new Error(stderr))
else
res(stdout)
}) })
}) })
} }
;(async () => {
(async () => {
try { try {
if (!args.includes("--skipBuild") && !args.includes("-sb")) if (!args.includes('--skipBuild') && !args.includes('-sb'))
await run("npm run build") await run('npm run build')
await run("git add .") await run('git add .')
await run(`git commit -m " ${message} "`) await run(`git commit -m " ${message} "`)
await run("git push origin master") await run('git push origin master')
await new Promise((res, rej) => { await new Promise((res, rej) => {
console.log("🏃 Updating github pages") console.log('🏃 Updating github pages')
//@ts-ignore //@ts-ignore
publish("dist", (err) => { publish('dist', err => {
if (err) if (err) rej(err)
rej(err)
console.log(`😄 Succesfully published to github pages`) console.log(`😄 Succesfully published to github pages`)
res(true) res(true)
}) })
}) })
} } catch (err) {
catch (err) {
console.log(`😭 Something went wrong: ${err}`) console.log(`😭 Something went wrong: ${err}`)
} }
})() })()

BIN
docs/assets/gist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
docs/assets/gist_url.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

93
docs/controls.md Normal file
View file

@ -0,0 +1,93 @@
# Controls
## Moving around
- To move around, just click anywhere in the enviroment, drag in the oposite of the direction you want to moe in.
- Release when you finished the desired movement.
## Zooming
- To zoom in, scroll upwards.
- To zoom out, scroll downwards.
- The zoom will be applied in the position pointed by the mouse.
## Moving a component
- To move a component, left click on it.
- The gate will follow your mouse
- Release when the gate got in the desired position
## Deleting a component
- To remove a component, right click on it.
## Connection 2 pins
- To connect 2 pins, first click on one of them.
- Click on the other pin
> Note: You cannot connect 2 pins of the same type.
## Deleting a wire
- To delete a wire, click on it
## Opening the command palette
- To open the command palette, press ctrl + shift + p
## Creating a simulation
- To create a simulation, click the first button from the top of the sidebar, then type the desired name.
## Saving a simulation
- To save a simulation, follow one of the following actions:
1. Press ctrl + s
2. Open the command palette and type save, then press enter
3. Click on the 'simulation' button, then click 'save'
## Opening a simulation
- To open a simulation, click 'open simulation', then click the name of the simulation
## Deleting a simulation
- To delete a simulation, click 'open simulation', and then click the 'delete' icon on the row of your desired simulation.
## Rewind to the latest save (undo)
- To rewind to the latest save, follow one of the following actions:
1. Press ctrl + z
2. Click 'simulation' and then click 'undo'
## Downloading a simulation
- To download a simulation, follow one of the following actions:
1. Click 'simulation' and then type 'download'
2. Open the command palette, type 'download' and then press enter
> Note: You can also type 'download --save' or 'download -s' in the command palette to also save the simulation before downloading it
## Deleting a simulation
- To delete a simulation, press 'simulation' and then press 'delete'
- Press 'yes'
## Refreshing the enviroment
- To refresh the enviroment (reload all components), follow one of the following actions:
1. Click 'simulation' and then click 'refresh'
2. Press sfhit + delete
> Note: this won't refresh the whole window. To refresh the whole window, use the ui built in your browser
> Note 2: this can be useful if you just edited a custom logic gate and you want to see the changes without refreshing the whole window
## Clearing a simulation
> Note: cleaning = deleting all logic gates wich are not connected to anything
- To clear a simulation follow one of the following actions:
1. Click 'simulation'and then click 'clean'
2. Press shift + delete

13
docs/import.md Normal file
View file

@ -0,0 +1,13 @@
# importing a logic gate
## Opening the import palette
- To open the import palette, follow one of the following actions:
1. Press ctrl + g
2. Press 'custom gates' and then press 'import new gate'
## Importing a logic gate
- Open the import palette
- Type a valid command (see **[the url parser](./url.md)**)

10
docs/main.md Normal file
View file

@ -0,0 +1,10 @@
# Welcome
Hello, and welcome to my logic gate simulator! I know it can look kinda scary at first, so that's why i wrote these docs!
I recomand reading the first 3 chapters before you start, and then only dig deeper into the others when you feel you mastered the basics!
# Table of contents
1. [Basic controls](./controls.md)
2. [Importing a custom logic gate](import.md)

13
docs/url.md Normal file
View file

@ -0,0 +1,13 @@
# The url parser
If the first word is 'gist', the parser will automatcally try to fetch the github gist with the id equl to the second word:
**_Eg_**:
```
gist 8886faa6f99a7d2667ea8aa2f81ace04
```
![example of a gist id](./assets/gist_url.png)
Else, the parser will just try to fetch directly from the full string

41
package-lock.json generated
View file

@ -2891,7 +2891,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -2912,12 +2913,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -2932,17 +2935,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -3059,7 +3065,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -3071,6 +3078,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -3085,6 +3093,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -3092,12 +3101,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -3116,6 +3127,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -3196,7 +3208,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -3208,6 +3221,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -3293,7 +3307,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -3329,6 +3344,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -3348,6 +3364,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -3391,12 +3408,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },

View file

@ -1,146 +1,154 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<title>Logic gate simulator</title>
<head> <meta
<title>Game</title> name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0"> <style>
html,
body {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
<style> body {
html, background: #222;
body { display: flex;
padding: 0; justify-content: center;
margin: 0; align-items: center;
height: 100%; flex-direction: column;
width: 100%; }
}
body { .sk-folding-cube {
background: #222; margin: 20px auto;
display: flex; width: 40px;
justify-content: center; height: 40px;
align-items: center; position: relative;
flex-direction: column; -webkit-transform: rotateZ(45deg);
} transform: rotateZ(45deg);
}
.sk-folding-cube { .sk-folding-cube .sk-cube {
margin: 20px auto; float: left;
width: 40px; width: 50%;
height: 40px; height: 50%;
position: relative; position: relative;
-webkit-transform: rotateZ(45deg); -webkit-transform: scale(1.1);
transform: rotateZ(45deg); -ms-transform: scale(1.1);
} transform: scale(1.1);
}
.sk-folding-cube .sk-cube { .sk-folding-cube .sk-cube:before {
float: left; content: '';
width: 50%; position: absolute;
height: 50%; top: 0;
position: relative; left: 0;
-webkit-transform: scale(1.1); width: 100%;
-ms-transform: scale(1.1); height: 100%;
transform: scale(1.1); background-color: #bbb;
} -webkit-animation: sk-foldCubeAngle 2.4s infinite linear both;
animation: sk-foldCubeAngle 2.4s infinite linear both;
-webkit-transform-origin: 100% 100%;
-ms-transform-origin: 100% 100%;
transform-origin: 100% 100%;
}
.sk-folding-cube .sk-cube:before { .sk-folding-cube .sk-cube2 {
content: ''; -webkit-transform: scale(1.1) rotateZ(90deg);
position: absolute; transform: scale(1.1) rotateZ(90deg);
top: 0; }
left: 0;
width: 100%;
height: 100%;
background-color: #bbb;
-webkit-animation: sk-foldCubeAngle 2.4s infinite linear both;
animation: sk-foldCubeAngle 2.4s infinite linear both;
-webkit-transform-origin: 100% 100%;
-ms-transform-origin: 100% 100%;
transform-origin: 100% 100%;
}
.sk-folding-cube .sk-cube2 { .sk-folding-cube .sk-cube3 {
-webkit-transform: scale(1.1) rotateZ(90deg); -webkit-transform: scale(1.1) rotateZ(180deg);
transform: scale(1.1) rotateZ(90deg); transform: scale(1.1) rotateZ(180deg);
} }
.sk-folding-cube .sk-cube3 { .sk-folding-cube .sk-cube4 {
-webkit-transform: scale(1.1) rotateZ(180deg); -webkit-transform: scale(1.1) rotateZ(270deg);
transform: scale(1.1) rotateZ(180deg); transform: scale(1.1) rotateZ(270deg);
} }
.sk-folding-cube .sk-cube4 { .sk-folding-cube .sk-cube2:before {
-webkit-transform: scale(1.1) rotateZ(270deg); -webkit-animation-delay: 0.3s;
transform: scale(1.1) rotateZ(270deg); animation-delay: 0.3s;
} }
.sk-folding-cube .sk-cube2:before { .sk-folding-cube .sk-cube3:before {
-webkit-animation-delay: 0.3s; -webkit-animation-delay: 0.6s;
animation-delay: 0.3s; animation-delay: 0.6s;
} }
.sk-folding-cube .sk-cube3:before { .sk-folding-cube .sk-cube4:before {
-webkit-animation-delay: 0.6s; -webkit-animation-delay: 0.9s;
animation-delay: 0.6s; animation-delay: 0.9s;
} }
.sk-folding-cube .sk-cube4:before { @-webkit-keyframes sk-foldCubeAngle {
-webkit-animation-delay: 0.9s; 0%,
animation-delay: 0.9s; 10% {
} -webkit-transform: perspective(140px) rotateX(-180deg);
transform: perspective(140px) rotateX(-180deg);
opacity: 0;
}
25%,
75% {
-webkit-transform: perspective(140px) rotateX(0deg);
transform: perspective(140px) rotateX(0deg);
opacity: 1;
}
90%,
100% {
-webkit-transform: perspective(140px) rotateY(180deg);
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}
@-webkit-keyframes sk-foldCubeAngle { @keyframes sk-foldCubeAngle {
0%, 0%,
10% { 10% {
-webkit-transform: perspective(140px) rotateX(-180deg); -webkit-transform: perspective(140px) rotateX(-180deg);
transform: perspective(140px) rotateX(-180deg); transform: perspective(140px) rotateX(-180deg);
opacity: 0; opacity: 0;
} }
25%, 25%,
75% { 75% {
-webkit-transform: perspective(140px) rotateX(0deg); -webkit-transform: perspective(140px) rotateX(0deg);
transform: perspective(140px) rotateX(0deg); transform: perspective(140px) rotateX(0deg);
opacity: 1; opacity: 1;
} }
90%, 90%,
100% { 100% {
-webkit-transform: perspective(140px) rotateY(180deg); -webkit-transform: perspective(140px) rotateY(180deg);
transform: perspective(140px) rotateY(180deg); transform: perspective(140px) rotateY(180deg);
opacity: 0; opacity: 0;
} }
} }
</style>
@keyframes sk-foldCubeAngle { <link
0%, rel="stylesheet"
10% { href="https://fonts.googleapis.com/icon?family=Material+Icons"
-webkit-transform: perspective(140px) rotateX(-180deg); />
transform: perspective(140px) rotateX(-180deg); <link rel="stylesheet" href="style.css" />
opacity: 0; </head>
}
25%,
75% {
-webkit-transform: perspective(140px) rotateX(0deg);
transform: perspective(140px) rotateX(0deg);
opacity: 1;
}
90%,
100% {
-webkit-transform: perspective(140px) rotateY(180deg);
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}
</style>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel='stylesheet' href='style.css'>
</head>
<body ondragstart="return false;" ondrop="return false;" oncontextmenu="return false">
<div class="sk-folding-cube">
<div class="sk-cube1 sk-cube"></div>
<div class="sk-cube2 sk-cube"></div>
<div class="sk-cube4 sk-cube"></div>
<div class="sk-cube3 sk-cube"></div>
</div>
</body>
<body
ondragstart="return false;"
ondrop="return false;"
oncontextmenu="return false"
>
<div class="sk-folding-cube">
<div class="sk-cube1 sk-cube"></div>
<div class="sk-cube2 sk-cube"></div>
<div class="sk-cube4 sk-cube"></div>
<div class="sk-cube3 sk-cube"></div>
</div>
</body>
</html> </html>

View file

@ -1,8 +1,8 @@
import { Pin } from "../pin"; import { Pin } from '../pin'
export interface ComponentState { export interface ComponentState {
position: [number,number] position: [number, number]
scale: [number,number] scale: [number, number]
template: string template: string
id: number id: number
} }
@ -11,8 +11,8 @@ export interface activationContext {
inputs: Pin[] inputs: Pin[]
outputs: Pin[] outputs: Pin[]
succes: (mes: string) => any succes: (mes: string) => any
error: (mes:string) => any error: (mes: string) => any
color: (color:string) => void color: (color: string) => void
} }
export type materialMode = "standard_image" | "color" export type materialMode = 'standard_image' | 'color' | 'url'

View file

@ -1,6 +1,6 @@
import { svg, Part } from "lit-html"; import { svg, Part } from 'lit-html'
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from 'rxjs'
import { materialMode } from "./interfaces"; import { materialMode } from './interfaces'
declare function require<T>(path: string): T declare function require<T>(path: string): T
@ -10,24 +10,27 @@ export class Material {
private static images: { private static images: {
[key: string]: string [key: string]: string
} = { } = {
and: require("../../../assets/and_gate.jpg"), and: require('../../../assets/and_gate.jpg'),
or: require("../../../assets/or_gate.png"), or: require('../../../assets/or_gate.png'),
xor: require("../../../assets/xor_gate.png"), xor: require('../../../assets/xor_gate.png'),
nor: require("../../../assets/nor_gate.png") nor: require('../../../assets/nor_gate.png')
} }
public color = new BehaviorSubject<string>('rgba(0,0,0,0)')
public color = new BehaviorSubject<string>("rgba(0,0,0,0)") constructor(public mode: materialMode, public data: string) {
if (this.mode === 'color') this.color.next(data)
constructor(public mode: string, public name: materialMode | string) {
if (this.mode === "color")
this.color.next(name)
} }
innerHTML(x: partFactory, y: partFactory, w: partFactory, h: partFactory) { innerHTML(x: partFactory, y: partFactory, w: partFactory, h: partFactory) {
const src =
this.mode === 'standard_image'
? Material.images[this.data]
: this.data
return svg`<foreignobject x=${x} y=${y} width=${w} height=${h}> return svg`<foreignobject x=${x} y=${y} width=${w} height=${h}>
<div class="component-container"> <div class="component-container">
<img src=${Material.images[this.name]} height="97%" width="97%" draggable=false class="component"> <img src=${src} height="97%" width="97%" draggable=false class="component">
</div> </div>
</foreignobject>` </foreignobject>`
} }

View file

@ -0,0 +1,23 @@
import { fecthAsJson } from './fetchJson'
import { getFirstFileFromGist, getGist } from './getGist'
export const evalImport = async <T>(
command: string,
extension = 'json'
): Promise<T> => {
const words = command.split(' ')
let final: T
if (words.length === 1) {
if (extension === 'json') {
final = await fecthAsJson<T>(command)
} else {
final = ((await (await fetch(command)).text()) as unknown) as T
}
} else if (words[0] === 'gist') {
final = getFirstFileFromGist(await getGist(words[1]), extension)
}
return final
}

View file

@ -0,0 +1,6 @@
export const fecthAsJson = async <T>(url: string) => {
const res = await fetch(url)
const json = await res.json()
return json as T
}

View file

@ -0,0 +1,27 @@
import { fecthAsJson } from './fetchJson'
export interface Gist {
files: Record<string, { content: string }>
}
export const getGist = async (id: string) => {
const url = `https://api.github.com/gists/${id}`
const json = await fecthAsJson<Gist>(url)
return json
}
export const getFirstFileFromGist = (gist: Gist, extension = 'json') => {
const content =
gist.files[
Object.keys(gist.files).find(
name => name.indexOf(`.${extension}`) !== -1
)
].content
if (extension === 'json') {
return JSON.parse(content)
}
return content
}

View file

@ -0,0 +1,62 @@
import { ComponentTemplate } from '../componentManager/interfaces'
import { ComponentManager } from '../componentManager'
import { materialMode } from '../component/interfaces'
import { fecthAsJson } from './fetchJson'
import { getGist, getFirstFileFromGist } from './getGist'
import { evalImport } from './evalImport'
export interface Importable {
name: string
activation: string
onClick?: string
inputs: number
outputs: number
material: {
mode: materialMode
data: string
}
}
const defaults: Importable = {
activation: 'ctx.outputs[0].value = ctx.inputs[0].value',
inputs: 1,
outputs: 1,
material: {
mode: 'color',
data: 'red'
},
name: 'Imported component'
}
export async function parseActivation(activaton: string) {
const words = activaton.split(' ')
if (words[0] === 'url') {
return await evalImport<string>(words.slice(1).join(' '), 'js')
} else {
return activaton
}
}
export async function importComponent(
manager: ComponentManager,
command: string
): Promise<ComponentTemplate> {
const final: Importable = await evalImport(command)
const template: ComponentTemplate = {
...defaults,
...final,
editable: false,
version: '1.0.0',
imported: true,
importCommand: command
}
template.activation = await parseActivation(template.activation)
manager.templateStore.store.set(template.name, template)
manager.succes(`Succesfully imported component ${template.name}`)
return template
}

View file

@ -1,38 +1,38 @@
import { Singleton } from "@eix/utils"; import { Singleton } from '@eix/utils'
import { Component } from "../component"; import { Component } from '../component'
import { Subject, BehaviorSubject, fromEvent } from "rxjs"; import { Subject, BehaviorSubject, fromEvent } from 'rxjs'
import { svg, SVGTemplateResult, html } from "lit-html"; import { svg, SVGTemplateResult, html } from 'lit-html'
import { subscribe } from "lit-rx"; import { subscribe } from 'lit-rx'
import { Screen } from "../screen.ts"; import { Screen } from '../screen.ts'
import { ManagerState, ComponentTemplate } from "./interfaces"; import { ManagerState, ComponentTemplate } from './interfaces'
import { Store } from "../store"; import { Store } from '../store'
import { KeyboardInput } from "@eix/input" import { KeyboardInput } from '@eix/input'
import { success, error } from "toastr" import { success, error } from 'toastr'
import { ComponentTemplateStore } from "./componentTemplateStore"; import { ComponentTemplateStore } from './componentTemplateStore'
import { alertOptions } from "./alertOptions"; import { alertOptions } from './alertOptions'
import { WireManager } from "../wires"; import { WireManager } from '../wires'
import { runCounter } from "../component/runCounter"; import { runCounter } from '../component/runCounter'
import { Settings } from "../store/settings"; import { Settings } from '../store/settings'
import { download } from "./download"; import { download } from './download'
import { modal } from "../modals"; import { modal } from '../modals'
import { map } from "rxjs/operators"; import { map } from 'rxjs/operators'
import { persistent } from "../store/persistent"; import { persistent } from '../store/persistent'
import { MDCTextField } from '@material/textfield'; import { MDCTextField } from '@material/textfield'
import { importComponent } from '../componentImporter/importComponent'
const defaultName = "default"
const defaultName = 'default'
@Singleton @Singleton
export class ComponentManager { export class ComponentManager {
public components: Component[] = [] public components: Component[] = []
public svgs = new Subject<SVGTemplateResult>() public svgs = new Subject<SVGTemplateResult>()
public placeholder = new BehaviorSubject("Create simulation") public placeholder = new BehaviorSubject('Create simulation')
public barAlpha = new BehaviorSubject<string>("0"); public barAlpha = new BehaviorSubject<string>('0')
public wireManager = new WireManager() public wireManager = new WireManager()
public onTop: Component public onTop: Component
public templateStore = new ComponentTemplateStore() public templateStore = new ComponentTemplateStore()
private temporaryCommnad = "" private temporaryCommnad = ''
private clicked = false private clicked = false
private ignoreKeyDowns = false private ignoreKeyDowns = false
@ -42,66 +42,79 @@ export class ComponentManager {
offset: number offset: number
scale: [number, number] scale: [number, number]
} = { } = {
offset: 50, offset: 50,
scale: [100, 100] scale: [100, 100]
} }
private commandHistoryStore = new Store<string>("commandHistory") private commandHistoryStore = new Store<string>('commandHistory')
private store = new Store<ManagerState>("simulationStates") private store = new Store<ManagerState>('simulationStates')
private saveEvent = new KeyboardInput("s") private saveEvent = new KeyboardInput('s')
private createEvent = new KeyboardInput("m") private createEvent = new KeyboardInput('m')
private closeInputEvent = new KeyboardInput("enter") private closeInputEvent = new KeyboardInput('enter')
private ctrlEvent = new KeyboardInput("ctrl") private ctrlEvent = new KeyboardInput('ctrl')
private palleteEvent = new KeyboardInput("p") private palleteEvent = new KeyboardInput('p')
private undoEvent = new KeyboardInput("z") private undoEvent = new KeyboardInput('z')
private shiftEvent = new KeyboardInput("shift") private shiftEvent = new KeyboardInput('shift')
private refreshEvent = new KeyboardInput("r") private refreshEvent = new KeyboardInput('r')
private clearEvent = new KeyboardInput("delete") private gEvent = new KeyboardInput('g')
private upEvent = new KeyboardInput("up") private clearEvent = new KeyboardInput('delete')
private downEvent = new KeyboardInput("down") private upEvent = new KeyboardInput('up')
private downEvent = new KeyboardInput('down')
@persistent<ComponentManager, string>(defaultName, "main") @persistent<ComponentManager, string>(defaultName, 'main')
public name: string public name: string
public alertOptions = alertOptions public alertOptions = alertOptions
private commandHistory: string[] = [] private commandHistory: string[] = []
private commands: { private commands: {
[key: string]: (ctx: ComponentManager, args: string[], flags: string[]) => any [key: string]: (
ctx: ComponentManager,
args: string[],
flags: string[]
) => any
} = { } = {
clear(ctx: ComponentManager) { clear(ctx: ComponentManager) {
ctx.clear() ctx.clear()
}, },
save(ctx: ComponentManager) { save(ctx: ComponentManager) {
ctx.save() ctx.save()
}, },
ls(ctx: ComponentManager) { ls(ctx: ComponentManager) {
const data = ctx.store.ls() const data = ctx.store.ls()
const message = data.join("\n") const message = data.join('\n')
success(message, "", ctx.alertOptions) success(message, '', ctx.alertOptions)
}, },
help(ctx: ComponentManager) { help(ctx: ComponentManager) {
success(`Usage: &ltcommand> <br> success(
`Usage: &ltcommand> <br>
Where &ltcommand> is one of: Where &ltcommand> is one of:
<ul> <ul>
${Object.keys(ctx.commands).map(val => ` ${Object.keys(ctx.commands)
.map(
val => `
<li>${val}</li> <li>${val}</li>
`).join("")} `
)
.join('')}
</ul> </ul>
`, "", ctx.alertOptions) `,
}, '',
refresh(ctx: ComponentManager) { ctx.alertOptions
ctx.refresh() )
}, },
rewind(ctx:ComponentManager){ refresh(ctx: ComponentManager) {
localStorage.clear() ctx.refresh()
success("Succesfully cleared localStorage!","",ctx.alertOptions) },
}, rewind(ctx: ComponentManager) {
ctp: this.templateStore.commands.template, localStorage.clear()
settings: this.settings.commands, success('Succesfully cleared localStorage!', '', ctx.alertOptions)
download },
} ctp: this.templateStore.commands.template,
settings: this.settings.commands,
download
}
private inputMode: string private inputMode: string
public gates = this.templateStore.store.lsChanges public gates = this.templateStore.store.lsChanges
@ -110,24 +123,24 @@ export class ComponentManager {
public file: { public file: {
[key: string]: () => void [key: string]: () => void
} = { } = {
clear: () => this.clear(), clear: () => this.clear(),
clean: () => this.smartClear(), clean: () => this.smartClear(),
save: () => this.save(), save: () => this.save(),
undo: () => this.refresh(), undo: () => this.refresh(),
download: () => download(this, [], []), download: () => download(this, [], []),
delete: () => this.delete(this.name), delete: () => this.delete(this.name),
refresh: () => this.silentRefresh(true) refresh: () => this.silentRefresh(true)
} }
public shortcuts: { public shortcuts: {
[key: string]: string [key: string]: string
} = { } = {
clear: "shift delete", clear: 'shift delete',
clean: "delete", clean: 'delete',
save: "ctrl s", save: 'ctrl s',
undo: "ctrl z", undo: 'ctrl z',
refresh: "ctrl r" refresh: 'ctrl r'
} }
constructor() { constructor() {
runCounter.increase() runCounter.increase()
@ -136,22 +149,22 @@ export class ComponentManager {
this.refresh() this.refresh()
fromEvent(document.body, "keydown").subscribe((e: KeyboardEvent) => { fromEvent(document.body, 'keydown').subscribe((e: KeyboardEvent) => {
if (this.barAlpha.value == "1") { if (this.barAlpha.value == '1') {
const elem = document.getElementById("nameInput") const elem = document.getElementById('nameInput')
elem.focus() elem.focus()
} } else if (!this.ignoreKeyDowns) {
else if (!this.ignoreKeyDowns) {
e.preventDefault() e.preventDefault()
} }
}) })
fromEvent(document.body, "keyup").subscribe((e: MouseEvent) => { fromEvent(document.body, 'keyup').subscribe((e: MouseEvent) => {
if (this.barAlpha.value === "1") { if (this.barAlpha.value === '1') {
if (this.closeInputEvent.value) if (this.closeInputEvent.value) this.create()
this.create() else if (this.inputMode === 'command') {
else if (this.inputMode === "command") { const elem = <HTMLInputElement>(
const elem = <HTMLInputElement>document.getElementById("nameInput") document.getElementById('nameInput')
)
if (this.upEvent.value) { if (this.upEvent.value) {
document.body.focus() document.body.focus()
e.preventDefault() e.preventDefault()
@ -159,10 +172,12 @@ export class ComponentManager {
if (index) { if (index) {
//save drafts //save drafts
if (index === -1) if (index === -1) this.temporaryCommnad = elem.value
this.temporaryCommnad = elem.value
const newIndex = (index === -1) ? this.commandHistory.length - 1 : index - 1 const newIndex =
index === -1
? this.commandHistory.length - 1
: index - 1
elem.value = this.commandHistory[newIndex] elem.value = this.commandHistory[newIndex]
} }
} }
@ -173,36 +188,36 @@ export class ComponentManager {
if (index > -1) { if (index > -1) {
const maxIndex = this.commandHistory.length - 1 const maxIndex = this.commandHistory.length - 1
elem.value = (index === maxIndex) ? this.temporaryCommnad : this.commandHistory[index + 1] elem.value =
index === maxIndex
? this.temporaryCommnad
: this.commandHistory[index + 1]
} }
} }
} }
} } else {
else {
if (this.ctrlEvent.value) { if (this.ctrlEvent.value) {
if (this.createEvent.value) { if (this.createEvent.value) {
this.prepareNewSimulation() this.prepareNewSimulation()
} } else if (
else if (this.shiftEvent.value && this.palleteEvent.value) { this.shiftEvent.value &&
this.palleteEvent.value
) {
this.preInput() this.preInput()
this.inputMode = "command" this.inputMode = 'command'
this.placeholder.next("Command palette") this.placeholder.next('Command palette')
} } else if (this.gEvent.value) {
else if (this.saveEvent.value) { this.importGate()
} else if (this.saveEvent.value) {
this.save() this.save()
} } else if (this.undoEvent.value) {
else if (this.undoEvent.value) {
this.refresh() this.refresh()
} } else if (this.refreshEvent.value) {
else if (this.refreshEvent.value) {
this.silentRefresh(true) this.silentRefresh(true)
} }
} } else if (this.clearEvent.value) {
else if (this.clearEvent.value) { if (this.shiftEvent.value) this.clear()
if (this.shiftEvent.value) else this.smartClear()
this.clear()
else
this.smartClear()
} }
} }
}) })
@ -212,22 +227,20 @@ export class ComponentManager {
this.update() this.update()
// this.save() // this.save()
}) })
if (this.saves.value.length === 0) if (this.saves.value.length === 0) this.save()
this.save()
} }
private initEmptyGate(name: string) { private initEmptyGate(name: string) {
const obj: ComponentTemplate = { const obj: ComponentTemplate = {
inputs: 1, inputs: 1,
name, name,
version: "1.0.0", version: '1.0.0',
outputs: 1, outputs: 1,
activation: "", activation: '',
editable: true, editable: true,
material: { material: {
mode: "color", mode: 'color',
data: "blue" data: 'blue'
} }
} }
@ -238,44 +251,57 @@ export class ComponentManager {
public newGate() { public newGate() {
this.preInput() this.preInput()
this.inputMode = "gate" this.inputMode = 'gate'
this.placeholder.next("Gate name") this.placeholder.next('Gate name')
}
public importGate() {
this.preInput()
this.inputMode = 'importGate'
this.placeholder.next('Gate url')
} }
public prepareNewSimulation() { public prepareNewSimulation() {
this.preInput() this.preInput()
this.inputMode = "create" this.inputMode = 'create'
this.placeholder.next("Create simulation") this.placeholder.next('Create simulation')
} }
private preInput() { private preInput() {
const elem = <HTMLInputElement>document.getElementById("nameInput") const elem = <HTMLInputElement>document.getElementById('nameInput')
elem.value = "" elem.value = ''
this.barAlpha.next("1") this.barAlpha.next('1')
} }
private async create() { private async create() {
const elem = <HTMLInputElement>document.getElementById("nameInput") const elem = <HTMLInputElement>document.getElementById('nameInput')
this.barAlpha.next("0") this.barAlpha.next('0')
if (this.inputMode === "create") { if (this.inputMode === 'create') {
await this.createEmptySimulation(elem.value) await this.createEmptySimulation(elem.value)
success(`Succesfully created simulation ${elem.value}`, "", this.alertOptions) success(
`Succesfully created simulation ${elem.value}`,
'',
this.alertOptions
)
} else if (this.inputMode === 'command') this.eval(elem.value)
else if (this.inputMode === 'gate') this.initEmptyGate(elem.value)
else if (this.inputMode === 'importGate') {
importComponent(this, elem.value)
} }
}
else if (this.inputMode === "command") public succes(message: string) {
this.eval(elem.value) success(message, '', this.alertOptions)
else if (this.inputMode === "gate")
this.initEmptyGate(elem.value)
} }
private async handleDuplicateModal(name: string) { private async handleDuplicateModal(name: string) {
const result = await modal({ const result = await modal({
title: "Warning", title: 'Warning',
content: html`There was already a simulation called ${name}, content: html`
are you sure you want to override it? There was already a simulation called ${name}, are you sure you
All your work will be lost!` want to override it? All your work will be lost!
`
}) })
return result return result
@ -286,46 +312,80 @@ export class ComponentManager {
const gate = this.templateStore.store.get(name) const gate = this.templateStore.store.get(name)
modal({ modal({
no: "", no: '',
yes: "save", yes: 'save',
title: `Edit ${name}`, title: `Edit ${name}`,
content: html`${html` content: html`
<br> ${html`
<div class="mdc-text-field mdc-text-field--textarea"> <br />
<textarea id="codeArea" class="mdc-text-field__input js" rows="8" cols="40">${ <div class="mdc-text-field mdc-text-field--textarea">
gate.activation <textarea
}</textarea> id="codeArea"
<div class="mdc-notched-outline"> class="mdc-text-field__input js"
<div class="mdc-notched-outline__leading"></div> rows="8"
<div class="mdc-notched-outline__notch"> cols="40"
<label for="textarea" class="mdc-floating-label">Activation function</label> >
${gate.activation}</textarea
>
<div class="mdc-notched-outline">
<div class="mdc-notched-outline__leading"></div>
<div class="mdc-notched-outline__notch">
<label for="textarea" class="mdc-floating-label"
>Activation function</label
>
</div>
<div class="mdc-notched-outline__trailing"></div>
</div> </div>
<div class="mdc-notched-outline__trailing"></div>
</div> </div>
</div><br><br> <br /><br />
<div class="mdc-text-field" id="inputCount"> <div class="mdc-text-field" id="inputCount">
<input type="number" id="my-text-field" class="mdc-text-field__input inputCount-i" value=${gate.inputs}> <input
<label class="mdc-floating-label" for="my-text-field">Inputs</label> type="number"
<div class="mdc-line-ripple"></div> id="my-text-field"
</div><br><br> class="mdc-text-field__input inputCount-i"
<div class="mdc-text-field" id="outputCount"> value=${gate.inputs}
<input type="number" id="my-text-field" class="mdc-text-field__input outputCount-i" value=${gate.outputs}> />
<label class="mdc-floating-label" for="my-text-field">Outputs</label> <label class="mdc-floating-label" for="my-text-field"
<div class="mdc-line-ripple"></div> >Inputs</label
</div><br><br> >
<div class="mdc-text-field" id="color"> <div class="mdc-line-ripple"></div>
<input type="string" id="my-text-field" class="mdc-text-field__input color-i" value=${gate.material.data}> </div>
<label class="mdc-floating-label" for="my-text-field">Outputs</label> <br /><br />
<div class="mdc-line-ripple"></div> <div class="mdc-text-field" id="outputCount">
</div><br> <input
`}` type="number"
id="my-text-field"
class="mdc-text-field__input outputCount-i"
value=${gate.outputs}
/>
<label class="mdc-floating-label" for="my-text-field"
>Outputs</label
>
<div class="mdc-line-ripple"></div>
</div>
<br /><br />
<div class="mdc-text-field" id="color">
<input
type="string"
id="my-text-field"
class="mdc-text-field__input color-i"
value=${gate.material.data}
/>
<label class="mdc-floating-label" for="my-text-field"
>Color</label
>
<div class="mdc-line-ripple"></div>
</div>
<br />
`}
`
}).then(val => { }).then(val => {
this.ignoreKeyDowns = false this.ignoreKeyDowns = false
const elems: (HTMLInputElement | HTMLTextAreaElement)[] = [ const elems: (HTMLInputElement | HTMLTextAreaElement)[] = [
document.querySelector("#codeArea"), document.querySelector('#codeArea'),
document.querySelector(".inputCount-i"), document.querySelector('.inputCount-i'),
document.querySelector(".outputCount-i"), document.querySelector('.outputCount-i'),
document.querySelector(".color-i") document.querySelector('.color-i')
] ]
const data = elems.map(val => val.value) const data = elems.map(val => val.value)
@ -334,21 +394,25 @@ export class ComponentManager {
activation: data[0], activation: data[0],
inputs: Number(data[1]), inputs: Number(data[1]),
outputs: Number(data[2]), outputs: Number(data[2]),
material:{ material: {
mode: "color", mode: 'color',
data: data[3] data: data[3]
} }
}) })
}) })
new MDCTextField(document.querySelector('.mdc-text-field')); new MDCTextField(document.querySelector('.mdc-text-field'))
new MDCTextField(document.querySelector('#outputCount')); new MDCTextField(document.querySelector('#outputCount'))
new MDCTextField(document.querySelector('#inputCount')); new MDCTextField(document.querySelector('#inputCount'))
new MDCTextField(document.querySelector('#color')); new MDCTextField(document.querySelector('#color'))
} }
public add(template: string, position?: [number, number]) { public add(template: string, position?: [number, number]) {
const pos = position ? position : [...Array(2)].fill(this.standard.offset * this.components.length) as [number, number] const pos = position
? position
: ([...Array(2)].fill(
this.standard.offset * this.components.length
) as [number, number])
this.components.push(new Component(template, pos, this.standard.scale)) this.components.push(new Component(template, pos, this.standard.scale))
this.update() this.update()
@ -356,17 +420,20 @@ export class ComponentManager {
public async delete(name: string) { public async delete(name: string) {
const res = await modal({ const res = await modal({
title: "Are you sure?", title: 'Are you sure?',
content: html`Deleting a simulations is ireversible, and all work will be lost!` content: html`
Deleting a simulations is ireversible, and all work will be
lost!
`
}) })
if (res) { if (res) {
if (this.name === name) { if (this.name === name) {
if (this.saves.value.length > 1) { if (this.saves.value.length > 1) {
this.switchTo(this.saves.value.find(val => val !== name)) this.switchTo(this.saves.value.find(val => val !== name))
} } else {
else { let newName =
let newName = (name === defaultName) ? `${defaultName}(1)` : defaultName name === defaultName ? `${defaultName}(1)` : defaultName
await this.createEmptySimulation(newName) await this.createEmptySimulation(newName)
this.switchTo(newName) this.switchTo(newName)
} }
@ -384,15 +451,18 @@ export class ComponentManager {
scale: [1, 1] scale: [1, 1]
}) })
if (name !== this.name) if (name !== this.name) this.save()
this.save()
this.name = name this.name = name
this.refresh() this.refresh()
} }
return new Promise(async (res) => {//get wheater theres already a simulation with that name return new Promise(async res => {
if (this.store.get(name) && await this.handleDuplicateModal(name) || //get wheater theres already a simulation with that name
!this.store.get(name)) { if (
(this.store.get(name) &&
(await this.handleDuplicateModal(name))) ||
!this.store.get(name)
) {
create() create()
res(true) res(true)
} }
@ -402,38 +472,53 @@ export class ComponentManager {
public switchTo(name: string) { public switchTo(name: string) {
const data = this.store.get(name) const data = this.store.get(name)
if (!data) if (!data)
error(`An error occured when trying to load ${name}`, "", this.alertOptions) error(
`An error occured when trying to load ${name}`,
'',
this.alertOptions
)
this.name = name this.name = name
this.refresh() this.refresh()
} }
eval(command: string) { eval(command: string) {
if (!this.commandHistory.includes(command)) // no duplicates if (!this.commandHistory.includes(command))
// no duplicates
this.commandHistory.push(command) this.commandHistory.push(command)
while (this.commandHistory.length > 10) // max of 10 elements while (
this.commandHistory.length > 10 // max of 10 elements
)
this.commandHistory.shift() this.commandHistory.shift()
const words = command.split(" ") const words = command.split(' ')
if (words[0] in this.commands) { if (words[0] in this.commands) {
const remaining = words.slice(1) const remaining = words.slice(1)
const flags = remaining.filter(val => val[0] == "-") const flags = remaining.filter(val => val[0] == '-')
const args = remaining.filter(val => val[0] != "-") const args = remaining.filter(val => val[0] != '-')
this.commands[words[0]](this, args, flags) this.commands[words[0]](this, args, flags)
} } else
else error(
error(`Command ${words} doesn't exist. Run help to get a list of all commands.`, `Command ${words} doesn't exist. Run help to get a list of all commands.`,
"", this.alertOptions) '',
this.alertOptions
)
} }
public smartClear() { public smartClear() {
this.components = this.components.filter(({ id }) => this.components = this.components.filter(({ id }) =>
this.wireManager.wires.find(val => val.input.of.id == id || val.output.of.id == id) this.wireManager.wires.find(
val => val.input.of.id == id || val.output.of.id == id
)
) )
this.update() this.update()
success("Succesfully cleared all unconnected components", "", this.alertOptions) success(
'Succesfully cleared all unconnected components',
'',
this.alertOptions
)
} }
public clear() { public clear() {
@ -441,7 +526,7 @@ export class ComponentManager {
this.wireManager.dispose() this.wireManager.dispose()
this.update() this.update()
success("Succesfully cleared all components", "", this.alertOptions) success('Succesfully cleared all components', '', this.alertOptions)
} }
public refresh() { public refresh() {
@ -454,7 +539,11 @@ export class ComponentManager {
this.update() this.update()
success("Succesfully refreshed to the latest save", "", this.alertOptions) success(
'Succesfully refreshed to the latest save',
'',
this.alertOptions
)
} }
update() { update() {
@ -488,12 +577,10 @@ export class ComponentManager {
// if (false) { } // if (false) { }
if (toAddOnTop >= 0) { if (toAddOnTop >= 0) {
this.top(this.components[toAddOnTop]) this.top(this.components[toAddOnTop])
} } else if (outsideComponents && this.clicked) {
else if (outsideComponents && this.clicked) {
const mousePosition = [e.clientX, e.clientY] const mousePosition = [e.clientX, e.clientY]
const delta = mousePosition.map((value, index) => const delta = mousePosition.map(
this.screen.mousePosition[index] - value (value, index) => this.screen.mousePosition[index] - value
) as [number, number] ) as [number, number]
this.screen.move(...delta) this.screen.move(...delta)
} }
@ -503,7 +590,11 @@ export class ComponentManager {
public silentRefresh(verboose = false) { public silentRefresh(verboose = false) {
this.loadState(this.state) this.loadState(this.state)
if (verboose) if (verboose)
success("Succesfully reloaded all components", "", this.alertOptions) success(
'Succesfully reloaded all components',
'',
this.alertOptions
)
} }
public top(component: Component) { public top(component: Component) {
@ -523,57 +614,67 @@ export class ComponentManager {
toRemoveDuplicatesFor = component toRemoveDuplicatesFor = component
} }
const stroke = subscribe(component.clickedChanges.pipe(map( const stroke = subscribe(
val => val ? "yellow" : "black" component.clickedChanges.pipe(
))) map(val => (val ? 'yellow' : 'black'))
)
)
return svg` return svg`
<g> <g>
${component.pinsSvg(10, 20)} ${component.pinsSvg(10, 20)}
${component.pinsSvg(10, 20, "output")} ${component.pinsSvg(10, 20, 'output')}
<g @mousedown=${ (e: MouseEvent) => component.handleClick(e)} <g @mousedown=${(e: MouseEvent) => component.handleClick(e)}
@touchstart=${(e: MouseEvent) => component.handleClick(e)} @touchstart=${(e: MouseEvent) => component.handleClick(e)}
@mouseup=${mouseupHandler} @mouseup=${mouseupHandler}
@touchend=${mouseupHandler}> @touchend=${mouseupHandler}>
<rect width=${ subscribe(component.width)} <rect width=${subscribe(component.width)}
height=${ subscribe(component.height)} height=${subscribe(component.height)}
x=${ subscribe(component.x)} x=${subscribe(component.x)}
y=${ subscribe(component.y)} y=${subscribe(component.y)}
stroke=${stroke} stroke=${stroke}
fill=${(component.material.mode === "standard_image") ? fill=${
"rgba(0,0,0,0)" : component.material.mode !== 'color'
subscribe(component.material.color)} ? 'rgba(0,0,0,0)'
: subscribe(component.material.color)
}
rx=20 rx=20
ry=20> ry=20>
</rect> </rect>
${(component.material.mode === "standard_image") ? component.material.innerHTML( ${
subscribe(component.x), component.material.mode !== 'color'
subscribe(component.y), ? component.material.innerHTML(
subscribe(component.width), subscribe(component.x),
subscribe(component.height)) : ""} subscribe(component.y),
subscribe(component.width),
subscribe(component.height)
)
: ''
}
</g> </g>
</g> </g>
`}); `
})
if (toRemoveDuplicatesFor) if (toRemoveDuplicatesFor) this.removeDuplicates(toRemoveDuplicatesFor)
this.removeDuplicates(toRemoveDuplicatesFor)
return svg`${this.wireManager.svg} ${result}` return svg`${this.wireManager.svg} ${result}`
} }
private removeDuplicates(component: Component) { private removeDuplicates(component: Component) {
let instances = this.components let instances = this.components
.map((value, index) => (value == component) ? index : null) .map((value, index) => (value == component ? index : null))
.filter(value => value) .filter(value => value)
instances.pop() instances.pop()
this.components = this.components this.components = this.components.filter(
.filter((_, index) => instances.indexOf(index) != -1) (_, index) => instances.indexOf(index) != -1
)
} }
get state(): ManagerState { get state(): ManagerState {
const components = Array.from((new Set(this.components)).values()) const components = Array.from(new Set(this.components).values())
return { return {
components: components.map(value => value.state), components: components.map(value => value.state),
position: this.screen.position as [number, number], position: this.screen.position as [number, number],
@ -591,17 +692,24 @@ export class ComponentManager {
} }
private loadState(state: ManagerState) { private loadState(state: ManagerState) {
if (!state.wires) //old state if (!state.wires)
//old state
return return
this.wireManager.dispose() this.wireManager.dispose()
this.clicked = false this.clicked = false
this.components = state.components.map(value => Component.fromState(value)) this.components = state.components.map(value =>
Component.fromState(value)
)
this.onTop = null this.onTop = null
state.wires.forEach(val => { state.wires.forEach(val => {
this.wireManager.start = this.getComponentById(val.from.owner).outputPins[val.from.index] this.wireManager.start = this.getComponentById(
this.wireManager.end = this.getComponentById(val.to.owner).inputPins[val.to.index] val.from.owner
).outputPins[val.from.index]
this.wireManager.end = this.getComponentById(
val.to.owner
).inputPins[val.to.index]
this.wireManager.tryResolving() this.wireManager.tryResolving()
}) })
@ -614,10 +722,14 @@ export class ComponentManager {
save() { save() {
for (let i = 0; i < this.commandHistory.length; i++) { for (let i = 0; i < this.commandHistory.length; i++) {
const element = this.commandHistory[i]; const element = this.commandHistory[i]
this.commandHistoryStore.set(i.toString(), element) this.commandHistoryStore.set(i.toString(), element)
} }
this.store.set(this.name, this.state) this.store.set(this.name, this.state)
success(`Saved the simulation ${this.name} succesfully!`, "", this.alertOptions) success(
`Saved the simulation ${this.name} succesfully!`,
'',
this.alertOptions
)
} }
} }

View file

@ -1,5 +1,5 @@
import { ComponentState, materialMode } from "../component/interfaces"; import { ComponentState, materialMode } from '../component/interfaces'
import { WireState } from "../wires/interface"; import { WireState } from '../wires/interface'
export interface ManagerState { export interface ManagerState {
components: ComponentState[] components: ComponentState[]
@ -20,4 +20,6 @@ export interface ComponentTemplate {
data: string data: string
} }
editable?: boolean editable?: boolean
imported?: boolean
importCommand?: string
} }

View file

@ -1,10 +1,11 @@
import { Singleton } from "@eix/utils"; import { Singleton } from '@eix/utils'
import { Pin } from "../pin"; import { Pin } from '../pin'
import { Wire } from "./wire"; import { Wire } from './wire'
import { svg } from "lit-html"; import { svg } from 'lit-html'
import { subscribe } from "lit-rx"; import { subscribe } from 'lit-rx'
import { Subject } from "rxjs"; import { Subject, combineLatest } from 'rxjs'
import { WireStateVal } from "./interface"; import { WireStateVal } from './interface'
import { merge, map } from 'rxjs/operators'
@Singleton @Singleton
export class WireManager { export class WireManager {
@ -15,20 +16,19 @@ export class WireManager {
public update = new Subject<boolean>() public update = new Subject<boolean>()
constructor() { } constructor() {}
public add(data: Pin) { public add(data: Pin) {
if (data.allowWrite) //output if (data.allowWrite)
//output
this.start = data this.start = data
else else this.end = data
this.end = data
this.tryResolving() this.tryResolving()
} }
public dispose() { public dispose() {
for (let i of this.wires) for (let i of this.wires) i.dispose()
i.dispose()
this.wires = [] this.wires = []
} }
@ -45,8 +45,7 @@ export class WireManager {
} }
private canBind(end: Pin) { private canBind(end: Pin) {
if (this.wires.find(val => val.output === end)) if (this.wires.find(val => val.output === end)) return false
return false
return true return true
} }
@ -57,35 +56,60 @@ export class WireManager {
} }
get svg() { get svg() {
return svg`${this.wires.map((val) => { return svg`${this.wires.map(val => {
const i = val.input.of const i = val.input.of
const o = val.output.of const o = val.output.of
const inputIndex = i.outputPins.indexOf(val.input) const inputIndex = i.outputPins.indexOf(val.input)
const input = i.piny(false, inputIndex) const inputY = i.piny(false, inputIndex)
const output = o.piny(true, o.inputPins.indexOf(val.output)) const outputY = o.piny(true, o.inputPins.indexOf(val.output))
const output = [o.pinx(true, 20), outputY]
const input = [i.pinx(false, 20), inputY]
const midX = combineLatest(output[0], input[0]).pipe(
map(values => {
return (values[0] + values[1]) / 2
})
)
const mid1 = [midX, outputY]
const mid2 = [midX, inputY]
const d = combineLatest<number[]>(
...output,
...mid1,
...mid2,
...input
).pipe(
map(
points =>
`M ${points.slice(0, 2).join(' ')} C ${points
.slice(2)
.join(' ')}`
)
)
return svg` return svg`
<line x2=${subscribe(i.pinx(false, 20))} <path d=${subscribe(d)}
x1=${subscribe(o.pinx(true, 20))}
y2=${subscribe(input)}
y1=${subscribe(output)}
stroke=${subscribe(val.input.svgColor)} stroke=${subscribe(val.input.svgColor)}
stroke-width=10 stroke-width=10
@click=${() => this.remove(val)} fill="rgba(0,0,0,0)"
> @click=${() => this.remove(val)} />
</line> `
`})}` })}`
} }
get state() { get state() {
return this.wires.map((val): WireStateVal => ({ return this.wires.map(
from: { (val): WireStateVal => ({
owner: val.input.of.id, from: {
index: val.input.of.outputPins.indexOf(val.input) owner: val.input.of.id,
}, index: val.input.of.outputPins.indexOf(val.input)
to: { },
owner: val.output.of.id, to: {
index: val.output.of.inputPins.indexOf(val.output) owner: val.output.of.id,
} index: val.output.of.inputPins.indexOf(val.output)
})) }
})
)
} }
} }

View file

@ -1,11 +1,12 @@
import { render, html } from "lit-html" import { render, html } from 'lit-html'
import { subscribe } from "lit-rx" import { subscribe } from 'lit-rx'
import { Screen } from "./common/screen.ts"; import { Screen } from './common/screen.ts'
import { ComponentManager } from "./common/componentManager"; import { ComponentManager } from './common/componentManager'
import { map } from "rxjs/operators"; import { map } from 'rxjs/operators'
import { MDCMenu } from '@material/menu'; import { MDCMenu } from '@material/menu'
import { error } from "toastr" import { error } from 'toastr'
import { modal } from "./common/modals"; import { modal } from './common/modals'
import { importComponent } from './common/componentImporter/importComponent'
const screen = new Screen() const screen = new Screen()
@ -13,71 +14,75 @@ export const manager = new ComponentManager()
manager.save() manager.save()
manager.update() manager.update()
window.onerror = (message: string, url: string, lineNumber: number): boolean => { window.onerror = (
error(message, "", { message: string,
url: string,
lineNumber: number
): boolean => {
error(message, '', {
...manager.alertOptions, ...manager.alertOptions,
onclick: () => modal({ onclick: () =>
no: "", modal({
yes: "close", no: '',
title: "Error", yes: 'close',
content: html` title: 'Error',
<table> content: html`
<tr> <table>
<td>Url:</td> <tr>
<td>${url}</td> <td>Url:</td>
</tr> <td>${url}</td>
<tr> </tr>
<td>Message:</td> <tr>
<td>${message}</td> <td>Message:</td>
</tr> <td>${message}</td>
<tr> </tr>
<td>Line:</td> <tr>
<td>${lineNumber}</td> <td>Line:</td>
</tr> <td>${lineNumber}</td>
</table> </tr>
` </table>
}) `
})
}) })
return true; return true
};
const handleEvent = <T>(e: T, func: (e: T) => any) => {
if (manager.barAlpha.value == "0")
func(e)
else if (manager.barAlpha.value == "1"
&& (e as unknown as MouseEvent).type == "mousedown"
&& (e as unknown as MouseEvent).target != document.getElementById("nameInput"))
manager.barAlpha.next("0")
} }
const moveHandler = (e: MouseEvent) => handleEvent(e, (e: MouseEvent) => { const handleEvent = <T>(e: T, func: (e: T) => any) => {
manager.handleMouseMove(e) if (manager.barAlpha.value == '0') func(e)
screen.updateMouse(e) else if (
}) manager.barAlpha.value == '1' &&
((e as unknown) as MouseEvent).type == 'mousedown' &&
((e as unknown) as MouseEvent).target !=
document.getElementById('nameInput')
)
manager.barAlpha.next('0')
}
render(html` const moveHandler = (e: MouseEvent) =>
handleEvent(e, (e: MouseEvent) => {
manager.handleMouseMove(e)
screen.updateMouse(e)
})
render(
html`
<div @mousemove=${moveHandler} <div @mousemove=${moveHandler}
@touchmove=${moveHandler} @touchmove=${moveHandler}
@mousedown=${(e: MouseEvent) => handleEvent(e, () => @mousedown=${(e: MouseEvent) =>
manager.handleMouseDown() handleEvent(e, () => manager.handleMouseDown())}
)} @touchdown=${(e: MouseEvent) =>
@touchdown=${(e: MouseEvent) => handleEvent(e, () => handleEvent(e, () => manager.handleMouseDown())}
manager.handleMouseDown() @mouseup=${(e: MouseEvent) =>
)} handleEvent(e, () => manager.handleMouseUp())}
@mouseup=${(e: MouseEvent) => handleEvent(e, () => @touchup=${(e: MouseEvent) =>
manager.handleMouseUp() handleEvent(e, () => manager.handleMouseUp())}
)} @wheel=${(e: MouseEvent) =>
@touchup=${(e: MouseEvent) => handleEvent(e, () => handleEvent(e, (e: WheelEvent) => screen.handleScroll(e))}>
manager.handleMouseUp()
)}
@wheel=${(e: MouseEvent) => handleEvent(e, (e: WheelEvent) =>
screen.handleScroll(e)
)}>
<div id=${subscribe(manager.barAlpha.pipe(map(val => <div id=${subscribe(
(val == "1") ? "shown" : "" manager.barAlpha.pipe(map(val => (val == '1' ? 'shown' : '')))
)))} )}
class=createBar> class=createBar>
<div class="topContainer"> <div class="topContainer">
<div> <div>
@ -87,41 +92,42 @@ render(html`
</div> </div>
</div> </div>
</div> </div>
<svg height=${ subscribe(screen.height)} <svg height=${subscribe(screen.height)}
width=${ subscribe(screen.width)} width=${subscribe(screen.width)}
viewBox=${subscribe(screen.viewBox)}> viewBox=${subscribe(screen.viewBox)}>
${ subscribe(manager.svgs)} ${subscribe(manager.svgs)}
</svg> </svg>
</div> </div>
<div class="ModalContainer"></div> <div class="ModalContainer"></div>
<aside class="mdc-drawer main-sidebar"> <aside class="mdc-drawer main-sidebar">
<div class="mdc-drawer__content"> <div class="mdc-drawer__content">
<nav class="mdc-list"> <nav class="mdc-list">
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page" @click=${() => manager.prepareNewSimulation()}> <a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page" @click=${() =>
manager.prepareNewSimulation()}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">note_add</i> <i class="material-icons mdc-list-item__graphic" aria-hidden="true">note_add</i>
<span class="mdc-list-item__text">Create new simulation</span> <span class="mdc-list-item__text">Create new simulation</span>
</a> </a>
<a class="mdc-list-item" href="#" id="openSimulation" @click=${() => { <a class="mdc-list-item" href="#" id="openSimulation" @click=${() => {
menus[0].open = true menus[0].open = true
}}> }}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">folder_open</i> <i class="material-icons mdc-list-item__graphic" aria-hidden="true">folder_open</i>
<span class="mdc-list-item__text">Open simulation</span> <span class="mdc-list-item__text">Open simulation</span>
</a> </a>
<a class="mdc-list-item" href="#" id="openFile" @click=${() => { <a class="mdc-list-item" href="#" id="openFile" @click=${() => {
menus[2].open = true menus[2].open = true
}}> }}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">insert_drive_file</i> <i class="material-icons mdc-list-item__graphic" aria-hidden="true">insert_drive_file</i>
<span class="mdc-list-item__text">Simulation</span> <span class="mdc-list-item__text">Simulation</span>
</a> </a>
<a class="mdc-list-item" href="#" id="openCustomGates" @click=${() => { <a class="mdc-list-item" href="#" id="openCustomGates" @click=${() => {
menus[3].open = true menus[3].open = true
}}> }}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">edit</i> <i class="material-icons mdc-list-item__graphic" aria-hidden="true">edit</i>
<span class="mdc-list-item__text">Custom gates</span> <span class="mdc-list-item__text">Custom gates</span>
</a> </a>
<a class="mdc-list-item" href="#" id="openGates" @click=${() => { <a class="mdc-list-item" href="#" id="openGates" @click=${() => {
menus[1].open = true menus[1].open = true
}}> }}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i> <i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i>
<span class="mdc-list-item__text">Add component</span> <span class="mdc-list-item__text">Add component</span>
</a> </a>
@ -132,61 +138,160 @@ render(html`
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="saveMenu"> <div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="saveMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1"> <ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${subscribe(manager.saves.pipe(map(_ => _.map(val => html` ${subscribe(
<li class= "mdc-list-item" role = "menuitem" @click=${() => manager.switchTo(val)}> manager.saves.pipe(
<span class="mdc-list-item__text"> ${val} </span> map(_ =>
<span class="material-icons mdc-list-item__meta" @click=${() => manager.delete(val)}> delete </span> _.map(
</li>` val => html`
))))} <li
class="mdc-list-item"
role="menuitem"
@click=${() => manager.switchTo(val)}
>
<span class="mdc-list-item__text">
${val}
</span>
<span
class="material-icons mdc-list-item__meta"
@click=${() => manager.delete(val)}
>
delete
</span>
</li>
`
)
)
)
)}
</ul> </ul>
</div> </div>
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="gateMenu"> <div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="gateMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1"> <ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${subscribe(manager.gates.pipe(map(_ => [..._].sort().map(val => html` ${subscribe(
<li class= "mdc-list-item" role = "menuitem" @click=${() => manager.add(val)}> manager.gates.pipe(
<span class="mdc-list-item__text"> ${val} </span> map(gates =>
${(manager.templateStore.store.get(val).editable ? [...gates].sort().map(name => {
html`&nbsp &nbsp &nbsp <span class="material-icons mdc-list-item__meta" @click=${ const gate = manager.templateStore.store.get(name)
() => manager.templateStore.store.delete(val) return html`
}> delete </span>` : <li
"" class="mdc-list-item"
)} role="menuitem"
</li>` @click=${() => manager.add(name)}
))))} >
<span class="mdc-list-item__text">
${name}
</span>
${gate.imported || gate.editable
? html`
&nbsp &nbsp &nbsp
<span
class="material-icons mdc-list-item__meta"
@click=${(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
manager.templateStore.store.delete(
name
)
}}
>
delete
</span>
`
: ''}
</li>
`
})
)
)
)}
</ul> </ul>
</div> </div>
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="fileMenu"> <div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="fileMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1"> <ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${[...Object.keys(manager.file)].sort().map(key => html` ${[...Object.keys(manager.file)].sort().map(
<li class= "mdc-list-item" role = "menuitem" @click=${() => manager.file[key]()}> key => html`
<span class="mdc-list-item__text">${key}</span> <li
${manager.shortcuts[key] ? html` class="mdc-list-item"
<span class="mdc-list-item__meta">&nbsp &nbsp &nbsp ${manager.shortcuts[key]}</span> role="menuitem"
` : ""} @click=${() => manager.file[key]()}
</li>` >
)} <span class="mdc-list-item__text">${key}</span>
${manager.shortcuts[key]
? html`
<span class="mdc-list-item__meta"
>&nbsp &nbsp &nbsp
${manager.shortcuts[key]}</span
>
`
: ''}
</li>
`
)}
</ul> </ul>
</div> </div>
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="customGateMenu"> <div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="customGateMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1"> <ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${subscribe(manager.gates.pipe(map(_ => _ ${subscribe(
.filter(val => manager.templateStore.store.get(val).editable) manager.gates.pipe(
.map(val => html` map(gates =>
<li class= "mdc-list-item" role = "menuitem" @click=${() => manager.edit(val)}> gates
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">edit</i> .map(name => manager.templateStore.store.get(name))
<span class="mdc-list-item__text"> ${val} </span> .filter(gate => gate.editable || gate.imported)
</li>` .map(
))))} gate => html`
<li class= "mdc-list-item" role = "menuitem" @click=${() => manager.newGate()}> <li
class="mdc-list-item"
role="menuitem"
@click=${(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (gate.editable) {
manager.edit(gate.name)
} else {
importComponent(
manager,
gate.importCommand
)
}
}}
>
<i
class="material-icons mdc-list-item__graphic"
aria-hidden="true"
>${gate.imported
? 'refresh'
: 'edit'}</i
>
<span class="mdc-list-item__text">
${gate.name}
</span>
</li>
`
)
)
)
)}
<li class= "mdc-list-item" role = "menuitem" @click=${() =>
manager.newGate()}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i> <i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i>
<span class="mdc-list-item__text"> New custom gate </span> <span class="mdc-list-item__text"> New custom gate </span>
</li> </li>
<li class= "mdc-list-item" role = "menuitem" @click=${() =>
manager.importGate()}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i>
<span class="mdc-list-item__text"> Import new gate </span>
<span class="mdc-list-item__meta">&nbsp &nbsp &nbsp ctrl + g</span>
</li>
</ul> </ul>
</div> </div>
`, document.body) `,
document.body
)
const menus = [ const menus = [
new MDCMenu(document.querySelector('#saveMenu')), new MDCMenu(document.querySelector('#saveMenu')),
@ -196,8 +301,8 @@ const menus = [
] ]
menus.forEach(menu => menu.hoistMenuToBody()) menus.forEach(menu => menu.hoistMenuToBody())
menus[0].setAnchorElement(document.querySelector(`#openSimulation`)) menus[0].setAnchorElement(document.querySelector(`#openSimulation`))
menus[1].setAnchorElement(document.querySelector("#openGates")) menus[1].setAnchorElement(document.querySelector('#openGates'))
menus[2].setAnchorElement(document.querySelector("#openFile")) menus[2].setAnchorElement(document.querySelector('#openFile'))
menus[3].setAnchorElement(document.querySelector("#openCustomGates")) menus[3].setAnchorElement(document.querySelector('#openCustomGates'))
manager.update() manager.update()