This commit is contained in:
Matei Adriel 2019-07-23 22:53:59 +03:00
parent 639f4b5aa0
commit fccc1922fb
25 changed files with 425 additions and 70 deletions

36
package-lock.json generated
View file

@ -2550,8 +2550,7 @@
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"buffer-indexof": {
"version": "1.1.1",
@ -3202,6 +3201,31 @@
"sha.js": "^2.4.8"
}
},
"cross-env": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz",
"integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==",
"dev": true,
"requires": {
"cross-spawn": "^6.0.5",
"is-windows": "^1.0.0"
},
"dependencies": {
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"dev": true,
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
}
}
},
"cross-spawn": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
@ -9469,8 +9493,7 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-resolve": {
"version": "0.5.2",
@ -9489,7 +9512,6 @@
"version": "0.5.12",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz",
"integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -9929,7 +9951,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.1.2.tgz",
"integrity": "sha512-jvNoEQSPXJdssFwqPSgWjsOrb+ELoE+ILpHPKXC83tIxOlh2U75F1KuB2luLD/3a6/7K3Vw5pDn+hvu0C4AzSw==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
@ -9939,8 +9960,7 @@
"commander": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
"dev": true
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
}
}
},

View file

@ -23,6 +23,7 @@
"babel-loader": "^8.0.6",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-regenerator-runtime": "^6.5.0",
"cross-env": "^5.2.0",
"css-loader": "^3.0.0",
"file-loader": "^4.1.0",
"html-webpack-inline-source-plugin": "0.0.10",
@ -50,6 +51,7 @@
"react-router-dom": "^5.0.1",
"react-toastify": "^5.3.2",
"rxjs": "^6.5.2",
"rxjs-hooks": "^0.5.1"
"rxjs-hooks": "^0.5.1",
"terser": "^4.1.2"
}
}

View file

@ -0,0 +1,15 @@
/**
* A type-safe querySelector function, which throws if the given element was not found
*
* @credit https://gitlab.com/wavedistrict/web-client/blob/master/src/common/dom/helpers/querySelector.ts
*/
export function querySelector<E extends Element>(
selector: string,
parent: Element = document.body
) {
const element = parent.querySelector(selector)
if (!element) {
throw `Could not find element with selector "${selector}"`
}
return element as E
}

View file

@ -0,0 +1,21 @@
export const getSafeErrorStack = (error: any) => {
const errorString: string = error.toString()
const stackString: string = error.stack
if (stackString) {
const safeStackString =
stackString.replace(errorString + '\n', '') || stackString
const stackItems = safeStackString.split('\n')
const safeStackItems = stackItems
.map(item => item.replace(' at ', ''))
.filter(item => item !== '')
.map(item => ` at ${item}`)
const safeStack = safeStackItems.join('\n')
return `${errorString}\n${safeStack}`
}
return errorString
}

View file

@ -28,5 +28,14 @@
oncontextmenu="return false"
>
<div id="app"></div>
<div class="Splash">
<div class="loading">
<div class="lds-ripple">
<div></div>
<div></div>
</div>
</div>
<noscript> JavaScript must be enabled to run this app. </noscript>
</div>
</body>
</html>

27
src/index.ts Normal file
View file

@ -0,0 +1,27 @@
import { Splash } from './modules/splash/classes/Splash'
async function main() {
let splash: Splash | undefined = undefined
try {
splash = new Splash()
} catch {}
try {
const app = await import('./main')
await app.start()
} catch (error) {
if (splash) splash.setError(error)
console.error(error.stack || error)
return
}
if (splash) {
splash.fade()
}
}
main().catch(error => {
console.error('Error loading app', error)
})

View file

@ -5,11 +5,19 @@ import { render } from 'react-dom'
import { handleErrors } from './modules/errors/helpers/handleErrors'
import { initKeyBindings } from './modules/keybindings/helpers/initialiseKeyBindings'
import { initBaseTemplates } from './modules/saving/helpers/initBaseTemplates'
import { loadSubject } from './modules/core/subjects/loadedSubject'
import { take } from 'rxjs/operators'
console.clear()
export const start = async () => {
console.clear()
handleErrors()
initKeyBindings()
initBaseTemplates()
const result = loadSubject.pipe(take(1)).toPromise()
render(<App />, document.getElementById('app'))
handleErrors()
initKeyBindings()
initBaseTemplates()
render(<App />, document.getElementById('app'))
await result
}

