before removing smooth shadows
This commit is contained in:
parent
b28eec6342
commit
9eba227ec3
|
@ -1,2 +0,0 @@
|
||||||
Pentru a rula aplicatia, deschideti dist/index.html.
|
|
||||||
Codul sursa se afla in folderul src.
|
|
17
babel.config.js
Normal file
17
babel.config.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@babel/preset-env',
|
||||||
|
'@babel/preset-react',
|
||||||
|
'@babel/preset-typescript'
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@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' } }]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
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 { 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 mFlag = (args.indexOf('--message') + 1 || args.indexOf('-m') + 1) - 1
|
const mFlag = (args.indexOf('--message') + 1 || args.indexOf('-m') + 1) - 1
|
||||||
const message = `${mFlag >= 0 ? args[mFlag + 1] : 'automated update'}`
|
const message = `${mFlag >= 0 ? args[mFlag + 1] : 'automated update'}`
|
||||||
|
@ -23,7 +21,6 @@ const run = (command: string): Promise<string> => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
if (!args.includes('--skipBuild') && !args.includes('-sb'))
|
if (!args.includes('--skipBuild') && !args.includes('-sb'))
|
||||||
|
|
3818
package-lock.json
generated
3818
package-lock.json
generated
File diff suppressed because it is too large
Load diff
80
package.json
80
package.json
|
@ -1,66 +1,50 @@
|
||||||
{
|
{
|
||||||
"name": "html5-game-template",
|
"name": "logic-gate-simulator",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A template for writing jam games in HTML5 + TypeScript",
|
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack-dev-server --open --mode development",
|
"dev": "webpack-dev-server --open --mode development",
|
||||||
"build": "webpack --mode production",
|
"build": "cross-env NODE_ENV=production webpack",
|
||||||
"deploy": "ts-node deploy"
|
"deploy": "ts-node deploy"
|
||||||
},
|
},
|
||||||
"sideEffects": [
|
|
||||||
"*.scss",
|
|
||||||
"./src/ts/main.ts"
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/neverix/html5-game-template.git"
|
|
||||||
},
|
|
||||||
"author": "neverix",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/neverix/html5-game-template/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/neverix/html5-game-template#readme",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/file-saver": "^2.0.1",
|
"@babel/core": "^7.5.0",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
|
"@babel/preset-env": "^7.5.0",
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"@babel/preset-typescript": "^7.3.3",
|
||||||
|
"@types/deepmerge": "^2.2.0",
|
||||||
"@types/gh-pages": "^2.0.0",
|
"@types/gh-pages": "^2.0.0",
|
||||||
"@types/micromodal": "^0.3.0",
|
"@types/mainloop.js": "^1.0.5",
|
||||||
"@types/toastr": "^2.1.37",
|
"@types/react-router-dom": "^4.3.4",
|
||||||
"css-loader": "^2.1.0",
|
"babel-loader": "^8.0.6",
|
||||||
"extract-loader": "^3.1.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
"babel-regenerator-runtime": "^6.5.0",
|
||||||
"file-loader": "^3.0.1",
|
"css-loader": "^3.0.0",
|
||||||
"gh-pages": "^2.0.1",
|
"html-webpack-inline-source-plugin": "0.0.10",
|
||||||
"html-loader": "^0.5.5",
|
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"node-sass": "^4.11.0",
|
"mini-css-extract-plugin": "^0.7.0",
|
||||||
"random-emoji": "^1.0.2",
|
"node-sass": "^4.12.0",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"source-map-loader": "^0.2.4",
|
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"terser-webpack-plugin": "^1.3.0",
|
|
||||||
"ts-loader": "^5.3.3",
|
|
||||||
"ts-node": "^8.2.0",
|
"ts-node": "^8.2.0",
|
||||||
"typescript": "^3.3.3333",
|
"typescript": "^3.5.2",
|
||||||
"webpack": "^4.29.5",
|
"webpack": "^4.35.2",
|
||||||
"webpack-cli": "^3.2.3",
|
"webpack-cli": "^3.3.5",
|
||||||
"webpack-dev-server": "^3.2.0"
|
"webpack-dev-server": "^3.7.2",
|
||||||
|
"webpack-merge": "^4.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eix/input": "git+https://github.com/eix-js/input.git",
|
"@eix-js/utils": "0.0.6",
|
||||||
"@eix/utils": "git+https://github.com/eix-js/utils.git",
|
"deepmerge": "^4.0.0",
|
||||||
"@material/button": "^2.3.0",
|
"mainloop.js": "^1.0.4",
|
||||||
"@material/dialog": "^2.3.0",
|
"react": "^16.8.6",
|
||||||
"@material/drawer": "^2.3.0",
|
"react-dom": "^16.8.6",
|
||||||
"@material/list": "^2.3.0",
|
"react-router-dom": "^5.0.1",
|
||||||
"@material/menu": "^2.3.0",
|
|
||||||
"@material/textfield": "^2.3.0",
|
|
||||||
"@material/theme": "^1.1.0",
|
|
||||||
"file-saver": "^2.0.2",
|
|
||||||
"lit-html": "^1.0.0",
|
|
||||||
"lit-rx": "0.0.2",
|
|
||||||
"rxjs": "^6.5.2",
|
"rxjs": "^6.5.2",
|
||||||
"toastr": "^2.1.4"
|
"rxjs-hooks": "^0.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
137
src/index.html
137
src/index.html
|
@ -7,136 +7,6 @@
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0"
|
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%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #222;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube {
|
|
||||||
margin: 20px auto;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
position: relative;
|
|
||||||
-webkit-transform: rotateZ(45deg);
|
|
||||||
transform: rotateZ(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube .sk-cube {
|
|
||||||
float: left;
|
|
||||||
width: 50%;
|
|
||||||
height: 50%;
|
|
||||||
position: relative;
|
|
||||||
-webkit-transform: scale(1.1);
|
|
||||||
-ms-transform: scale(1.1);
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube .sk-cube:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
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 {
|
|
||||||
-webkit-transform: scale(1.1) rotateZ(90deg);
|
|
||||||
transform: scale(1.1) rotateZ(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube .sk-cube3 {
|
|
||||||
-webkit-transform: scale(1.1) rotateZ(180deg);
|
|
||||||
transform: scale(1.1) rotateZ(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube .sk-cube4 {
|
|
||||||
-webkit-transform: scale(1.1) rotateZ(270deg);
|
|
||||||
transform: scale(1.1) rotateZ(270deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube .sk-cube2:before {
|
|
||||||
-webkit-animation-delay: 0.3s;
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube .sk-cube3:before {
|
|
||||||
-webkit-animation-delay: 0.6s;
|
|
||||||
animation-delay: 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sk-folding-cube .sk-cube4:before {
|
|
||||||
-webkit-animation-delay: 0.9s;
|
|
||||||
animation-delay: 0.9s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes sk-foldCubeAngle {
|
|
||||||
0%,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sk-foldCubeAngle {
|
|
||||||
0%,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" href="style.css" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
|
@ -144,11 +14,6 @@
|
||||||
ondrop="return false;"
|
ondrop="return false;"
|
||||||
oncontextmenu="return false"
|
oncontextmenu="return false"
|
||||||
>
|
>
|
||||||
<div class="sk-folding-cube">
|
<div id="app"></div>
|
||||||
<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>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
import "./ts/main"
|
|
||||||
import "./scss/base.scss"
|
|
6
src/main.tsx
Normal file
6
src/main.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import React from 'react'
|
||||||
|
import App from './modules/core/components/App'
|
||||||
|
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
|
||||||
|
render(<App />, document.getElementById('app'))
|
28
src/modules/core/classes/Screen.ts
Normal file
28
src/modules/core/classes/Screen.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Singleton } from '@eix-js/utils'
|
||||||
|
import { Observable, fromEvent, BehaviorSubject } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
export class Screen {
|
||||||
|
public width = new BehaviorSubject<number>(window.innerWidth)
|
||||||
|
public height = new BehaviorSubject<number>(window.innerHeight)
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
const resize = fromEvent(window, 'resize')
|
||||||
|
|
||||||
|
resize
|
||||||
|
.pipe(map(() => window.innerWidth))
|
||||||
|
.subscribe(val => this.width.next(val))
|
||||||
|
resize
|
||||||
|
.pipe(map(() => window.innerHeight))
|
||||||
|
.subscribe(val => this.height.next(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
public get x() {
|
||||||
|
return this.width.value
|
||||||
|
}
|
||||||
|
|
||||||
|
public get y() {
|
||||||
|
return this.height.value
|
||||||
|
}
|
||||||
|
}
|
7
src/modules/core/components/App.scss
Normal file
7
src/modules/core/components/App.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
10
src/modules/core/components/App.tsx
Normal file
10
src/modules/core/components/App.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react'
|
||||||
|
import '../styles/reset'
|
||||||
|
import './App.scss'
|
||||||
|
import Canvas from './Canvas'
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return <Canvas />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
56
src/modules/core/components/Canvas.tsx
Normal file
56
src/modules/core/components/Canvas.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import React, { Component, createRef, Ref, RefObject } from 'react'
|
||||||
|
import FluidCanvas, { MouseEventInfo } from './FluidCanvas'
|
||||||
|
import loop from 'mainloop.js'
|
||||||
|
import { Gate } from '../../simulation/classes/Gate'
|
||||||
|
import { SimulationRenderer } from '../../simulation/classes/SimulationRenderer'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
|
||||||
|
class Canvas extends Component {
|
||||||
|
private canvasRef: RefObject<HTMLCanvasElement> = createRef()
|
||||||
|
private renderingContext: CanvasRenderingContext2D | null
|
||||||
|
private renderer = new SimulationRenderer()
|
||||||
|
|
||||||
|
public constructor(props: {}) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
const foo = new Gate('blue')
|
||||||
|
const bar = new Gate('green')
|
||||||
|
|
||||||
|
foo.transform.position = [100, 100]
|
||||||
|
foo.transform.scale = [70, 70]
|
||||||
|
|
||||||
|
bar.transform.position = [200, 200]
|
||||||
|
bar.transform.scale = [70, 70]
|
||||||
|
|
||||||
|
this.renderer.simulation.push(foo, bar)
|
||||||
|
|
||||||
|
loop.setDraw(() => {
|
||||||
|
if (this.renderingContext)
|
||||||
|
this.renderer.render(this.renderingContext)
|
||||||
|
}).setUpdate(delta => this.renderer.update(delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
if (this.canvasRef.current)
|
||||||
|
this.renderingContext = this.canvasRef.current.getContext('2d')
|
||||||
|
|
||||||
|
loop.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
loop.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<FluidCanvas
|
||||||
|
ref={this.canvasRef}
|
||||||
|
mouseDownOuput={this.renderer.mouseDownOutput}
|
||||||
|
mouseUpOutput={this.renderer.mouseUpOutput}
|
||||||
|
mouseMoveOutput={this.renderer.mouseMoveOutput}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Canvas
|
53
src/modules/core/components/FluidCanvas.tsx
Normal file
53
src/modules/core/components/FluidCanvas.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { RefObject, forwardRef, MouseEvent } from 'react'
|
||||||
|
import { useObservable } from 'rxjs-hooks'
|
||||||
|
import { Screen } from '../classes/Screen'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
import { vector2 } from '../../simulation/classes/Transform'
|
||||||
|
|
||||||
|
const screen = new Screen()
|
||||||
|
|
||||||
|
export interface MouseEventInfo {
|
||||||
|
position: vector2
|
||||||
|
button: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FluidCanvasProps {
|
||||||
|
mouseDownOuput: Subject<MouseEventInfo>
|
||||||
|
mouseUpOutput: Subject<MouseEventInfo>
|
||||||
|
mouseMoveOutput: Subject<MouseEventInfo>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEventInfo = (
|
||||||
|
e: MouseEvent<HTMLCanvasElement>
|
||||||
|
): MouseEventInfo => {
|
||||||
|
return {
|
||||||
|
button: e.button,
|
||||||
|
position: [e.clientX, e.clientY]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mouseEventHandler = (output: Subject<MouseEventInfo>) => (
|
||||||
|
e: MouseEvent<HTMLCanvasElement>
|
||||||
|
) => {
|
||||||
|
output.next(getEventInfo(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
const FluidCanvas = forwardRef(
|
||||||
|
(props: FluidCanvasProps, ref: RefObject<HTMLCanvasElement>) => {
|
||||||
|
const width = useObservable(() => screen.width, 0)
|
||||||
|
const height = useObservable(() => screen.height, 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={ref}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
onMouseDown={mouseEventHandler(props.mouseDownOuput)}
|
||||||
|
onMouseUp={mouseEventHandler(props.mouseUpOutput)}
|
||||||
|
onMouseMove={mouseEventHandler(props.mouseMoveOutput)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default FluidCanvas
|
446
src/modules/core/styles/reset.scss
Normal file
446
src/modules/core/styles/reset.scss
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0-modified | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
applet,
|
||||||
|
object,
|
||||||
|
iframe,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make sure to set some focus styles for accessibility */
|
||||||
|
:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote:before,
|
||||||
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='search']::-webkit-search-cancel-button,
|
||||||
|
input[type='search']::-webkit-search-decoration,
|
||||||
|
input[type='search']::-webkit-search-results-button,
|
||||||
|
input[type='search']::-webkit-search-results-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='search'] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-box-sizing: content-box;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
vertical-align: top;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
video {
|
||||||
|
display: inline-block;
|
||||||
|
*display: inline;
|
||||||
|
*zoom: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
|
||||||
|
* Known issue: no IE 6 support.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
|
||||||
|
* `em` units.
|
||||||
|
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||||
|
* user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `outline` inconsistency between Chrome and other browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
outline: thin dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability when focused and also mouse hovered in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
|
||||||
|
* 2. Improve image quality when scaled in IE 7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
-ms-interpolation-mode: bicubic; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct margin displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define consistent border, margin, and padding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited in IE 6/7/8/9.
|
||||||
|
* 2. Correct text not wrapping in Firefox 3.
|
||||||
|
* 3. Correct alignment displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
padding: 0;
|
||||||
|
white-space: normal; /* 2 */
|
||||||
|
*margin-left: -7px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct font size not being inherited in all browsers.
|
||||||
|
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
|
||||||
|
* and Chrome.
|
||||||
|
* 3. Improve appearance and consistency in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
vertical-align: baseline; /* 3 */
|
||||||
|
*vertical-align: middle; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
|
||||||
|
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
|
||||||
|
* Known issue: inner spacing remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
*overflow: visible; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address box sizing set to content-box in IE 8/9.
|
||||||
|
* 2. Remove excess padding in IE 8/9.
|
||||||
|
* 3. Remove excess padding in IE 7.
|
||||||
|
* Known issue: excess padding remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
*height: 13px; /* 3 */
|
||||||
|
*width: 13px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||||
|
* (include `-moz` to future-proof).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type='search'] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
-webkit-box-sizing: content-box; /* 2 */
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
||||||
|
* on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type='search']::-webkit-search-cancel-button,
|
||||||
|
input[type='search']::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 3+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
|
||||||
|
* 2. Improve readability and alignment in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto; /* 1 */
|
||||||
|
vertical-align: top; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chromeframe {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
background: #ccc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
4
src/modules/core/types/MouseSubject.ts
Normal file
4
src/modules/core/types/MouseSubject.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
import { MouseEventInfo } from '../components/FluidCanvas'
|
||||||
|
|
||||||
|
export type MouseSubject = Subject<MouseEventInfo>
|
13
src/modules/simulation/classes/Camera.ts
Normal file
13
src/modules/simulation/classes/Camera.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { Transform, vector2 } from './Transform'
|
||||||
|
import { Screen } from '../../core/classes/Screen'
|
||||||
|
|
||||||
|
export class Camera {
|
||||||
|
private screen = new Screen()
|
||||||
|
public transform = new Transform([0, 0], [this.screen.x, this.screen.y])
|
||||||
|
|
||||||
|
public toWordPostition(position: vector2) {
|
||||||
|
return position.map(
|
||||||
|
(value, index) => value + this.transform.position[index]
|
||||||
|
) as vector2
|
||||||
|
}
|
||||||
|
}
|
10
src/modules/simulation/classes/Gate.ts
Normal file
10
src/modules/simulation/classes/Gate.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Transform, vector2 } from './Transform'
|
||||||
|
|
||||||
|
export class Gate {
|
||||||
|
public static lastId = 0
|
||||||
|
public transform = new Transform()
|
||||||
|
public id = Gate.lastId++
|
||||||
|
public shadow: vector2 = [0, 0]
|
||||||
|
|
||||||
|
public constructor(public color = 'blue') {}
|
||||||
|
}
|
70
src/modules/simulation/classes/GateStorage.ts
Normal file
70
src/modules/simulation/classes/GateStorage.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { LruCacheNode } from '@eix-js/utils'
|
||||||
|
import { Gate } from './Gate'
|
||||||
|
|
||||||
|
export type GateNode = LruCacheNode<Gate>
|
||||||
|
|
||||||
|
export class GateStorage {
|
||||||
|
private hashMap = new Map<number, GateNode>()
|
||||||
|
private head = new LruCacheNode<Gate>(0, null)
|
||||||
|
private tail = new LruCacheNode<Gate>(0, null)
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.head.next = this.tail
|
||||||
|
this.tail.previous = this.head
|
||||||
|
}
|
||||||
|
|
||||||
|
private delete(node: GateNode) {
|
||||||
|
node.previous.next = node.next
|
||||||
|
node.next.previous = node.previous
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(key: number, node: GateNode) {
|
||||||
|
this.hashMap.set(key, node)
|
||||||
|
this.addToHead(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
public addToHead(node: GateNode) {
|
||||||
|
node.next = this.head.next
|
||||||
|
node.next.previous = node
|
||||||
|
node.previous = this.head
|
||||||
|
this.head.next = node
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public moveOnTop(node: GateNode) {
|
||||||
|
this.delete(node).addToHead(node)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: number) {
|
||||||
|
const node = this.hashMap.get(key)
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
public first() {
|
||||||
|
const first = this.head.next
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
public *[Symbol.iterator](): Iterator<Gate> {
|
||||||
|
let last = this.tail
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (last.previous === this.head) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
if (last.previous.data) {
|
||||||
|
yield last.previous.data
|
||||||
|
}
|
||||||
|
|
||||||
|
last = last.previous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
src/modules/simulation/classes/MouseManager.ts
Normal file
86
src/modules/simulation/classes/MouseManager.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { Singleton } from '@eix-js/utils'
|
||||||
|
import { MouseSubject } from '../../core/types/MouseSubject'
|
||||||
|
import { clamp } from '../helpers/clamp'
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
export class MouseManager {
|
||||||
|
private history: number[] = []
|
||||||
|
private total = 0
|
||||||
|
private limit = 10
|
||||||
|
private lastPosition = 0
|
||||||
|
private lastDirection = 0
|
||||||
|
private minimumDifference = 10
|
||||||
|
|
||||||
|
private lastMove = performance.now()
|
||||||
|
private resetLimit = 500
|
||||||
|
|
||||||
|
// mouseMoveInput is optional because we want to be able to get
|
||||||
|
// the instance even if we don't have a subject
|
||||||
|
// (as long as we passed one the first time)
|
||||||
|
public constructor(public mouseMoveInput?: MouseSubject) {
|
||||||
|
if (this.mouseMoveInput) {
|
||||||
|
this.mouseMoveInput.subscribe(event => {
|
||||||
|
this.lastMove = performance.now()
|
||||||
|
|
||||||
|
const position = event.position[0]
|
||||||
|
const dx = position - this.lastPosition
|
||||||
|
|
||||||
|
if (Math.abs(dx) < this.minimumDifference) {
|
||||||
|
this.lastPosition = position
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dx === 0) {
|
||||||
|
this.lastDirection = 0
|
||||||
|
} else {
|
||||||
|
this.lastDirection = Math.abs(dx) / dx
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastPosition = event.position[0]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'You need to pass a MouseMoveInput the first time you instantiate this class!'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDirection() {
|
||||||
|
return clamp(-1, 1, this.total / this.history.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(maybeAgain = true) {
|
||||||
|
if (this.lastDirection === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.lastDirection !== 0 &&
|
||||||
|
performance.now() - this.lastMove > this.resetLimit
|
||||||
|
) {
|
||||||
|
this.lastDirection = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.history.push(this.lastDirection)
|
||||||
|
|
||||||
|
this.total += this.lastDirection
|
||||||
|
|
||||||
|
if (this.history.length > this.limit) {
|
||||||
|
const removed = this.history.shift()
|
||||||
|
|
||||||
|
if (removed !== undefined) {
|
||||||
|
this.total -= removed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(value?: number) {
|
||||||
|
if (value) {
|
||||||
|
this.lastPosition = value
|
||||||
|
}
|
||||||
|
this.history = []
|
||||||
|
this.total = 0
|
||||||
|
this.lastMove = performance.now()
|
||||||
|
this.lastDirection = 0
|
||||||
|
}
|
||||||
|
}
|
15
src/modules/simulation/classes/Simulation.ts
Normal file
15
src/modules/simulation/classes/Simulation.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Gate } from './Gate'
|
||||||
|
import { GateStorage } from './GateStorage'
|
||||||
|
import { LruCacheNode } from '@eix-js/utils'
|
||||||
|
|
||||||
|
export class Simulation {
|
||||||
|
public gates = new GateStorage()
|
||||||
|
|
||||||
|
public push(...gates: Gate[]) {
|
||||||
|
for (const gate of gates) {
|
||||||
|
const node = new LruCacheNode<Gate>(gate.id, gate)
|
||||||
|
|
||||||
|
this.gates.set(gate.id, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
201
src/modules/simulation/classes/SimulationRenderer.ts
Normal file
201
src/modules/simulation/classes/SimulationRenderer.ts
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
import { Camera } from './Camera'
|
||||||
|
import { Simulation } from './Simulation'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
import { MouseEventInfo } from '../../core/components/FluidCanvas'
|
||||||
|
import { pointInSquare } from '../helpers/pointInSquare'
|
||||||
|
import { vector2 } from './Transform'
|
||||||
|
import merge from 'deepmerge'
|
||||||
|
import { smoothStep } from '../../vector2/helpers/smoothStep'
|
||||||
|
import { renderGate } from '../helpers/renderGate'
|
||||||
|
import { renderGateShadow } from '../helpers/renderGateShadow'
|
||||||
|
import { Gate } from './Gate'
|
||||||
|
import { MouseManager } from './MouseManager'
|
||||||
|
import { Screen } from '../../core/classes/Screen'
|
||||||
|
import {
|
||||||
|
add,
|
||||||
|
invert,
|
||||||
|
ofLength,
|
||||||
|
length,
|
||||||
|
multiply
|
||||||
|
} from '../../vector2/helpers/basic'
|
||||||
|
|
||||||
|
export interface SimulationRendererOptions {
|
||||||
|
shadows: {
|
||||||
|
enabled: boolean
|
||||||
|
color: string
|
||||||
|
offset: number
|
||||||
|
speed: number
|
||||||
|
}
|
||||||
|
dnd: {
|
||||||
|
rotation: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultSimulationRendererOptions: SimulationRendererOptions = {
|
||||||
|
shadows: {
|
||||||
|
enabled: true,
|
||||||
|
color: 'rgba(0,0,0,0.3)',
|
||||||
|
offset: 15,
|
||||||
|
speed: 1
|
||||||
|
},
|
||||||
|
dnd: {
|
||||||
|
rotation: Math.PI / 12 // 7.5 degrees
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SimulationRenderer {
|
||||||
|
public camera = new Camera()
|
||||||
|
public mouseDownOutput = new Subject<MouseEventInfo>()
|
||||||
|
public mouseUpOutput = new Subject<MouseEventInfo>()
|
||||||
|
public mouseMoveOutput = new Subject<MouseEventInfo>()
|
||||||
|
|
||||||
|
public selectedGate: number | null = null
|
||||||
|
public selectOffset: vector2 = [0, 0]
|
||||||
|
public movedSelection = false
|
||||||
|
|
||||||
|
private options: SimulationRendererOptions
|
||||||
|
private mouseManager = new MouseManager(this.mouseMoveOutput)
|
||||||
|
private screen = new Screen()
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
options: Partial<SimulationRendererOptions> = {},
|
||||||
|
public simulation = new Simulation()
|
||||||
|
) {
|
||||||
|
this.options = merge(options, defaultSimulationRendererOptions)
|
||||||
|
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
this.mouseDownOutput.subscribe(event => {
|
||||||
|
const worldPosition = this.camera.toWordPostition(event.position)
|
||||||
|
const gates = Array.from(this.simulation.gates)
|
||||||
|
|
||||||
|
// We need to iterate from the last to the first
|
||||||
|
// because if we have 2 overlapping gates,
|
||||||
|
// we want to select the one on top
|
||||||
|
for (let index = gates.length - 1; index >= 0; index--) {
|
||||||
|
const { transform, id } = gates[index]
|
||||||
|
|
||||||
|
if (pointInSquare(worldPosition, transform)) {
|
||||||
|
this.mouseManager.clear(worldPosition[0])
|
||||||
|
|
||||||
|
this.movedSelection = false
|
||||||
|
|
||||||
|
this.selectedGate = id
|
||||||
|
this.selectOffset = worldPosition.map(
|
||||||
|
(position, index) =>
|
||||||
|
position - transform.position[index]
|
||||||
|
) as vector2
|
||||||
|
|
||||||
|
const gateNode = this.simulation.gates.get(id)
|
||||||
|
|
||||||
|
if (gateNode) {
|
||||||
|
return this.simulation.gates.moveOnTop(gateNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mouseUpOutput.subscribe(event => {
|
||||||
|
if (this.selectedGate !== null) {
|
||||||
|
const selected = this.getSelected()
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
selected.transform.rotation = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedGate = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mouseMoveOutput.subscribe(event => {
|
||||||
|
if (this.selectedGate !== null) {
|
||||||
|
const gate = this.getGateById(this.selectedGate)
|
||||||
|
|
||||||
|
if (!gate || !gate.data) return
|
||||||
|
|
||||||
|
const transform = gate.data.transform
|
||||||
|
const worldPosition = this.camera.toWordPostition(
|
||||||
|
event.position
|
||||||
|
)
|
||||||
|
|
||||||
|
transform.x = worldPosition[0] - this.selectOffset[0]
|
||||||
|
transform.y = worldPosition[1] - this.selectOffset[1]
|
||||||
|
|
||||||
|
if (!this.movedSelection) {
|
||||||
|
this.movedSelection = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(ctx: CanvasRenderingContext2D) {
|
||||||
|
this.clear(ctx)
|
||||||
|
|
||||||
|
// render gates
|
||||||
|
for (const gate of this.simulation.gates) {
|
||||||
|
if (this.options.shadows.enabled) {
|
||||||
|
renderGateShadow(ctx, this.options.shadows.color, gate)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGate(ctx, gate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(ctx: CanvasRenderingContext2D) {
|
||||||
|
const boundingBox = this.camera.transform.getBoundingBox()
|
||||||
|
ctx.clearRect(...boundingBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGateById(id: number) {
|
||||||
|
return this.simulation.gates.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOptimalShadow(gate: Gate) {
|
||||||
|
const center = multiply([this.screen.x, this.screen.y] as vector2, 0.5)
|
||||||
|
|
||||||
|
const difference = add(center, invert(gate.transform.position))
|
||||||
|
|
||||||
|
return add(
|
||||||
|
add(difference, center),
|
||||||
|
ofLength(difference, this.options.shadows.offset)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getShadowPosition(gate: Gate) {
|
||||||
|
return gate.transform.position.map(
|
||||||
|
(value, index) => value - this.getOptimalShadow(gate)[index]
|
||||||
|
) as vector2
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(delta: number) {
|
||||||
|
for (const gate of this.simulation.gates) {
|
||||||
|
gate.shadow = smoothStep(
|
||||||
|
this.options.shadows.speed,
|
||||||
|
gate.shadow,
|
||||||
|
this.getShadowPosition(gate)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = this.getSelected()
|
||||||
|
|
||||||
|
if (selected && this.movedSelection) {
|
||||||
|
this.mouseManager.update()
|
||||||
|
selected.transform.rotation =
|
||||||
|
this.mouseManager.getDirection() * this.options.dnd.rotation
|
||||||
|
} else {
|
||||||
|
this.mouseManager.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSelected() {
|
||||||
|
if (this.selectedGate === null) return null
|
||||||
|
|
||||||
|
const gate = this.getGateById(this.selectedGate)
|
||||||
|
|
||||||
|
if (!gate || !gate.data) return null
|
||||||
|
|
||||||
|
return gate.data
|
||||||
|
}
|
||||||
|
}
|
50
src/modules/simulation/classes/Transform.ts
Normal file
50
src/modules/simulation/classes/Transform.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { BehaviorSubject } from 'rxjs'
|
||||||
|
|
||||||
|
export type vector2 = [number, number]
|
||||||
|
export type vector4 = [number, number, number, number]
|
||||||
|
|
||||||
|
export class Transform {
|
||||||
|
public constructor(
|
||||||
|
public position: vector2 = [0, 0],
|
||||||
|
public scale: vector2 = [1, 1],
|
||||||
|
public rotation = 0
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public getBoundingBox() {
|
||||||
|
return [...this.position, ...this.scale] as vector4
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Short forms for random stuff */
|
||||||
|
|
||||||
|
get x() {
|
||||||
|
return this.position[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
get y() {
|
||||||
|
return this.position[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this.scale[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this.scale[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxX() {
|
||||||
|
return this.x + this.width
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxY() {
|
||||||
|
return this.y + this.height
|
||||||
|
}
|
||||||
|
|
||||||
|
set x(value: number) {
|
||||||
|
this.position = [value, this.y]
|
||||||
|
}
|
||||||
|
|
||||||
|
set y(value: number) {
|
||||||
|
this.position = [this.x, value]
|
||||||
|
}
|
||||||
|
}
|
6
src/modules/simulation/helpers/clamp.ts
Normal file
6
src/modules/simulation/helpers/clamp.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export const clamp = (low: number, high: number, current: number) => {
|
||||||
|
if (current < low) return low
|
||||||
|
if (current > high) return high
|
||||||
|
|
||||||
|
return current
|
||||||
|
}
|
31
src/modules/simulation/helpers/drawRotatedSquare.ts
Normal file
31
src/modules/simulation/helpers/drawRotatedSquare.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Transform } from '../classes/Transform'
|
||||||
|
|
||||||
|
export const drawRotatedSquare = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
{ position, scale, rotation }: Transform,
|
||||||
|
rotationMode = 0
|
||||||
|
) => {
|
||||||
|
ctx.save()
|
||||||
|
|
||||||
|
ctx.translate(...position)
|
||||||
|
|
||||||
|
if (rotationMode === 0) {
|
||||||
|
ctx.translate(scale[0] / 2, scale[1] / 2)
|
||||||
|
} else if (rotationMode === 1) {
|
||||||
|
ctx.translate(scale[0], scale[1])
|
||||||
|
} else if (rotationMode === 1) {
|
||||||
|
ctx.translate(0, scale[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.rotate(rotation)
|
||||||
|
|
||||||
|
if (rotationMode === 0) {
|
||||||
|
ctx.fillRect(scale[0] / -2, scale[1] / -2, ...scale)
|
||||||
|
} else if (rotationMode === 1) {
|
||||||
|
ctx.fillRect(-scale[0], -scale[1], ...scale)
|
||||||
|
} else if (rotationMode === -1) {
|
||||||
|
ctx.fillRect(0, 0, ...scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
11
src/modules/simulation/helpers/pointInSquare.ts
Normal file
11
src/modules/simulation/helpers/pointInSquare.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { vector2, Transform } from '../classes/Transform'
|
||||||
|
import { LruCacheNode } from '@eix-js/utils'
|
||||||
|
|
||||||
|
export const pointInSquare = (point: vector2, square: Transform) => {
|
||||||
|
return (
|
||||||
|
point[0] >= square.x &&
|
||||||
|
point[0] <= square.maxX &&
|
||||||
|
point[1] >= square.y &&
|
||||||
|
point[1] <= square.maxY
|
||||||
|
)
|
||||||
|
}
|
13
src/modules/simulation/helpers/renderGate.ts
Normal file
13
src/modules/simulation/helpers/renderGate.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { Gate } from '../classes/Gate'
|
||||||
|
import { drawRotatedSquare } from './drawRotatedSquare'
|
||||||
|
import { MouseManager } from '../classes/MouseManager'
|
||||||
|
|
||||||
|
export const renderGate = (ctx: CanvasRenderingContext2D, gate: Gate) => {
|
||||||
|
let mode = 0
|
||||||
|
|
||||||
|
if (gate.transform.rotation > 0) mode = 1
|
||||||
|
else if (gate.transform.rotation < 0) mode = -1
|
||||||
|
|
||||||
|
ctx.fillStyle = gate.color
|
||||||
|
drawRotatedSquare(ctx, gate.transform, mode)
|
||||||
|
}
|
19
src/modules/simulation/helpers/renderGateShadow.ts
Normal file
19
src/modules/simulation/helpers/renderGateShadow.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Gate } from '../classes/Gate'
|
||||||
|
import { vector2, Transform } from '../classes/Transform'
|
||||||
|
import { clamp } from './clamp'
|
||||||
|
import { drawRotatedSquare } from './drawRotatedSquare'
|
||||||
|
|
||||||
|
export const renderGateShadow = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
color: string,
|
||||||
|
gate: Gate
|
||||||
|
) => {
|
||||||
|
const scale = gate.transform.scale
|
||||||
|
|
||||||
|
ctx.fillStyle = color
|
||||||
|
|
||||||
|
drawRotatedSquare(
|
||||||
|
ctx,
|
||||||
|
new Transform(gate.shadow, scale, gate.transform.rotation)
|
||||||
|
)
|
||||||
|
}
|
1
src/modules/simulation/types/DeepPartial.ts
Normal file
1
src/modules/simulation/types/DeepPartial.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type DeepPartial<T> = { [key in keyof T]?: DeepPartial<T[key]> }
|
24
src/modules/vector2/helpers/basic.ts
Normal file
24
src/modules/vector2/helpers/basic.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { vector2 } from '../../simulation/classes/Transform'
|
||||||
|
|
||||||
|
// Basic stuff for arrays
|
||||||
|
|
||||||
|
export const add = (first: vector2, second: vector2) =>
|
||||||
|
first.map((value, index) => value + second[index]) as vector2
|
||||||
|
|
||||||
|
export const invert = (vector: vector2) => vector.map(val => -val) as vector2
|
||||||
|
|
||||||
|
export const length = (vector: vector2) =>
|
||||||
|
Math.sqrt(vector[0] ** 2 + vector[1] ** 2)
|
||||||
|
|
||||||
|
export const multiply = (vector: vector2, scalar: number) =>
|
||||||
|
vector.map(val => val * scalar) as vector2
|
||||||
|
|
||||||
|
export const normalise = (vector: vector2) => {
|
||||||
|
const size = length(vector)
|
||||||
|
|
||||||
|
return vector.map(val => val / size) as vector2
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ofLength = (vector: vector2, l: number) => {
|
||||||
|
return multiply(vector, l / length(vector))
|
||||||
|
}
|
8
src/modules/vector2/helpers/smoothStep.ts
Normal file
8
src/modules/vector2/helpers/smoothStep.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { vector2 } from '../../simulation/classes/Transform'
|
||||||
|
|
||||||
|
// TODO: rename
|
||||||
|
export const smoothStep = (step: number, current: vector2, target: vector2) => {
|
||||||
|
return current.map(
|
||||||
|
(position, index) => position + (target[index] - position) / step
|
||||||
|
) as vector2
|
||||||
|
}
|
|
@ -1,118 +0,0 @@
|
||||||
@import "./toastr.scss";
|
|
||||||
@import "./modal.scss";
|
|
||||||
|
|
||||||
$mdc-theme-primary: orange;
|
|
||||||
$mdc-theme-secondary: white;
|
|
||||||
$mdc-theme-on-primary: white;
|
|
||||||
$mdc-theme-surface: black;
|
|
||||||
$mdc-theme-on-secondary: white;
|
|
||||||
$mdc-theme-text-secondary-on-background: white;
|
|
||||||
|
|
||||||
@import "@material/drawer/mdc-drawer.scss";
|
|
||||||
@import "@material/list/mdc-list";
|
|
||||||
@import "@material/dialog/mdc-dialog";
|
|
||||||
@import "@material/button/mdc-button";
|
|
||||||
@import "@material/menu-surface/mdc-menu-surface";
|
|
||||||
@import "@material/menu/mdc-menu";
|
|
||||||
@import "@material/textfield/mdc-text-field";
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
height:100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
background-color: #222222;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
// .mdc-list-item__secondary-text {
|
|
||||||
// color: rgba(128,128,128,0.5) !important;
|
|
||||||
// margin-bottom: 3px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.createBar {
|
|
||||||
z-index:10;
|
|
||||||
position: absolute;
|
|
||||||
top:0px;
|
|
||||||
left:0px;
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
background-color: rgba(0,0,0,0.5);
|
|
||||||
transition: all 0.6s ease-in-out 0s;
|
|
||||||
.topContainer {
|
|
||||||
height: 30%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
div{
|
|
||||||
height:25%;
|
|
||||||
width:75%;
|
|
||||||
input{
|
|
||||||
background-color: #444444;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
font-size: 250%;
|
|
||||||
height:100%;
|
|
||||||
width:100%;
|
|
||||||
padding: 1%;
|
|
||||||
font-family: "roboto";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.createBar#shown{
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toasts{
|
|
||||||
background-color: #000000;
|
|
||||||
box-shadow: 0 0 0px black !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.component-container{
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
img.component {
|
|
||||||
// border: 10px solid black;
|
|
||||||
border-radius: 20px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-sidebar {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
background: #111111;
|
|
||||||
color: white;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mdc-list-item {
|
|
||||||
color:white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-icons {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mdc-list-item--activated {
|
|
||||||
background : orange;
|
|
||||||
*{
|
|
||||||
color: rgb(255, 255, 255);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
/**************************\
|
|
||||||
Basic Modal Styles
|
|
||||||
\**************************/
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0,0,0,0.6);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__container {
|
|
||||||
background-color: #222;
|
|
||||||
color: rgb(256,256,256);
|
|
||||||
padding: 30px;
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 100vh;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__title {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.25;
|
|
||||||
color: #0099aa;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__close {
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__header .modal__close:before { content: "\2715"; }
|
|
||||||
|
|
||||||
.modal__content {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: rgba(256,256,256,0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__btn {
|
|
||||||
font-size: .875rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
padding-top: .5rem;
|
|
||||||
padding-bottom: .5rem;
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
color: rgba(0,0,0,.8);
|
|
||||||
border-radius: .25rem;
|
|
||||||
border-style: none;
|
|
||||||
border-width: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-appearance: button;
|
|
||||||
text-transform: none;
|
|
||||||
overflow: visible;
|
|
||||||
line-height: 1.15;
|
|
||||||
margin: 0;
|
|
||||||
will-change: transform;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-backface-visibility: hidden;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
transform: translateZ(0);
|
|
||||||
transition: -webkit-transform .25s ease-out;
|
|
||||||
transition: transform .25s ease-out;
|
|
||||||
transition: transform .25s ease-out,-webkit-transform .25s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__btn:focus, .modal__btn:hover {
|
|
||||||
-webkit-transform: scale(1.05);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__btn-primary {
|
|
||||||
background-color: #00449e;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**************************\
|
|
||||||
Demo Animation Style
|
|
||||||
\**************************/
|
|
||||||
@keyframes mmfadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes mmfadeOut {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes mmslideIn {
|
|
||||||
from { transform: translateY(15%); }
|
|
||||||
to { transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes mmslideOut {
|
|
||||||
from { transform: translateY(0); }
|
|
||||||
to { transform: translateY(-10%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.micromodal-slide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.micromodal-slide.is-open {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.micromodal-slide[aria-hidden="false"] .modal__overlay {
|
|
||||||
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.micromodal-slide[aria-hidden="false"] .modal__container {
|
|
||||||
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.micromodal-slide[aria-hidden="true"] .modal__overlay {
|
|
||||||
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.micromodal-slide[aria-hidden="true"] .modal__container {
|
|
||||||
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.micromodal-slide .modal__container,
|
|
||||||
.micromodal-slide .modal__overlay {
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
|
@ -1,5 +0,0 @@
|
||||||
export default function clamp(value:number, min:number, max:number) {
|
|
||||||
return min < max
|
|
||||||
? (value < min ? min : value > max ? max : value)
|
|
||||||
: (value < max ? max : value > min ? min : value)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./clamp"
|
|
|
@ -1,231 +0,0 @@
|
||||||
import { BehaviorSubject, Subscription, timer } from "rxjs";
|
|
||||||
import { ComponentState, activationContext } from "./interfaces";
|
|
||||||
import { map, debounce } from "rxjs/operators";
|
|
||||||
import { Screen } from "../screen.ts";
|
|
||||||
import { ComponentTemplateStore } from "../componentManager/componentTemplateStore";
|
|
||||||
import { svg } from "lit-html";
|
|
||||||
import { subscribe } from "lit-rx";
|
|
||||||
import { Pin } from "../pin";
|
|
||||||
import { success, error } from "toastr"
|
|
||||||
import { alertOptions } from "../componentManager/alertOptions";
|
|
||||||
import { WireManager } from "../wires";
|
|
||||||
import { runCounter } from "./runCounter";
|
|
||||||
import { Material } from "./material";
|
|
||||||
import { manager } from "../../main";
|
|
||||||
|
|
||||||
export class Component {
|
|
||||||
private static store = new ComponentTemplateStore()
|
|
||||||
private static screen = new Screen()
|
|
||||||
private static wireManager = new WireManager()
|
|
||||||
|
|
||||||
public position = new BehaviorSubject<number[]>(null)
|
|
||||||
public scale = new BehaviorSubject<number[]>(null)
|
|
||||||
public clicked = false
|
|
||||||
public id: number
|
|
||||||
public material: Material
|
|
||||||
public clickedChanges = new BehaviorSubject(false)
|
|
||||||
public scaling = false
|
|
||||||
|
|
||||||
private mouserDelta: number[]
|
|
||||||
private strokeColor = "#888888"
|
|
||||||
private inputs: number
|
|
||||||
private outputs: number
|
|
||||||
private activation: ((ctx: activationContext) => any)[] = []
|
|
||||||
private subscriptions: Subscription[] = []
|
|
||||||
|
|
||||||
public inputPins: Pin[] = []
|
|
||||||
public outputPins: Pin[] = []
|
|
||||||
|
|
||||||
public x = this.position.pipe(map(val =>
|
|
||||||
val[0]
|
|
||||||
))
|
|
||||||
|
|
||||||
public y = this.position.pipe(map(val =>
|
|
||||||
val[1]
|
|
||||||
))
|
|
||||||
|
|
||||||
public width = this.scale.pipe(map(val =>
|
|
||||||
val[0]
|
|
||||||
))
|
|
||||||
|
|
||||||
public height = this.scale.pipe(map(val =>
|
|
||||||
val[1]
|
|
||||||
))
|
|
||||||
|
|
||||||
constructor(private template: string,
|
|
||||||
position: [number, number] = [0, 0],
|
|
||||||
scale: [number, number] = [0, 0],
|
|
||||||
id?: number) {
|
|
||||||
|
|
||||||
//set initial props
|
|
||||||
this.position.next(position)
|
|
||||||
this.scale.next(scale)
|
|
||||||
|
|
||||||
//set the correct id
|
|
||||||
this.id = (typeof id === "number") ? id : Component.getId()
|
|
||||||
|
|
||||||
//load template
|
|
||||||
const data = Component.store.store.get(template)
|
|
||||||
|
|
||||||
if (!data)
|
|
||||||
throw new Error(`Template ${template} doesnt exist`)
|
|
||||||
|
|
||||||
this.inputs = data.inputs
|
|
||||||
this.outputs = data.outputs
|
|
||||||
|
|
||||||
this.inputPins = [...Array(this.inputs)].fill(true).map(() => new Pin(false, this))
|
|
||||||
this.outputPins = [...Array(this.outputs)].fill(true).map(() => new Pin(true, this))
|
|
||||||
|
|
||||||
this.activation = [data.activation, data.onclick ? data.onclick : ""]
|
|
||||||
.map(val => {
|
|
||||||
return new Function(`return (ctx) => {
|
|
||||||
try{
|
|
||||||
${val}
|
|
||||||
}
|
|
||||||
catch(err){
|
|
||||||
ctx.error(err,"",ctx.alertOptions)
|
|
||||||
}
|
|
||||||
}`)()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.inputPins.forEach(val => {
|
|
||||||
const subscription = val.valueChanges.pipe(debounce(() => timer(1000 / 60)))
|
|
||||||
.subscribe(() => this.activate())
|
|
||||||
this.subscriptions.push(subscription)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.material = new Material(data.material.mode, data.material.data)
|
|
||||||
this.activate()
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose() {
|
|
||||||
this.subscriptions.forEach(val => val.unsubscribe())
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMouseUp() {
|
|
||||||
this.clicked = false
|
|
||||||
this.scaling = false
|
|
||||||
this.clickedChanges.next(this.clicked)
|
|
||||||
}
|
|
||||||
|
|
||||||
private activate(index: number = 0) {
|
|
||||||
this.activation[index]({
|
|
||||||
outputs: this.outputPins,
|
|
||||||
inputs: this.inputPins,
|
|
||||||
succes: (mes: string) => { success(mes, "", alertOptions) },
|
|
||||||
error: (mes: string) => { error(mes, "", alertOptions) },
|
|
||||||
color: (color: string) => {
|
|
||||||
this.material.color.next(color)
|
|
||||||
}
|
|
||||||
} as activationContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
move(e: MouseEvent) {
|
|
||||||
const mousePosition = Component.screen.getWorldPosition(e.clientX, e.clientY)
|
|
||||||
this.position.next(mousePosition.map((value, index) =>
|
|
||||||
value - this.mouserDelta[index]
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(e: MouseEvent) {
|
|
||||||
if (e.button === 0) {
|
|
||||||
const mousePosition = Component.screen.getWorldPosition(e.clientX, e.clientY)
|
|
||||||
|
|
||||||
this.mouserDelta = this.position.value.map((value, index) =>
|
|
||||||
mousePosition[index] - value
|
|
||||||
)
|
|
||||||
this.clicked = true
|
|
||||||
this.clickedChanges.next(this.clicked)
|
|
||||||
|
|
||||||
this.activate(1)
|
|
||||||
this.activate(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (e.button === 1) {
|
|
||||||
this.scaling = true
|
|
||||||
}
|
|
||||||
else if (e.button === 2) {
|
|
||||||
manager.components = manager.components.filter(({ id }) => id !== this.id)
|
|
||||||
manager.wireManager.wires
|
|
||||||
.filter(val => val.input.of.id == this.id || val.output.of.id == this.id)
|
|
||||||
.forEach(val => {
|
|
||||||
manager.wireManager.remove(val)
|
|
||||||
})
|
|
||||||
manager.silentRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePinClick(pin: Pin) {
|
|
||||||
Component.wireManager.add(pin)
|
|
||||||
}
|
|
||||||
|
|
||||||
get state(): ComponentState {
|
|
||||||
return {
|
|
||||||
position: this.position.value as [number, number],
|
|
||||||
scale: this.scale.value as [number, number],
|
|
||||||
template: this.template,
|
|
||||||
id: this.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pinsSvg(pinScale: number, pinLength = 20, mode = "input") {
|
|
||||||
const stroke = 3
|
|
||||||
|
|
||||||
return ((mode === "input") ? this.inputPins : this.outputPins)
|
|
||||||
.map((val, index) => {
|
|
||||||
const y = subscribe(this.piny(mode === "input", index))
|
|
||||||
|
|
||||||
const x = subscribe(this.pinx(mode === "input", pinLength))
|
|
||||||
|
|
||||||
const linex = subscribe(this.x.pipe(map(val =>
|
|
||||||
val + ((mode === "input") ? -pinLength : pinLength + this.scale.value[0])
|
|
||||||
)))
|
|
||||||
|
|
||||||
const middleX = subscribe(this.x.pipe(map(val => {
|
|
||||||
const scale = this.scale.value[0]
|
|
||||||
return val + ((mode === "input") ? scale / 10 : 9 * scale / 10)
|
|
||||||
})))
|
|
||||||
|
|
||||||
return svg`
|
|
||||||
<line stroke=${this.strokeColor} y1=${y} y2=${y}
|
|
||||||
x1=${(mode === "input") ? linex : middleX}
|
|
||||||
x2=${(mode !== "input") ? linex : middleX}
|
|
||||||
stroke-width=${stroke}></line>
|
|
||||||
|
|
||||||
<circle fill=${subscribe(val.svgColor)}
|
|
||||||
stroke=${this.strokeColor}
|
|
||||||
r=${pinScale}
|
|
||||||
cx=${x}
|
|
||||||
cy=${y} stroke-width=${stroke}
|
|
||||||
@click=${() => this.handlePinClick(val)}
|
|
||||||
></circle>
|
|
||||||
`})
|
|
||||||
}
|
|
||||||
|
|
||||||
public pinx(mode = true, pinLength = 15) {
|
|
||||||
return this.position.pipe(
|
|
||||||
map(val => val[0] + (
|
|
||||||
(mode) ?
|
|
||||||
-pinLength :
|
|
||||||
this.scale.value[0] + pinLength
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public piny(mode = true, index: number) {
|
|
||||||
const space = this.scale.value[1] / (mode ? this.inputs : this.outputs)
|
|
||||||
return this.y.pipe(
|
|
||||||
map(val => val + space * (2 * index + 1) / 2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromState(state: ComponentState) {
|
|
||||||
return new Component(state.template, state.position, state.scale, state.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getId() {
|
|
||||||
const data = runCounter.get()
|
|
||||||
runCounter.increase()
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./component"
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Pin } from '../pin'
|
|
||||||
|
|
||||||
export interface ComponentState {
|
|
||||||
position: [number, number]
|
|
||||||
scale: [number, number]
|
|
||||||
template: string
|
|
||||||
id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface activationContext {
|
|
||||||
inputs: Pin[]
|
|
||||||
outputs: Pin[]
|
|
||||||
succes: (mes: string) => any
|
|
||||||
error: (mes: string) => any
|
|
||||||
color: (color: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type materialMode = 'standard_image' | 'color' | 'url'
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { svg, Part } from 'lit-html'
|
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import { materialMode } from './interfaces'
|
|
||||||
|
|
||||||
declare function require<T>(path: string): T
|
|
||||||
|
|
||||||
type partFactory = (part: Part) => void
|
|
||||||
|
|
||||||
export class Material {
|
|
||||||
private static images: {
|
|
||||||
[key: string]: string
|
|
||||||
} = {
|
|
||||||
and: require('../../../assets/and_gate.jpg'),
|
|
||||||
or: require('../../../assets/or_gate.png'),
|
|
||||||
xor: require('../../../assets/xor_gate.png'),
|
|
||||||
nor: require('../../../assets/nor_gate.png')
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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}>
|
|
||||||
<div class="component-container">
|
|
||||||
<img src=${src} height="97%" width="97%" draggable=false class="component">
|
|
||||||
</div>
|
|
||||||
</foreignobject>`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Store } from "../store";
|
|
||||||
|
|
||||||
export const runCounter = {
|
|
||||||
store: new Store<number>("runCounter"),
|
|
||||||
get(){
|
|
||||||
return runCounter.store.get("main")
|
|
||||||
},
|
|
||||||
increase(){
|
|
||||||
runCounter.store.set("main", runCounter.store.get("main") + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!runCounter.get())
|
|
||||||
runCounter.store.set("main",1)
|
|
|
@ -1,23 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export const fecthAsJson = async <T>(url: string) => {
|
|
||||||
const res = await fetch(url)
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
return json as T
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export const alertOptions = {
|
|
||||||
positionClass: "toast-bottom-right",
|
|
||||||
toastClass: "toasts"
|
|
||||||
}
|
|
|
@ -1,735 +0,0 @@
|
||||||
import { Singleton } from '@eix/utils'
|
|
||||||
import { Component } from '../component'
|
|
||||||
import { Subject, BehaviorSubject, fromEvent } from 'rxjs'
|
|
||||||
import { svg, SVGTemplateResult, html } from 'lit-html'
|
|
||||||
import { subscribe } from 'lit-rx'
|
|
||||||
import { Screen } from '../screen.ts'
|
|
||||||
import { ManagerState, ComponentTemplate } from './interfaces'
|
|
||||||
import { Store } from '../store'
|
|
||||||
import { KeyboardInput } from '@eix/input'
|
|
||||||
import { success, error } from 'toastr'
|
|
||||||
import { ComponentTemplateStore } from './componentTemplateStore'
|
|
||||||
import { alertOptions } from './alertOptions'
|
|
||||||
import { WireManager } from '../wires'
|
|
||||||
import { runCounter } from '../component/runCounter'
|
|
||||||
import { Settings } from '../store/settings'
|
|
||||||
import { download } from './download'
|
|
||||||
import { modal } from '../modals'
|
|
||||||
import { map } from 'rxjs/operators'
|
|
||||||
import { persistent } from '../store/persistent'
|
|
||||||
import { MDCTextField } from '@material/textfield'
|
|
||||||
import { importComponent } from '../componentImporter/importComponent'
|
|
||||||
|
|
||||||
const defaultName = 'default'
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
export class ComponentManager {
|
|
||||||
public components: Component[] = []
|
|
||||||
public svgs = new Subject<SVGTemplateResult>()
|
|
||||||
public placeholder = new BehaviorSubject('Create simulation')
|
|
||||||
public barAlpha = new BehaviorSubject<string>('0')
|
|
||||||
public wireManager = new WireManager()
|
|
||||||
public onTop: Component
|
|
||||||
public templateStore = new ComponentTemplateStore()
|
|
||||||
|
|
||||||
private temporaryCommnad = ''
|
|
||||||
private clicked = false
|
|
||||||
private ignoreKeyDowns = false
|
|
||||||
|
|
||||||
private screen = new Screen()
|
|
||||||
private settings = new Settings()
|
|
||||||
private standard: {
|
|
||||||
offset: number
|
|
||||||
scale: [number, number]
|
|
||||||
} = {
|
|
||||||
offset: 50,
|
|
||||||
scale: [100, 100]
|
|
||||||
}
|
|
||||||
|
|
||||||
private commandHistoryStore = new Store<string>('commandHistory')
|
|
||||||
private store = new Store<ManagerState>('simulationStates')
|
|
||||||
|
|
||||||
private saveEvent = new KeyboardInput('s')
|
|
||||||
private createEvent = new KeyboardInput('m')
|
|
||||||
private closeInputEvent = new KeyboardInput('enter')
|
|
||||||
private ctrlEvent = new KeyboardInput('ctrl')
|
|
||||||
private palleteEvent = new KeyboardInput('p')
|
|
||||||
private undoEvent = new KeyboardInput('z')
|
|
||||||
private shiftEvent = new KeyboardInput('shift')
|
|
||||||
private refreshEvent = new KeyboardInput('r')
|
|
||||||
private gEvent = new KeyboardInput('g')
|
|
||||||
private clearEvent = new KeyboardInput('delete')
|
|
||||||
private upEvent = new KeyboardInput('up')
|
|
||||||
private downEvent = new KeyboardInput('down')
|
|
||||||
|
|
||||||
@persistent<ComponentManager, string>(defaultName, 'main')
|
|
||||||
public name: string
|
|
||||||
public alertOptions = alertOptions
|
|
||||||
|
|
||||||
private commandHistory: string[] = []
|
|
||||||
private commands: {
|
|
||||||
[key: string]: (
|
|
||||||
ctx: ComponentManager,
|
|
||||||
args: string[],
|
|
||||||
flags: string[]
|
|
||||||
) => any
|
|
||||||
} = {
|
|
||||||
clear(ctx: ComponentManager) {
|
|
||||||
ctx.clear()
|
|
||||||
},
|
|
||||||
save(ctx: ComponentManager) {
|
|
||||||
ctx.save()
|
|
||||||
},
|
|
||||||
ls(ctx: ComponentManager) {
|
|
||||||
const data = ctx.store.ls()
|
|
||||||
const message = data.join('\n')
|
|
||||||
|
|
||||||
success(message, '', ctx.alertOptions)
|
|
||||||
},
|
|
||||||
help(ctx: ComponentManager) {
|
|
||||||
success(
|
|
||||||
`Usage: <command> <br>
|
|
||||||
Where <command> is one of:
|
|
||||||
<ul>
|
|
||||||
${Object.keys(ctx.commands)
|
|
||||||
.map(
|
|
||||||
val => `
|
|
||||||
<li>${val}</li>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
'',
|
|
||||||
ctx.alertOptions
|
|
||||||
)
|
|
||||||
},
|
|
||||||
refresh(ctx: ComponentManager) {
|
|
||||||
ctx.refresh()
|
|
||||||
},
|
|
||||||
rewind(ctx: ComponentManager) {
|
|
||||||
localStorage.clear()
|
|
||||||
success('Succesfully cleared localStorage!', '', ctx.alertOptions)
|
|
||||||
},
|
|
||||||
ctp: this.templateStore.commands.template,
|
|
||||||
settings: this.settings.commands,
|
|
||||||
download
|
|
||||||
}
|
|
||||||
private inputMode: string
|
|
||||||
|
|
||||||
public gates = this.templateStore.store.lsChanges
|
|
||||||
public saves = this.store.lsChanges
|
|
||||||
|
|
||||||
public file: {
|
|
||||||
[key: string]: () => void
|
|
||||||
} = {
|
|
||||||
clear: () => this.clear(),
|
|
||||||
clean: () => this.smartClear(),
|
|
||||||
save: () => this.save(),
|
|
||||||
undo: () => this.refresh(),
|
|
||||||
download: () => download(this, [], []),
|
|
||||||
delete: () => this.delete(this.name),
|
|
||||||
refresh: () => this.silentRefresh(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
public shortcuts: {
|
|
||||||
[key: string]: string
|
|
||||||
} = {
|
|
||||||
clear: 'shift delete',
|
|
||||||
clean: 'delete',
|
|
||||||
save: 'ctrl s',
|
|
||||||
undo: 'ctrl z',
|
|
||||||
refresh: 'ctrl r'
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
runCounter.increase()
|
|
||||||
|
|
||||||
this.svgs.next(this.render())
|
|
||||||
|
|
||||||
this.refresh()
|
|
||||||
|
|
||||||
fromEvent(document.body, 'keydown').subscribe((e: KeyboardEvent) => {
|
|
||||||
if (this.barAlpha.value == '1') {
|
|
||||||
const elem = document.getElementById('nameInput')
|
|
||||||
elem.focus()
|
|
||||||
} else if (!this.ignoreKeyDowns) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fromEvent(document.body, 'keyup').subscribe((e: MouseEvent) => {
|
|
||||||
if (this.barAlpha.value === '1') {
|
|
||||||
if (this.closeInputEvent.value) this.create()
|
|
||||||
else if (this.inputMode === 'command') {
|
|
||||||
const elem = <HTMLInputElement>(
|
|
||||||
document.getElementById('nameInput')
|
|
||||||
)
|
|
||||||
if (this.upEvent.value) {
|
|
||||||
document.body.focus()
|
|
||||||
e.preventDefault()
|
|
||||||
const index = this.commandHistory.indexOf(elem.value)
|
|
||||||
|
|
||||||
if (index) {
|
|
||||||
//save drafts
|
|
||||||
if (index === -1) this.temporaryCommnad = elem.value
|
|
||||||
|
|
||||||
const newIndex =
|
|
||||||
index === -1
|
|
||||||
? this.commandHistory.length - 1
|
|
||||||
: index - 1
|
|
||||||
elem.value = this.commandHistory[newIndex]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.downEvent.value) {
|
|
||||||
document.body.focus()
|
|
||||||
e.preventDefault()
|
|
||||||
const index = this.commandHistory.indexOf(elem.value)
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
const maxIndex = this.commandHistory.length - 1
|
|
||||||
elem.value =
|
|
||||||
index === maxIndex
|
|
||||||
? this.temporaryCommnad
|
|
||||||
: this.commandHistory[index + 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.ctrlEvent.value) {
|
|
||||||
if (this.createEvent.value) {
|
|
||||||
this.prepareNewSimulation()
|
|
||||||
} else if (
|
|
||||||
this.shiftEvent.value &&
|
|
||||||
this.palleteEvent.value
|
|
||||||
) {
|
|
||||||
this.preInput()
|
|
||||||
this.inputMode = 'command'
|
|
||||||
this.placeholder.next('Command palette')
|
|
||||||
} else if (this.gEvent.value) {
|
|
||||||
this.importGate()
|
|
||||||
} else if (this.saveEvent.value) {
|
|
||||||
this.save()
|
|
||||||
} else if (this.undoEvent.value) {
|
|
||||||
this.refresh()
|
|
||||||
} else if (this.refreshEvent.value) {
|
|
||||||
this.silentRefresh(true)
|
|
||||||
}
|
|
||||||
} else if (this.clearEvent.value) {
|
|
||||||
if (this.shiftEvent.value) this.clear()
|
|
||||||
else this.smartClear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.wireManager.update.subscribe(() => {
|
|
||||||
// this.save()
|
|
||||||
this.update()
|
|
||||||
// this.save()
|
|
||||||
})
|
|
||||||
if (this.saves.value.length === 0) this.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
private initEmptyGate(name: string) {
|
|
||||||
const obj: ComponentTemplate = {
|
|
||||||
inputs: 1,
|
|
||||||
name,
|
|
||||||
version: '1.0.0',
|
|
||||||
outputs: 1,
|
|
||||||
activation: '',
|
|
||||||
editable: true,
|
|
||||||
material: {
|
|
||||||
mode: 'color',
|
|
||||||
data: 'blue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.templateStore.store.set(name, obj)
|
|
||||||
|
|
||||||
this.edit(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
public newGate() {
|
|
||||||
this.preInput()
|
|
||||||
this.inputMode = 'gate'
|
|
||||||
this.placeholder.next('Gate name')
|
|
||||||
}
|
|
||||||
|
|
||||||
public importGate() {
|
|
||||||
this.preInput()
|
|
||||||
this.inputMode = 'importGate'
|
|
||||||
this.placeholder.next('Gate url')
|
|
||||||
}
|
|
||||||
|
|
||||||
public prepareNewSimulation() {
|
|
||||||
this.preInput()
|
|
||||||
this.inputMode = 'create'
|
|
||||||
this.placeholder.next('Create simulation')
|
|
||||||
}
|
|
||||||
|
|
||||||
private preInput() {
|
|
||||||
const elem = <HTMLInputElement>document.getElementById('nameInput')
|
|
||||||
elem.value = ''
|
|
||||||
this.barAlpha.next('1')
|
|
||||||
}
|
|
||||||
|
|
||||||
private async create() {
|
|
||||||
const elem = <HTMLInputElement>document.getElementById('nameInput')
|
|
||||||
this.barAlpha.next('0')
|
|
||||||
|
|
||||||
if (this.inputMode === 'create') {
|
|
||||||
await this.createEmptySimulation(elem.value)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public succes(message: string) {
|
|
||||||
success(message, '', this.alertOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleDuplicateModal(name: string) {
|
|
||||||
const result = await modal({
|
|
||||||
title: 'Warning',
|
|
||||||
content: html`
|
|
||||||
There was already a simulation called ${name}, are you sure you
|
|
||||||
want to override it? All your work will be lost!
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
public async edit(name: string) {
|
|
||||||
this.ignoreKeyDowns = true
|
|
||||||
const gate = this.templateStore.store.get(name)
|
|
||||||
|
|
||||||
modal({
|
|
||||||
no: '',
|
|
||||||
yes: 'save',
|
|
||||||
title: `Edit ${name}`,
|
|
||||||
content: html`
|
|
||||||
${html`
|
|
||||||
<br />
|
|
||||||
<div class="mdc-text-field mdc-text-field--textarea">
|
|
||||||
<textarea
|
|
||||||
id="codeArea"
|
|
||||||
class="mdc-text-field__input js"
|
|
||||||
rows="8"
|
|
||||||
cols="40"
|
|
||||||
>
|
|
||||||
${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>
|
|
||||||
<br /><br />
|
|
||||||
<div class="mdc-text-field" id="inputCount">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="my-text-field"
|
|
||||||
class="mdc-text-field__input inputCount-i"
|
|
||||||
value=${gate.inputs}
|
|
||||||
/>
|
|
||||||
<label class="mdc-floating-label" for="my-text-field"
|
|
||||||
>Inputs</label
|
|
||||||
>
|
|
||||||
<div class="mdc-line-ripple"></div>
|
|
||||||
</div>
|
|
||||||
<br /><br />
|
|
||||||
<div class="mdc-text-field" id="outputCount">
|
|
||||||
<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 => {
|
|
||||||
this.ignoreKeyDowns = false
|
|
||||||
const elems: (HTMLInputElement | HTMLTextAreaElement)[] = [
|
|
||||||
document.querySelector('#codeArea'),
|
|
||||||
document.querySelector('.inputCount-i'),
|
|
||||||
document.querySelector('.outputCount-i'),
|
|
||||||
document.querySelector('.color-i')
|
|
||||||
]
|
|
||||||
const data = elems.map(val => val.value)
|
|
||||||
|
|
||||||
this.templateStore.store.set(name, {
|
|
||||||
...gate,
|
|
||||||
activation: data[0],
|
|
||||||
inputs: Number(data[1]),
|
|
||||||
outputs: Number(data[2]),
|
|
||||||
material: {
|
|
||||||
mode: 'color',
|
|
||||||
data: data[3]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
new MDCTextField(document.querySelector('.mdc-text-field'))
|
|
||||||
new MDCTextField(document.querySelector('#outputCount'))
|
|
||||||
new MDCTextField(document.querySelector('#inputCount'))
|
|
||||||
new MDCTextField(document.querySelector('#color'))
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(template: string, position?: [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.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
public async delete(name: string) {
|
|
||||||
const res = await modal({
|
|
||||||
title: 'Are you sure?',
|
|
||||||
content: html`
|
|
||||||
Deleting a simulations is ireversible, and all work will be
|
|
||||||
lost!
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
if (this.name === name) {
|
|
||||||
if (this.saves.value.length > 1) {
|
|
||||||
this.switchTo(this.saves.value.find(val => val !== name))
|
|
||||||
} else {
|
|
||||||
let newName =
|
|
||||||
name === defaultName ? `${defaultName}(1)` : defaultName
|
|
||||||
await this.createEmptySimulation(newName)
|
|
||||||
this.switchTo(newName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.store.delete(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public createEmptySimulation(name: string) {
|
|
||||||
const create = () => {
|
|
||||||
this.store.set(name, {
|
|
||||||
wires: [],
|
|
||||||
components: [],
|
|
||||||
position: [0, 0],
|
|
||||||
scale: [1, 1]
|
|
||||||
})
|
|
||||||
|
|
||||||
if (name !== this.name) this.save()
|
|
||||||
this.name = name
|
|
||||||
this.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise(async res => {
|
|
||||||
//get wheater theres already a simulation with that name
|
|
||||||
if (
|
|
||||||
(this.store.get(name) &&
|
|
||||||
(await this.handleDuplicateModal(name))) ||
|
|
||||||
!this.store.get(name)
|
|
||||||
) {
|
|
||||||
create()
|
|
||||||
res(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public switchTo(name: string) {
|
|
||||||
const data = this.store.get(name)
|
|
||||||
if (!data)
|
|
||||||
error(
|
|
||||||
`An error occured when trying to load ${name}`,
|
|
||||||
'',
|
|
||||||
this.alertOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
this.name = name
|
|
||||||
this.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
eval(command: string) {
|
|
||||||
if (!this.commandHistory.includes(command))
|
|
||||||
// no duplicates
|
|
||||||
this.commandHistory.push(command)
|
|
||||||
|
|
||||||
while (
|
|
||||||
this.commandHistory.length > 10 // max of 10 elements
|
|
||||||
)
|
|
||||||
this.commandHistory.shift()
|
|
||||||
|
|
||||||
const words = command.split(' ')
|
|
||||||
|
|
||||||
if (words[0] in this.commands) {
|
|
||||||
const remaining = words.slice(1)
|
|
||||||
const flags = remaining.filter(val => val[0] == '-')
|
|
||||||
const args = remaining.filter(val => val[0] != '-')
|
|
||||||
this.commands[words[0]](this, args, flags)
|
|
||||||
} else
|
|
||||||
error(
|
|
||||||
`Command ${words} doesn't exist. Run help to get a list of all commands.`,
|
|
||||||
'',
|
|
||||||
this.alertOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public smartClear() {
|
|
||||||
this.components = this.components.filter(({ id }) =>
|
|
||||||
this.wireManager.wires.find(
|
|
||||||
val => val.input.of.id == id || val.output.of.id == id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
this.update()
|
|
||||||
success(
|
|
||||||
'Succesfully cleared all unconnected components',
|
|
||||||
'',
|
|
||||||
this.alertOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear() {
|
|
||||||
this.components = []
|
|
||||||
this.wireManager.dispose()
|
|
||||||
this.update()
|
|
||||||
|
|
||||||
success('Succesfully cleared all components', '', this.alertOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
public refresh() {
|
|
||||||
if (this.store.get(this.name)) {
|
|
||||||
this.loadState(this.store.get(this.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const i of this.commandHistoryStore.ls())
|
|
||||||
this.commandHistory[Number(i)] = this.commandHistoryStore.get(i)
|
|
||||||
|
|
||||||
this.update()
|
|
||||||
|
|
||||||
success(
|
|
||||||
'Succesfully refreshed to the latest save',
|
|
||||||
'',
|
|
||||||
this.alertOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.svgs.next(this.render())
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDown() {
|
|
||||||
this.clicked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseUp() {
|
|
||||||
this.clicked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseMove(e: MouseEvent) {
|
|
||||||
if (e.button === 0) {
|
|
||||||
let toAddOnTop: number
|
|
||||||
let outsideComponents = true
|
|
||||||
|
|
||||||
for (let i = 0; i < this.components.length; i++) {
|
|
||||||
const component = this.components[i]
|
|
||||||
if (component.clicked) {
|
|
||||||
outsideComponents = false
|
|
||||||
component.move(e)
|
|
||||||
if (this.onTop != component) {
|
|
||||||
toAddOnTop = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (false) { }
|
|
||||||
if (toAddOnTop >= 0) {
|
|
||||||
this.top(this.components[toAddOnTop])
|
|
||||||
} else if (outsideComponents && this.clicked) {
|
|
||||||
const mousePosition = [e.clientX, e.clientY]
|
|
||||||
const delta = mousePosition.map(
|
|
||||||
(value, index) => this.screen.mousePosition[index] - value
|
|
||||||
) as [number, number]
|
|
||||||
this.screen.move(...delta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public silentRefresh(verboose = false) {
|
|
||||||
this.loadState(this.state)
|
|
||||||
if (verboose)
|
|
||||||
success(
|
|
||||||
'Succesfully reloaded all components',
|
|
||||||
'',
|
|
||||||
this.alertOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public top(component: Component) {
|
|
||||||
if (this.onTop !== component) {
|
|
||||||
this.onTop = component
|
|
||||||
this.components.push(component)
|
|
||||||
}
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
private render() {
|
|
||||||
let toRemoveDuplicatesFor: Component
|
|
||||||
|
|
||||||
const result = this.components.map(component => {
|
|
||||||
const mouseupHandler = () => {
|
|
||||||
component.handleMouseUp()
|
|
||||||
toRemoveDuplicatesFor = component
|
|
||||||
}
|
|
||||||
|
|
||||||
const stroke = subscribe(
|
|
||||||
component.clickedChanges.pipe(
|
|
||||||
map(val => (val ? 'yellow' : 'black'))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return svg`
|
|
||||||
<g>
|
|
||||||
${component.pinsSvg(10, 20)}
|
|
||||||
${component.pinsSvg(10, 20, 'output')}
|
|
||||||
|
|
||||||
<g @mousedown=${(e: MouseEvent) => component.handleClick(e)}
|
|
||||||
@touchstart=${(e: MouseEvent) => component.handleClick(e)}
|
|
||||||
@mouseup=${mouseupHandler}
|
|
||||||
@touchend=${mouseupHandler}>
|
|
||||||
<rect width=${subscribe(component.width)}
|
|
||||||
height=${subscribe(component.height)}
|
|
||||||
x=${subscribe(component.x)}
|
|
||||||
y=${subscribe(component.y)}
|
|
||||||
stroke=${stroke}
|
|
||||||
fill=${
|
|
||||||
component.material.mode !== 'color'
|
|
||||||
? 'rgba(0,0,0,0)'
|
|
||||||
: subscribe(component.material.color)
|
|
||||||
}
|
|
||||||
rx=20
|
|
||||||
ry=20>
|
|
||||||
</rect>
|
|
||||||
${
|
|
||||||
component.material.mode !== 'color'
|
|
||||||
? component.material.innerHTML(
|
|
||||||
subscribe(component.x),
|
|
||||||
subscribe(component.y),
|
|
||||||
subscribe(component.width),
|
|
||||||
subscribe(component.height)
|
|
||||||
)
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
if (toRemoveDuplicatesFor) this.removeDuplicates(toRemoveDuplicatesFor)
|
|
||||||
|
|
||||||
return svg`${this.wireManager.svg} ${result}`
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeDuplicates(component: Component) {
|
|
||||||
let instances = this.components
|
|
||||||
.map((value, index) => (value == component ? index : null))
|
|
||||||
.filter(value => value)
|
|
||||||
instances.pop()
|
|
||||||
|
|
||||||
this.components = this.components.filter(
|
|
||||||
(_, index) => instances.indexOf(index) != -1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get state(): ManagerState {
|
|
||||||
const components = Array.from(new Set(this.components).values())
|
|
||||||
return {
|
|
||||||
components: components.map(value => value.state),
|
|
||||||
position: this.screen.position as [number, number],
|
|
||||||
scale: this.screen.scale as [number, number],
|
|
||||||
wires: this.wireManager.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get scaling(): Component {
|
|
||||||
return this.components.find(val => val.scaling)
|
|
||||||
}
|
|
||||||
|
|
||||||
public getComponentById(id: number) {
|
|
||||||
return this.components.find(val => val.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadState(state: ManagerState) {
|
|
||||||
if (!state.wires)
|
|
||||||
//old state
|
|
||||||
return
|
|
||||||
|
|
||||||
this.wireManager.dispose()
|
|
||||||
this.clicked = false
|
|
||||||
this.components = state.components.map(value =>
|
|
||||||
Component.fromState(value)
|
|
||||||
)
|
|
||||||
this.onTop = null
|
|
||||||
|
|
||||||
state.wires.forEach(val => {
|
|
||||||
this.wireManager.start = this.getComponentById(
|
|
||||||
val.from.owner
|
|
||||||
).outputPins[val.from.index]
|
|
||||||
this.wireManager.end = this.getComponentById(
|
|
||||||
val.to.owner
|
|
||||||
).inputPins[val.to.index]
|
|
||||||
this.wireManager.tryResolving()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.screen.scale = state.scale
|
|
||||||
this.screen.position = state.position
|
|
||||||
this.screen.update()
|
|
||||||
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
for (let i = 0; i < this.commandHistory.length; i++) {
|
|
||||||
const element = this.commandHistory[i]
|
|
||||||
this.commandHistoryStore.set(i.toString(), element)
|
|
||||||
}
|
|
||||||
this.store.set(this.name, this.state)
|
|
||||||
success(
|
|
||||||
`Saved the simulation ${this.name} succesfully!`,
|
|
||||||
'',
|
|
||||||
this.alertOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
import { Singleton } from "@eix/utils";
|
|
||||||
import { Store } from "../store";
|
|
||||||
import { ComponentTemplate } from "./interfaces";
|
|
||||||
import { ComponentManager } from "./componentManager";
|
|
||||||
import { success, error } from "toastr"
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
export class ComponentTemplateStore {
|
|
||||||
public store = new Store<ComponentTemplate>("componentTemplate")
|
|
||||||
|
|
||||||
public commands = {
|
|
||||||
template: (ctx: ComponentManager, args: string[], flags: string[]) => {
|
|
||||||
const command = args[0]
|
|
||||||
switch (command) {
|
|
||||||
case (undefined):
|
|
||||||
for (let i of flags) {
|
|
||||||
if (i === "--version" || i === "-v")
|
|
||||||
return success("1.0.1", "", ctx.alertOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
error(`Welcome to the component template program!
|
|
||||||
To get started, try running this basic commands:
|
|
||||||
${["--version", "ls"].map(val => `${val}`).join(" ")}
|
|
||||||
`, "", {
|
|
||||||
...ctx.alertOptions,
|
|
||||||
timeOut: 7500
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
case ("ls"):
|
|
||||||
success(`Here is a list of all the current registered component templates (including ics):
|
|
||||||
<ul>
|
|
||||||
${this.store.ls().map(val => `
|
|
||||||
<li>
|
|
||||||
${val}
|
|
||||||
</li>
|
|
||||||
`).join(" ")}
|
|
||||||
</ul>
|
|
||||||
`, "", ctx.alertOptions)
|
|
||||||
break
|
|
||||||
case ("info"):
|
|
||||||
if (!args[1])
|
|
||||||
return error("You need to specify a template name", "", ctx.alertOptions)
|
|
||||||
|
|
||||||
const data = this.store.get(args[1])
|
|
||||||
|
|
||||||
if (!data)
|
|
||||||
return error(`Component ${args[1]} doesnt exist`, "", ctx.alertOptions)
|
|
||||||
|
|
||||||
const showFunction = flags.find(value =>
|
|
||||||
value === "-sf" || value === "--showFunctions"
|
|
||||||
)
|
|
||||||
|
|
||||||
success(`
|
|
||||||
Name: ${data.name} <br>
|
|
||||||
Inputs: ${data.inputs} <br>
|
|
||||||
Outputs: ${data.outputs}
|
|
||||||
${showFunction ? `<br> Activation: ${data.activation}` : ""}
|
|
||||||
`, "", ctx.alertOptions)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
error(`${command} is not a valid command for the template program`, "", ctx.alertOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (this.store.ls().length) return
|
|
||||||
|
|
||||||
this.store.set("buffer", {
|
|
||||||
inputs: 1,
|
|
||||||
outputs: 1,
|
|
||||||
name: "buffer",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
ctx.outputs[0].value = ctx.inputs[0].value
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "color",
|
|
||||||
data: "blue"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.store.set("not", {
|
|
||||||
inputs: 1,
|
|
||||||
outputs: 1,
|
|
||||||
name: "buffer",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
ctx.outputs[0].value = !ctx.inputs[0].value
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "color",
|
|
||||||
data: "red"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.store.set("and", {
|
|
||||||
inputs: 2,
|
|
||||||
outputs: 1,
|
|
||||||
name: "and",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
ctx.outputs[0].value = ctx.inputs[0].value && ctx.inputs[1].value
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "standard_image",
|
|
||||||
data: "and"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.store.set("or", {
|
|
||||||
inputs: 2,
|
|
||||||
outputs: 1,
|
|
||||||
name: "or",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
ctx.outputs[0].value = ctx.inputs[0].value || ctx.inputs[1].value
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "standard_image",
|
|
||||||
data: "or"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.store.set("nor", {
|
|
||||||
inputs: 2,
|
|
||||||
outputs: 1,
|
|
||||||
name: "nor",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
ctx.outputs[0].value = !(ctx.inputs[0].value || ctx.inputs[1].value)
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "standard_image",
|
|
||||||
data: "nor"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.store.set("xor", {
|
|
||||||
inputs: 2,
|
|
||||||
outputs: 1,
|
|
||||||
name: "xor",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
ctx.outputs[0].value = (ctx.inputs[0].value || ctx.inputs[1].value) && !(ctx.inputs[0].value && ctx.inputs[1].value)
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "standard_image",
|
|
||||||
data: "xor"
|
|
||||||
},
|
|
||||||
editable: false
|
|
||||||
})
|
|
||||||
this.store.set("light", {
|
|
||||||
inputs: 1,
|
|
||||||
outputs: 0,
|
|
||||||
name: "light",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
if (ctx.inputs[0].value)
|
|
||||||
ctx.color("yellow")
|
|
||||||
else
|
|
||||||
ctx.color("white")
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "color",
|
|
||||||
data: "white"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.store.set("button", {
|
|
||||||
inputs: 0,
|
|
||||||
outputs: 1,
|
|
||||||
name: "button",
|
|
||||||
version: "1.0.0",
|
|
||||||
activation: `
|
|
||||||
ctx.outputs[0].value = ctx.outputs[0].memory.value
|
|
||||||
`.trim(),
|
|
||||||
material: {
|
|
||||||
mode: "color",
|
|
||||||
data: "red"
|
|
||||||
},
|
|
||||||
onclick: `
|
|
||||||
ctx.outputs[0].memory.value = !ctx.outputs[0].memory.value
|
|
||||||
if (ctx.outputs[0].memory.value)
|
|
||||||
ctx.color("#550000")
|
|
||||||
else
|
|
||||||
ctx.color("red")
|
|
||||||
`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { ComponentManager } from "./componentManager";
|
|
||||||
import { success } from "toastr"
|
|
||||||
import { saveAs } from "file-saver"
|
|
||||||
|
|
||||||
const version = "1.0.0"
|
|
||||||
|
|
||||||
export const download = (ctx: ComponentManager, args: string[], flags:string[]) => {
|
|
||||||
//important flags
|
|
||||||
for (let i of flags) {
|
|
||||||
if (i === "--version" || i === "-v")
|
|
||||||
return success(`${version}`, "", ctx.alertOptions)
|
|
||||||
else if (i === "--help" || i === "-h")
|
|
||||||
return success(`Run "download" to download the save as a json file.
|
|
||||||
Flags:<ul>
|
|
||||||
<li>-v or --version to get the version.</li>
|
|
||||||
<li>-h or --help to get help (ou are reading that right now)</li>
|
|
||||||
<li>-s or --save to automatically save before downloading</li>
|
|
||||||
</ul>`,"",ctx.alertOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = args[0]
|
|
||||||
|
|
||||||
if (command === undefined){
|
|
||||||
if (flags.includes("-s") || flags.includes("--save"))
|
|
||||||
ctx.save()
|
|
||||||
|
|
||||||
const data = JSON.stringify(ctx.state)
|
|
||||||
saveAs(new Blob([data]), `${ctx.name}.json`)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./componentManager"
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentState, materialMode } from '../component/interfaces'
|
|
||||||
import { WireState } from '../wires/interface'
|
|
||||||
|
|
||||||
export interface ManagerState {
|
|
||||||
components: ComponentState[]
|
|
||||||
scale: [number, number]
|
|
||||||
position: [number, number]
|
|
||||||
wires: WireState
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComponentTemplate {
|
|
||||||
name: string
|
|
||||||
version: string
|
|
||||||
activation: string
|
|
||||||
onclick?: string
|
|
||||||
inputs: number
|
|
||||||
outputs: number
|
|
||||||
material: {
|
|
||||||
mode: materialMode
|
|
||||||
data: string
|
|
||||||
}
|
|
||||||
editable?: boolean
|
|
||||||
imported?: boolean
|
|
||||||
importCommand?: string
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./modal"
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { TemplateResult } from "lit-html";
|
|
||||||
|
|
||||||
export interface confirmModalOptions {
|
|
||||||
title: string
|
|
||||||
content: TemplateResult
|
|
||||||
yes: string
|
|
||||||
no: string
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
import { render, html } from "lit-html"
|
|
||||||
import { confirmModalOptions } from "./interfaces";
|
|
||||||
import { fromEvent } from "rxjs";
|
|
||||||
import { MDCDialog } from '@material/dialog';
|
|
||||||
|
|
||||||
let lastId = 0
|
|
||||||
|
|
||||||
export const modal = (options: Partial<confirmModalOptions>) => new Promise((res, rej) => {
|
|
||||||
const defaultOptions = {
|
|
||||||
yes: "yes",
|
|
||||||
no: "no",
|
|
||||||
title: "modal",
|
|
||||||
content: html`Hello world!`
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = document.getElementsByClassName("ModalContainer")[0]
|
|
||||||
const { title, content, yes, no }: confirmModalOptions = { ...defaultOptions, ...options }
|
|
||||||
const id = lastId++
|
|
||||||
|
|
||||||
if (!parent)
|
|
||||||
rej(false)
|
|
||||||
|
|
||||||
const template = html`
|
|
||||||
<div class="mdc-dialog"
|
|
||||||
id="modal-${id}"
|
|
||||||
role="alertdialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="${title}"
|
|
||||||
aria-describedby="my-dialog-content">
|
|
||||||
<div class="mdc-dialog__container">
|
|
||||||
<div class="mdc-dialog__surface">
|
|
||||||
<h2 class="mdc-dialog__title" id="${title}">
|
|
||||||
${title}
|
|
||||||
</h2>
|
|
||||||
<div class="mdc-dialog__content" id="my-dialog-content">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
<footer class="mdc-dialog__actions">
|
|
||||||
${(no !== "") ?
|
|
||||||
html`<button type="button" class="mdc-button mdc-dialog__button" id="no-${id}">
|
|
||||||
<span class="mdc-button__label">${no}</span>
|
|
||||||
</button>` : no
|
|
||||||
}
|
|
||||||
${(yes !== "") ?
|
|
||||||
html`<button type="button" class="mdc-button mdc-dialog__button" id="yes-${id}">
|
|
||||||
<span class="mdc-button__label">${yes}</span>
|
|
||||||
</button>` : yes
|
|
||||||
}
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mdc-dialog__scrim"></div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
render(template, parent)
|
|
||||||
|
|
||||||
const dialog = new MDCDialog(document.querySelector(`#modal-${id}`))
|
|
||||||
dialog.open()
|
|
||||||
|
|
||||||
const _yes = document.getElementById(`yes-${id}`)
|
|
||||||
const _no = document.getElementById(`no-${id}`)
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
// render(html``,parent)
|
|
||||||
dialog.close()
|
|
||||||
subscriptions.forEach(val => val.unsubscribe())
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscriptions = [
|
|
||||||
fromEvent(_yes, "click").subscribe(val => {
|
|
||||||
clear()
|
|
||||||
res(true)
|
|
||||||
}),
|
|
||||||
fromEvent(_no, "click").subscribe(val => {
|
|
||||||
clear()
|
|
||||||
res(false)
|
|
||||||
})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./pin"
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { BehaviorSubject, Subject, Subscription } from "rxjs";
|
|
||||||
import { map } from "rxjs/operators"
|
|
||||||
import clamp from "../clamp/clamp";
|
|
||||||
import { Component } from "../component";
|
|
||||||
|
|
||||||
export class Pin {
|
|
||||||
private static lastId = 0
|
|
||||||
|
|
||||||
public pairs: Pin[] = []
|
|
||||||
private subscriptions: {
|
|
||||||
subscription: Subscription
|
|
||||||
key: Pin
|
|
||||||
}[] = []
|
|
||||||
|
|
||||||
public id: number
|
|
||||||
public _value = 0
|
|
||||||
public color = new BehaviorSubject<[number, number, number, number]>([0, 0, 0, 0])
|
|
||||||
public memory: any = {}
|
|
||||||
public valueChanges = new Subject<number>()
|
|
||||||
|
|
||||||
public svgColor = this.color.pipe(map(val =>
|
|
||||||
`rgba(${val.join(",")})`
|
|
||||||
))
|
|
||||||
|
|
||||||
constructor(public allowWrite = true, public of: Component) {
|
|
||||||
this.setValue(0)
|
|
||||||
this.id = Pin.lastId++
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
return this._value
|
|
||||||
}
|
|
||||||
|
|
||||||
set value(value: number) {
|
|
||||||
if (!this.allowWrite) return
|
|
||||||
this.setValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
public setValue(value: number) {
|
|
||||||
this._value = clamp(value, 0, 1)
|
|
||||||
this.valueChanges.next(this._value)
|
|
||||||
|
|
||||||
const color: [number, number, number, number] = (value > 0.5) ?
|
|
||||||
[255, 216, 20, 1] :
|
|
||||||
[90, 90, 90, 1]
|
|
||||||
|
|
||||||
this.color.next((this.pairs.length) ? color : [0, 0, 0, 0])
|
|
||||||
}
|
|
||||||
|
|
||||||
public bindTo(pin: Pin) {
|
|
||||||
this.pairs.push(pin)
|
|
||||||
const subscription = pin.valueChanges.subscribe(val => this.setValue(val))
|
|
||||||
|
|
||||||
this.subscriptions.push({
|
|
||||||
subscription,
|
|
||||||
key: pin
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public unbind(pin: Pin) {
|
|
||||||
if (this.pairs.includes(pin)) {
|
|
||||||
this.pairs = this.pairs.filter(val => val !== pin)
|
|
||||||
this.subscriptions.filter(val => val.key === pin).forEach(({ subscription }) => subscription.unsubscribe())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public update() {
|
|
||||||
this.setValue(this._value)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./screen"
|
|
|
@ -1,90 +0,0 @@
|
||||||
import { fromEvent, BehaviorSubject, combineLatest } from "rxjs"
|
|
||||||
import { Singleton } from "@eix/utils"
|
|
||||||
import { map, take } from "rxjs/operators";
|
|
||||||
import clamp from "../clamp/clamp";
|
|
||||||
import { manager } from "../../main";
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
export class Screen {
|
|
||||||
width = new BehaviorSubject<number>(0)
|
|
||||||
height = new BehaviorSubject<number>(0)
|
|
||||||
viewBox = combineLatest(this.width, this.height).pipe(map((values: [number, number]) =>
|
|
||||||
this.getViewBox(...values)
|
|
||||||
));
|
|
||||||
|
|
||||||
public position = [0, 0]
|
|
||||||
public scale = [2, 2]
|
|
||||||
|
|
||||||
private zoomLimits: [number, number] = [0.1, 10]
|
|
||||||
|
|
||||||
private scrollStep = 1.3
|
|
||||||
public mousePosition = [this.width.value / 2, this.height.value / 2]
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.update()
|
|
||||||
|
|
||||||
fromEvent(window, "resize").subscribe(() => this.update())
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMouse(e: MouseEvent) {
|
|
||||||
this.mousePosition = [e.clientX, e.clientY]
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScroll(e: WheelEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const componentToScale = manager.scaling
|
|
||||||
const sign = e.deltaY / Math.abs(e.deltaY)
|
|
||||||
const zoom = this.scrollStep ** sign
|
|
||||||
|
|
||||||
if (componentToScale) {
|
|
||||||
const oldScale = componentToScale.scale.value
|
|
||||||
const newScale = oldScale.map(val => val / zoom)
|
|
||||||
|
|
||||||
componentToScale.scale.next(newScale)
|
|
||||||
componentToScale.position.pipe(take(1)).subscribe(data => {
|
|
||||||
componentToScale.position.next(data.map((val, index) =>
|
|
||||||
val - (newScale[index] - oldScale[index]) / 2
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
manager.top(componentToScale)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const size = [this.width.value, this.height.value]
|
|
||||||
const mouseFraction = size.map((value, index) => this.mousePosition[index] / value)
|
|
||||||
const newScale = this.scale.map(value => clamp(value * zoom, ...this.zoomLimits))
|
|
||||||
const delta = this.scale.map((_, index) =>
|
|
||||||
size[index] * (newScale[index] - this.scale[index]) * mouseFraction[index]
|
|
||||||
)
|
|
||||||
|
|
||||||
this.scale = newScale
|
|
||||||
this.position = this.position.map((value, index) => value - delta[index])
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
move(x: number, y: number) {
|
|
||||||
this.position[0] += x * this.scale[0]
|
|
||||||
this.position[1] += y * this.scale[1]
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
getViewBox(width: number, height: number) {
|
|
||||||
return [
|
|
||||||
this.position[0],
|
|
||||||
this.position[1],
|
|
||||||
this.scale[0] * width,
|
|
||||||
this.scale[1] * height
|
|
||||||
].join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.height.next(window.innerHeight)
|
|
||||||
this.width.next(window.innerWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
getWorldPosition(x: number, y: number) {
|
|
||||||
return [x * this.scale[0], y * this.scale[1]]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./store"
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { Store } from "./store";
|
|
||||||
|
|
||||||
export const persistent = <T,K>(_default:K,storeKey = "main") => (target:T, key: keyof T & string) => {
|
|
||||||
let secret: K
|
|
||||||
const store = new Store<K>(key)
|
|
||||||
if (store.get(storeKey))
|
|
||||||
secret = store.get(storeKey)
|
|
||||||
else
|
|
||||||
secret = _default
|
|
||||||
|
|
||||||
Object.defineProperty(target,key,{
|
|
||||||
get() {
|
|
||||||
return secret
|
|
||||||
},
|
|
||||||
set(value:K) {
|
|
||||||
secret = value
|
|
||||||
store.set(storeKey, secret)
|
|
||||||
},
|
|
||||||
enumerable: true
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { Singleton } from "@eix/utils";
|
|
||||||
import { ComponentManager } from "../componentManager";
|
|
||||||
import { success, error } from "toastr"
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
export class Settings {
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
jumpToNewSimulations: true
|
|
||||||
}
|
|
||||||
|
|
||||||
commands = (ctx: ComponentManager, args: string[], flags: string[]) => {
|
|
||||||
//important flags
|
|
||||||
for (let i of flags) {
|
|
||||||
if (i === "--version" || i === "-v")
|
|
||||||
return success(`${this.version}`, "", ctx.alertOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = args[0]
|
|
||||||
|
|
||||||
if (command === undefined)
|
|
||||||
return success(
|
|
||||||
`Welcome to the settings cli. You can use this to tweak settings in any way imaginable!`,
|
|
||||||
"",
|
|
||||||
ctx.alertOptions)
|
|
||||||
|
|
||||||
//nothing here
|
|
||||||
error(`Commands ${args} couldnt be found`,"",ctx.alertOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
export class Store<T> {
|
|
||||||
public lsChanges = new BehaviorSubject<string[]>([])
|
|
||||||
|
|
||||||
constructor(private name: string){
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
update(){
|
|
||||||
this.lsChanges.next(this.ls())
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key:string):T{
|
|
||||||
const data = localStorage[`${this.name}/${key}`]
|
|
||||||
|
|
||||||
if(data)
|
|
||||||
return JSON.parse(data).value
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(key:string){
|
|
||||||
localStorage.removeItem(`${this.name}/${key}`)
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key:string,value:T){
|
|
||||||
localStorage[`${this.name}/${key}`] = JSON.stringify({ value })
|
|
||||||
|
|
||||||
this.update()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
ls() {
|
|
||||||
let keys = []
|
|
||||||
|
|
||||||
for (const i in localStorage){
|
|
||||||
if (i.indexOf(this.name) == 0)
|
|
||||||
keys.push(i.substr(this.name.length + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./wireManager"
|
|
|
@ -1,12 +0,0 @@
|
||||||
export interface WireStateVal {
|
|
||||||
from: {
|
|
||||||
owner: number
|
|
||||||
index: number
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
owner: number
|
|
||||||
index: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WireState = WireStateVal[]
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Pin } from "../pin";
|
|
||||||
|
|
||||||
export class Wire {
|
|
||||||
constructor (public input:Pin,public output:Pin){
|
|
||||||
this.output.bindTo(this.input)
|
|
||||||
this.input.pairs.push(this.output)
|
|
||||||
this.input.update()
|
|
||||||
this.output.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(){
|
|
||||||
this.output.setValue(0)
|
|
||||||
this.output.unbind(this.input)
|
|
||||||
this.input.unbind(this.output)
|
|
||||||
this.input.update()
|
|
||||||
this.output.update()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
import { Singleton } from '@eix/utils'
|
|
||||||
import { Pin } from '../pin'
|
|
||||||
import { Wire } from './wire'
|
|
||||||
import { svg } from 'lit-html'
|
|
||||||
import { subscribe } from 'lit-rx'
|
|
||||||
import { Subject, combineLatest } from 'rxjs'
|
|
||||||
import { WireStateVal } from './interface'
|
|
||||||
import { merge, map } from 'rxjs/operators'
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
export class WireManager {
|
|
||||||
public start: Pin
|
|
||||||
public end: Pin
|
|
||||||
|
|
||||||
public wires: Wire[] = []
|
|
||||||
|
|
||||||
public update = new Subject<boolean>()
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
public add(data: Pin) {
|
|
||||||
if (data.allowWrite)
|
|
||||||
//output
|
|
||||||
this.start = data
|
|
||||||
else this.end = data
|
|
||||||
|
|
||||||
this.tryResolving()
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose() {
|
|
||||||
for (let i of this.wires) i.dispose()
|
|
||||||
|
|
||||||
this.wires = []
|
|
||||||
}
|
|
||||||
|
|
||||||
public tryResolving() {
|
|
||||||
if (this.start && this.end && this.start != this.end) {
|
|
||||||
if (this.canBind(this.end)) {
|
|
||||||
this.wires.push(new Wire(this.start, this.end))
|
|
||||||
this.start = null
|
|
||||||
this.end = null
|
|
||||||
this.update.next(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private canBind(end: Pin) {
|
|
||||||
if (this.wires.find(val => val.output === end)) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public remove(target: Wire) {
|
|
||||||
target.dispose()
|
|
||||||
this.wires = this.wires.filter(val => val !== target)
|
|
||||||
this.update.next(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
get svg() {
|
|
||||||
return svg`${this.wires.map(val => {
|
|
||||||
const i = val.input.of
|
|
||||||
const o = val.output.of
|
|
||||||
const inputIndex = i.outputPins.indexOf(val.input)
|
|
||||||
const inputY = i.piny(false, inputIndex)
|
|
||||||
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`
|
|
||||||
<path d=${subscribe(d)}
|
|
||||||
stroke=${subscribe(val.input.svgColor)}
|
|
||||||
stroke-width=10
|
|
||||||
fill="rgba(0,0,0,0)"
|
|
||||||
@click=${() => this.remove(val)} />
|
|
||||||
`
|
|
||||||
})}`
|
|
||||||
}
|
|
||||||
|
|
||||||
get state() {
|
|
||||||
return this.wires.map(
|
|
||||||
(val): WireStateVal => ({
|
|
||||||
from: {
|
|
||||||
owner: val.input.of.id,
|
|
||||||
index: val.input.of.outputPins.indexOf(val.input)
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
owner: val.output.of.id,
|
|
||||||
index: val.output.of.inputPins.indexOf(val.output)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
308
src/ts/main.ts
308
src/ts/main.ts
|
@ -1,308 +0,0 @@
|
||||||
import { render, html } from 'lit-html'
|
|
||||||
import { subscribe } from 'lit-rx'
|
|
||||||
import { Screen } from './common/screen.ts'
|
|
||||||
import { ComponentManager } from './common/componentManager'
|
|
||||||
import { map } from 'rxjs/operators'
|
|
||||||
import { MDCMenu } from '@material/menu'
|
|
||||||
import { error } from 'toastr'
|
|
||||||
import { modal } from './common/modals'
|
|
||||||
import { importComponent } from './common/componentImporter/importComponent'
|
|
||||||
|
|
||||||
const screen = new Screen()
|
|
||||||
|
|
||||||
export const manager = new ComponentManager()
|
|
||||||
manager.save()
|
|
||||||
manager.update()
|
|
||||||
|
|
||||||
window.onerror = (
|
|
||||||
message: string,
|
|
||||||
url: string,
|
|
||||||
lineNumber: number
|
|
||||||
): boolean => {
|
|
||||||
error(message, '', {
|
|
||||||
...manager.alertOptions,
|
|
||||||
onclick: () =>
|
|
||||||
modal({
|
|
||||||
no: '',
|
|
||||||
yes: 'close',
|
|
||||||
title: 'Error',
|
|
||||||
content: html`
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Url:</td>
|
|
||||||
<td>${url}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Message:</td>
|
|
||||||
<td>${message}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Line:</td>
|
|
||||||
<td>${lineNumber}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
manager.handleMouseMove(e)
|
|
||||||
screen.updateMouse(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
render(
|
|
||||||
html`
|
|
||||||
<div @mousemove=${moveHandler}
|
|
||||||
@touchmove=${moveHandler}
|
|
||||||
@mousedown=${(e: MouseEvent) =>
|
|
||||||
handleEvent(e, () => manager.handleMouseDown())}
|
|
||||||
@touchdown=${(e: MouseEvent) =>
|
|
||||||
handleEvent(e, () => manager.handleMouseDown())}
|
|
||||||
@mouseup=${(e: MouseEvent) =>
|
|
||||||
handleEvent(e, () => manager.handleMouseUp())}
|
|
||||||
@touchup=${(e: MouseEvent) =>
|
|
||||||
handleEvent(e, () => manager.handleMouseUp())}
|
|
||||||
@wheel=${(e: MouseEvent) =>
|
|
||||||
handleEvent(e, (e: WheelEvent) => screen.handleScroll(e))}>
|
|
||||||
|
|
||||||
<div id=${subscribe(
|
|
||||||
manager.barAlpha.pipe(map(val => (val == '1' ? 'shown' : '')))
|
|
||||||
)}
|
|
||||||
class=createBar>
|
|
||||||
<div class="topContainer">
|
|
||||||
<div>
|
|
||||||
<input name="ComponentName" id="nameInput"
|
|
||||||
placeholder=${subscribe(manager.placeholder)}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<svg height=${subscribe(screen.height)}
|
|
||||||
width=${subscribe(screen.width)}
|
|
||||||
viewBox=${subscribe(screen.viewBox)}>
|
|
||||||
${subscribe(manager.svgs)}
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ModalContainer"></div>
|
|
||||||
<aside class="mdc-drawer main-sidebar">
|
|
||||||
<div class="mdc-drawer__content">
|
|
||||||
<nav class="mdc-list">
|
|
||||||
<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>
|
|
||||||
<span class="mdc-list-item__text">Create new simulation</span>
|
|
||||||
</a>
|
|
||||||
<a class="mdc-list-item" href="#" id="openSimulation" @click=${() => {
|
|
||||||
menus[0].open = true
|
|
||||||
}}>
|
|
||||||
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">folder_open</i>
|
|
||||||
<span class="mdc-list-item__text">Open simulation</span>
|
|
||||||
</a>
|
|
||||||
<a class="mdc-list-item" href="#" id="openFile" @click=${() => {
|
|
||||||
menus[2].open = true
|
|
||||||
}}>
|
|
||||||
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">insert_drive_file</i>
|
|
||||||
<span class="mdc-list-item__text">Simulation</span>
|
|
||||||
</a>
|
|
||||||
<a class="mdc-list-item" href="#" id="openCustomGates" @click=${() => {
|
|
||||||
menus[3].open = true
|
|
||||||
}}>
|
|
||||||
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">edit</i>
|
|
||||||
<span class="mdc-list-item__text">Custom gates</span>
|
|
||||||
</a>
|
|
||||||
<a class="mdc-list-item" href="#" id="openGates" @click=${() => {
|
|
||||||
menus[1].open = true
|
|
||||||
}}>
|
|
||||||
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i>
|
|
||||||
<span class="mdc-list-item__text">Add component</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
${subscribe(
|
|
||||||
manager.saves.pipe(
|
|
||||||
map(_ =>
|
|
||||||
_.map(
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
${subscribe(
|
|
||||||
manager.gates.pipe(
|
|
||||||
map(gates =>
|
|
||||||
[...gates].sort().map(name => {
|
|
||||||
const gate = manager.templateStore.store.get(name)
|
|
||||||
return html`
|
|
||||||
<li
|
|
||||||
class="mdc-list-item"
|
|
||||||
role="menuitem"
|
|
||||||
@click=${() => manager.add(name)}
|
|
||||||
>
|
|
||||||
<span class="mdc-list-item__text">
|
|
||||||
${name}
|
|
||||||
</span>
|
|
||||||
${gate.imported || gate.editable
|
|
||||||
? html`
|
|
||||||
     
|
|
||||||
<span
|
|
||||||
class="material-icons mdc-list-item__meta"
|
|
||||||
@click=${(e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
manager.templateStore.store.delete(
|
|
||||||
name
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
delete
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
${[...Object.keys(manager.file)].sort().map(
|
|
||||||
key => html`
|
|
||||||
<li
|
|
||||||
class="mdc-list-item"
|
|
||||||
role="menuitem"
|
|
||||||
@click=${() => manager.file[key]()}
|
|
||||||
>
|
|
||||||
<span class="mdc-list-item__text">${key}</span>
|
|
||||||
${manager.shortcuts[key]
|
|
||||||
? html`
|
|
||||||
<span class="mdc-list-item__meta"
|
|
||||||
>     
|
|
||||||
${manager.shortcuts[key]}</span
|
|
||||||
>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
${subscribe(
|
|
||||||
manager.gates.pipe(
|
|
||||||
map(gates =>
|
|
||||||
gates
|
|
||||||
.map(name => manager.templateStore.store.get(name))
|
|
||||||
.filter(gate => gate.editable || gate.imported)
|
|
||||||
.map(
|
|
||||||
gate => html`
|
|
||||||
<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>
|
|
||||||
<span class="mdc-list-item__text"> New custom gate </span>
|
|
||||||
</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">      ctrl + g</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
document.body
|
|
||||||
)
|
|
||||||
|
|
||||||
const menus = [
|
|
||||||
new MDCMenu(document.querySelector('#saveMenu')),
|
|
||||||
new MDCMenu(document.querySelector('#gateMenu')),
|
|
||||||
new MDCMenu(document.querySelector('#fileMenu')),
|
|
||||||
new MDCMenu(document.querySelector('#customGateMenu'))
|
|
||||||
]
|
|
||||||
menus.forEach(menu => menu.hoistMenuToBody())
|
|
||||||
menus[0].setAnchorElement(document.querySelector(`#openSimulation`))
|
|
||||||
menus[1].setAnchorElement(document.querySelector('#openGates'))
|
|
||||||
menus[2].setAnchorElement(document.querySelector('#openFile'))
|
|
||||||
menus[3].setAnchorElement(document.querySelector('#openCustomGates'))
|
|
||||||
|
|
||||||
manager.update()
|
|
|
@ -1,32 +1,13 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"moduleResolution": "node",
|
||||||
"paths": {
|
"esModuleInterop": true,
|
||||||
"*": [
|
"jsx": "preserve",
|
||||||
"types/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"module": "esnext",
|
|
||||||
"target": "esnext",
|
|
||||||
"removeComments": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"lib": [
|
|
||||||
"es2015",
|
|
||||||
"es2017",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"alwaysStrict": true,
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"esModuleInterop": true
|
"target": "esnext",
|
||||||
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": [
|
"exclude": ["node_modules"],
|
||||||
"src/**/*.ts",
|
"include": ["src"]
|
||||||
"deploy.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist"
|
|
||||||
]
|
|
||||||
}
|
}
|
9
types/random-emoji.d.ts
vendored
9
types/random-emoji.d.ts
vendored
|
@ -1,9 +0,0 @@
|
||||||
export function haiku(options: any): any;
|
|
||||||
export function random(options: {
|
|
||||||
count: number
|
|
||||||
}): {
|
|
||||||
character: string,
|
|
||||||
name: string,
|
|
||||||
image: string,
|
|
||||||
imageSrc: string
|
|
||||||
}[];
|
|
|
@ -1,82 +1,114 @@
|
||||||
const HtmlWebPackPlugin = require("html-webpack-plugin");
|
const { resolve } = require('path')
|
||||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
const TerserPlugin = require('terser-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')
|
||||||
|
|
||||||
module.exports = {
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
devtool: 'inline-source-map',
|
|
||||||
module: {
|
const projectRoot = resolve(__dirname)
|
||||||
rules: [
|
const sourceFolder = resolve(projectRoot, 'src')
|
||||||
{
|
const buildFolder = resolve(projectRoot, 'dist')
|
||||||
test: /\.ts$/,
|
const htmlTemplateFile = resolve(sourceFolder, 'index.html')
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/
|
const babelRule = {
|
||||||
},
|
test: /\.(js|tsx?)$/,
|
||||||
{
|
use: 'babel-loader'
|
||||||
test: /\.html$/,
|
}
|
||||||
use: [
|
|
||||||
{
|
const sassRule = {
|
||||||
loader: "html-loader"
|
test: /\.scss$/,
|
||||||
}
|
use: [
|
||||||
]
|
isProduction
|
||||||
},
|
? MiniCssExtractPlugin.loader
|
||||||
{
|
: {
|
||||||
test: /\.(png|jpg|mp3|wav)$/,
|
loader: 'style-loader',
|
||||||
use: [
|
options: {
|
||||||
{
|
singleton: true
|
||||||
loader: 'file-loader',
|
}
|
||||||
options: {}
|
},
|
||||||
}
|
{ loader: 'css-loader' },
|
||||||
]
|
{
|
||||||
},
|
loader: 'sass-loader',
|
||||||
{
|
options: {
|
||||||
test: /\.scss$/,
|
includePaths: [sourceFolder]
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: 'style.css',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ loader: 'extract-loader' },
|
|
||||||
{ loader: 'css-loader' },
|
|
||||||
{
|
|
||||||
loader: 'sass-loader',
|
|
||||||
options: {
|
|
||||||
includePaths: ['./node_modules']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(eot|svg|ttf|woff|woff2)$/,
|
|
||||||
use: {
|
|
||||||
loader: 'file-loader?name=./res/fonts/[name].[ext]'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseConfig = {
|
||||||
|
mode: 'none',
|
||||||
|
entry: ['babel-regenerator-runtime', resolve(sourceFolder, 'main')],
|
||||||
|
output: {
|
||||||
|
filename: 'js/[name].js',
|
||||||
|
path: buildFolder,
|
||||||
|
publicPath: '/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [babelRule, sassRule]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx', '.scss']
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
}
|
||||||
|
|
||||||
|
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: [
|
plugins: [
|
||||||
new HtmlWebPackPlugin({
|
new MiniCssExtractPlugin({
|
||||||
template: "./src/index.html",
|
filename: 'css/[name].min.css'
|
||||||
filename: "./index.html"
|
|
||||||
}),
|
}),
|
||||||
new ExtractTextPlugin(
|
new OptimizeCssAssetsWebpackPlugin(),
|
||||||
{
|
new HtmlWebpackPlugin({
|
||||||
filename: 'style.css',
|
template: htmlTemplateFile,
|
||||||
allChunks: true
|
minify: {
|
||||||
}
|
removeComments: true,
|
||||||
)
|
collapseWhitespace: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
keepClosingSlash: true,
|
||||||
|
minifyJS: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyURLs: true
|
||||||
|
},
|
||||||
|
inject: true
|
||||||
|
}),
|
||||||
|
new HtmlWebpackInlineSourcePlugin()
|
||||||
],
|
],
|
||||||
resolve: {
|
devtool: 'source-map'
|
||||||
extensions: [
|
}
|
||||||
".js",
|
|
||||||
".ts"
|
function getFinalConfig() {
|
||||||
]
|
if (process.env.NODE_ENV === 'production') {
|
||||||
},
|
console.info('Running production config')
|
||||||
entry: [
|
return webpackMerge(baseConfig, prodConfig)
|
||||||
"./src/index.ts"
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
minimizer: [new TerserPlugin()],
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
console.info('Running development config')
|
||||||
|
return webpackMerge(baseConfig, devConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getFinalConfig()
|
||||||
|
|
Loading…
Reference in a new issue