View file

@ -1,8 +1,16 @@
/**
* Transforms js code into a function
*
* @param source tThe js code
* @param args The name of arguments to pass to the function
*/
export const toFunction = <T extends unknown[]>(
source: string,
...args: string[]
): ((...args: T) => void) => {
return new Function(`return (${args.join(',')}) => {
const raw = `return (${args.join(',')}) => {
${source}
}`)()
}`
return new Function(raw)()
}

View file

@ -5,6 +5,7 @@ import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationR
import { renderSimulation } from '../../simulationRenderer/helpers/renderSimulation'
import { updateSimulation } from '../../simulationRenderer/helpers/updateSimulation'
import { rendererSubject } from '../subjects/rendererSubject'
import { loadSubject } from '../subjects/loadedSubject'
class Canvas extends Component {
private canvasRef: RefObject<HTMLCanvasElement> = createRef()
@ -24,6 +25,8 @@ class Canvas extends Component {
}
public componentDidMount() {
loadSubject.next(true)
if (this.canvasRef.current) {
this.renderingContext = this.canvasRef.current.getContext('2d')
this.renderer.updateWheelListener()

View file

@ -0,0 +1,3 @@
import { Subject } from 'rxjs'
export const loadSubject = new Subject<true>()

View file

@ -5,8 +5,10 @@ import {
simulationInputCount,
simulationOutputCount
} from './simulationIoCount'
import { InitialisationContext } from '../../activation/types/Context'
import { templateStore } from '../../saving/stores/templateStore'
import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments'
import { CurrentLanguage } from '../../internalisation/stores/currentLanguage'
/**
* Compiles a simulation into a logicGate
@ -18,6 +20,7 @@ export const compileIc = ({ mode, name, gates }: SimulationState) => {
throw new SimulationError('Cannot compile project')
}
const translation = CurrentLanguage.getTranslation()
const inputCount = simulationInputCount(gates)
const outputCount = simulationOutputCount(gates)
@ -37,4 +40,10 @@ export const compileIc = ({ mode, name, gates }: SimulationState) => {
}
templateStore.set(name, result)
toast(
...createToastArguments(
translation.messages.compiledIc(name),
'markunread_mailbox'
)
)
}

View file

@ -27,6 +27,7 @@ export const EnglishTranslation: Translation = {
createdSimulation: name => `Succesfully created simulation '${name}'`,
switchedToSimulation: name =>
`Succesfully switched to simulation '${name}'`,
savedSimulation: name => `Succesfully saved simulation '${name}'`
savedSimulation: name => `Succesfully saved simulation '${name}'`,
compiledIc: name => `Succesfully compiled circuit '${name}'`
}
}

View file

@ -28,6 +28,7 @@ export const DutchTranslation: Translation = {
createdSimulation: name => `Simulatie '${name}' succesvol gecreerd`,
switchedToSimulation: name =>
`Succesvol veranderd naar simulatie '${name}'`,
savedSimulation: name => `Simulatie succesvol opgeslagen '${name}'`
savedSimulation: name => `Simulatie succesvol opgeslagen '${name}'`,
compiledIc: name => `Todo: ${name}`
}
}

View file

@ -28,6 +28,7 @@ export const RomanianTranslation: Translation = {
`Simulația '${name}' a fost creeată cu succes`,
switchedToSimulation: name =>
`Simulația '${name}' a fost deschisă cu succes`,
savedSimulation: name => `Simulația '${name}' a fost salvată cu succes`
savedSimulation: name => `Simulația '${name}' a fost salvată cu succes`,
compiledIc: name => `Simulația '${name}' a fost compilată cu succes`
}
}

View file

@ -28,5 +28,6 @@ export interface Translation {
createdSimulation: NameSentence
switchedToSimulation: NameSentence
savedSimulation: NameSentence
compiledIc: NameSentence
}
}

View file

@ -16,6 +16,8 @@ $item-color: $grey;
@include modal-container();
justify-content: center;
overflow-y: auto;
height: 100%;
}
.visible#logic-gate-modal-container {
@ -42,7 +44,7 @@ $item-color: $grey;
}
#logic-gate-modal-container > .logic-gate-item > * {
font-size: 3em;
font-size: 2.5em;
}
#logic-gate-modal-container > .logic-gate-item > .logic-gate-item-type {
@ -60,3 +62,9 @@ $item-color: $grey;
#logic-gate-modal-container > .logic-gate-item > .logic-gate-item-name {
flex-grow: 1;
}
#logic-gate-modal-container > .logic-gate-item:first-child {
// height: 4em;
opacity: 0;
}

View file

@ -6,12 +6,10 @@ import { LogicGateList } from '../subjects/LogicGateList'
import Icon from '@material-ui/core/Icon'
import Typography from '@material-ui/core/Typography'
import { addGate } from '../../simulation/helpers/addGate'
import { rendererSubject } from '../../core/subjects/rendererSubject'
import { SimulationError } from '../../errors/classes/SimulationError'
import { templateStore } from '../../saving/stores/templateStore'
import { randomItem } from '../../internalisation/helpers/randomItem'
import { completeTemplate } from '../helpers/completeTemplate'
import { gateIcons } from '../constants'
import { getTemplateSafely } from '../helpers/getTemplateSafely'
import { getRendererSafely } from '../helpers/getRendererSafely'
/**
* Subject containing the open state of the modal
@ -31,6 +29,7 @@ export const handleClose = () => {
const LogicGateModal = () => {
const openSnapshot = useObservable(() => open, false)
const gates = useObservable(() => LogicGateList, [])
const renderer = getRendererSafely()
return (
<div
@ -38,36 +37,33 @@ const LogicGateModal = () => {
id="logic-gate-modal-container"
onClick={handleClose}
>
{gates.map((gate, index) => {
const renderer = rendererSubject.value
if (!renderer) {
throw new SimulationError(`Renderer not found`)
}
const template = completeTemplate(templateStore.get(gate) || {})
if (!template) {
throw new SimulationError(
`Template ${gate} cannot be found`
<div className="logic-gate-item">---</div>
{gates
.map(getTemplateSafely)
.filter(template => {
return (
renderer.simulation.mode === 'project' ||
template.metadata.name !== renderer.simulation.name
)
}
})
.map((template, index) => {
const { name } = template.metadata
return (
<div
key={index}
className="logic-gate-item"
onClick={e => {
addGate(renderer.simulation, gate)
onClick={() => {
addGate(renderer.simulation, name)
}}
>
<Icon className="lgi-icon logic-gate-item-type">
{gateIcons[template.tags[0]]}
</Icon>
<Typography className="logic-gate-item-name">
{gate}
{name}
</Typography>
{template.info.length && (
{template.info.length ? (
<a
target="_blank"
className="logic-gate-item-info"
@ -79,6 +75,8 @@ const LogicGateModal = () => {
>
<Icon className="lgi-icon">info</Icon>
</a>
) : (
''
)}
</div>
)

View file

@ -0,0 +1,17 @@
import { rendererSubject } from '../../core/subjects/rendererSubject'
import { SimulationError } from '../../errors/classes/SimulationError'
/**
* Gets the current simulation renderer
*
* @throws SimulationError no renderer was found
*/
export const getRendererSafely = () => {
const renderer = rendererSubject.value
if (!renderer) {
throw new SimulationError(`Renderer not found`)
}
return renderer
}

View file

@ -0,0 +1,20 @@
import { templateStore } from '../../saving/stores/templateStore'
import { SimulationError } from '../../errors/classes/SimulationError'
import { completeTemplate } from './completeTemplate'
/**
* Gets a gate template from localStorage
*
* @param name - The name of the template
*
* @throws SimulationError if the template cant be found
*/
export const getTemplateSafely = (name: string) => {
const template = completeTemplate(templateStore.get(name) || {})
if (!template) {
throw new SimulationError(`Template ${name} cannot be found`)
}
return template
}

View file

@ -5,6 +5,7 @@ import { saveStore } from '../stores/saveStore'
import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments'
import { CurrentLanguage } from '../../internalisation/stores/currentLanguage'
import { compileIc } from '../../integrated-circuits/helpers/compileIc'
/**
* Inits a simulation by:
@ -31,5 +32,9 @@ export const initSimulation = (name: string, mode: simulationMode) => {
)
)
if (mode === 'ic') {
compileIc(state.simulation)
}
return state
}

View file

@ -0,0 +1,32 @@
.lds-ripple {
display: inline-block;
position: relative;
width: 64px;
height: 64px;
}
.lds-ripple div {
position: absolute;
border: 4px solid #fff;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 28px;
left: 28px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: -1px;
left: -1px;
width: 58px;
height: 58px;
opacity: 0;
}
}

View file

@ -0,0 +1,61 @@
@import './Spinner.scss';
@import '../../core/styles/colors.scss';
.Splash {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: $grey;
padding: 32px;
overflow-y: auto;
z-index: 999999999999999;
}
.Splash > .loading {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.Splash > .error {
display: none;
}
.Splash > .error > .title {
font-size: 24px;
margin-bottom: 16px;
}
.Splash > .error > .details {
padding: 16px;
background-color: shade(darker);
border-radius: 3px;
margin-bottom: 16px;
font-family: monospace;
}
.Splash > .error > .description {
color: font-color(normal);
margin-bottom: 16px;
font-size: 14px;
}
.Splash.-hasError {
> .error {
display: block;
}
> .loading {
display: none;
}
}

View file

@ -0,0 +1,84 @@
import { querySelector } from '../../../common/dom/helpers/querySelector'
import { getSafeErrorStack } from '../../../common/lang/errors/helpers/getSafeErrorStack'
import './Splash.scss'
export class Splash {
private element = querySelector<HTMLDivElement>('.Splash')
public fade() {
this.element.style.transition = '0.3s'
this.element.style.opacity = '0'
this.element.style.visibility = 'hidden'
setTimeout(() => {
this.element.remove()
}, 300)
}
private createErrorDOM() {
const root = document.createElement('div')
root.className = 'error'
const title = document.createElement('div')
title.className = 'title'
title.innerText = 'Oops! An error occurred.'
const details = document.createElement('pre')
details.className = 'details'
const description = document.createElement('div')
description.className = 'description'
description.innerHTML = `
<article class="Document">
<p>Your browser might not be supported, or your data might be corrupt.
Press "Clear data" below to reset the simulator and try again.</br>
</br></br>
We do not support the following browsers:</p>
<ul>
<li><span>Opera Mini</span></li>
<li><span>Internet Explorer</span></li>
</ul>
</article>
`
const actions = document.createElement('div')
actions.className = 'actions'
root.appendChild(title)
root.appendChild(details)
root.appendChild(description)
root.appendChild(actions)
this.element.appendChild(root)
}
public setError(error: any) {
this.createErrorDOM()
const details = querySelector<HTMLDivElement>(
'.Splash > .error > .details'
)
const actions = querySelector<HTMLDivElement>(
'.Splash > .error > .actions'
)
details.innerText = getSafeErrorStack(error)
actions.appendChild(this.getClearButton())
this.element.classList.add('-hasError')
}
private getClearButton() {
const clearButton = document.createElement('button')
clearButton.classList.add('PrimaryButton')
clearButton.textContent = 'Clear data'
clearButton.addEventListener('click', () => {
localStorage.clear()
location.reload()
})
return clearButton
}
}

View file

@ -6,7 +6,8 @@
"noImplicitAny": true,
"experimentalDecorators": true,
"target": "esnext",
"strictNullChecks": true
"strictNullChecks": true,
"module": "esnext"
},
"exclude": ["node_modules"],
"include": ["src"]

View file

@ -54,7 +54,7 @@ const sassRule = {
const baseConfig = {
mode: 'none',
entry: ['babel-regenerator-runtime', resolve(sourceFolder, 'main')],
entry: ['babel-regenerator-runtime', resolve(sourceFolder, 'index')],
output: {
filename: 'js/[name].js',
path: buildFolder,