Error handling + fixed pin connecting

This commit is contained in:
Matei Adriel 2019-07-18 12:42:21 +03:00
parent d38ce7cd1b
commit fbcfb76305
25 changed files with 431 additions and 45 deletions

40
package-lock.json generated
View file

@ -2194,6 +2194,11 @@
}
}
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"clean-css": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
@ -3058,6 +3063,14 @@
"utila": "~0.4"
}
},
"dom-helpers": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
@ -7715,6 +7728,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-router": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.1.tgz",
@ -7761,6 +7779,28 @@
"tiny-warning": "^1.0.0"
}
},
"react-toastify": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-5.3.2.tgz",
"integrity": "sha512-YHTTey7JWqXVkkBIeJ34PAvQELmGfLEGCx9bu68aIZYd+kRU2u9k/nG3AydgbX/uevIb4QNpeeE98DjkooMs5w==",
"requires": {
"@babel/runtime": "^7.4.2",
"classnames": "^2.2.6",
"prop-types": "^15.7.2",
"react-transition-group": "^2.6.1"
}
},
"react-transition-group": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
"integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
"requires": {
"dom-helpers": "^3.4.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",

View file

@ -44,6 +44,7 @@
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1",
"react-toastify": "^5.3.2",
"rxjs": "^6.5.2",
"rxjs-hooks": "^0.5.1"
}

View file

@ -2,5 +2,8 @@ import React from 'react'
import App from './modules/core/components/App'
import { render } from 'react-dom'
import { handleErrors } from './modules/errors/helpers/handlErrors'
render(<App />, document.getElementById('app'))
handleErrors()

View file

@ -8,4 +8,5 @@ body {
canvas {
background-color: #222222;
z-index: -1;
}

View file

@ -1,10 +1,26 @@
import React from 'react'
import '../styles/reset'
import './App.scss'
import 'react-toastify/dist/ReactToastify.css'
import Canvas from './Canvas'
import { ToastContainer } from 'react-toastify'
const App = () => {
return <Canvas />
return (
<>
<ToastContainer
position="top-left"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
draggable
pauseOnHover
/>
<Canvas />
</>
)
}
export default App

View file

@ -0,0 +1,9 @@
export class SimulationError extends Error {
public constructor(public mesagge: string = '') {
super()
}
public toString() {
return `SimulationError: ${this.mesagge}`
}
}

View file

@ -0,0 +1,14 @@
import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments'
export const handleErrors = () => {
window.onerror = (a, b, c, d, error) => {
if (error) {
const args = createToastArguments(error.toString())
toast.error(...args)
}
console.log(a)
}
}

View file

@ -1,15 +1,18 @@
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
import { Gate } from '../../simulation/classes/Gate'
import { Gate, PinWrapper } from '../../simulation/classes/Gate'
import {
GateState,
TransformState,
RendererState,
CameraState,
SimulationState
SimulationState,
WireState,
WireLimit
} from '../types/SimulationSave'
import { Transform } from '../../../common/math/classes/Transform'
import { Camera } from '../../simulationRenderer/classes/Camera'
import { Simulation } from '../../simulation/classes/Simulation'
import { Wire } from '../../simulation/classes/Wire'
export const getTransformState = (transform: Transform): TransformState => {
return {
@ -25,9 +28,26 @@ export const getCameraState = (camera: Camera): CameraState => {
}
}
export const getWireLimit = (pin: PinWrapper): WireLimit => {
return {
id: pin.value.gate.id,
index: pin.index
}
}
export const getWireState = (wire: Wire): WireState => {
return {
from: getWireLimit(wire.start),
to: getWireLimit(wire.end),
id: wire.id
}
}
export const getSimulationState = (simulation: Simulation): SimulationState => {
return {
gates: Array.from(simulation.gates).map(getGateState)
gates: Array.from(simulation.gates).map(getGateState),
wires: simulation.wires.map(getWireState),
mode: simulation.mode
}
}

View file

@ -1,5 +1,7 @@
import { vector2 } from '../../../common/math/classes/Transform'
export type simulationMode = 'ic' | 'project'
export interface TransformState {
position: vector2
scale: vector2
@ -16,8 +18,22 @@ export interface CameraState {
transform: TransformState
}
export interface WireLimit {
id: number
index: number
}
export interface WireState {
from: WireLimit
to: WireLimit
id: number
}
export interface SimulationState {
gates: GateState[]
wires: WireState[]
mode: simulationMode
}
export interface RendererState {

View file

@ -3,6 +3,7 @@ import { Pin } from './Pin'
import merge from 'deepmerge'
import { GateTemplate, PinCount } from '../types/GateTemplate'
import { DefaultGateTemplate } from '../constants'
import { idStore } from '../stores/idStore'
export interface GatePins {
inputs: Pin[]
@ -16,22 +17,30 @@ export interface PinWrapper {
}
export class Gate {
public static lastId = 0
public transform = new Transform()
public id = Gate.lastId++
public _pins: GatePins = {
inputs: [],
outputs: []
}
public id: number
public template: GateTemplate
public constructor(template: DeepPartial<GateTemplate> = {}) {
public constructor(template: DeepPartial<GateTemplate> = {}, id?: number) {
this.template = merge(DefaultGateTemplate, template) as GateTemplate
this._pins.inputs = Gate.generatePins(this.template.pins.inputs, 1)
this._pins.outputs = Gate.generatePins(this.template.pins.outputs, 2)
this._pins.inputs = Gate.generatePins(
this.template.pins.inputs,
1,
this
)
this._pins.outputs = Gate.generatePins(
this.template.pins.outputs,
2,
this
)
this.id = id !== undefined ? id : idStore.generate()
}
private wrapPins(pins: Pin[]) {
@ -58,7 +67,9 @@ export class Gate {
return result
}
private static generatePins(options: PinCount, type: number) {
return [...Array(options.count)].fill(true).map(() => new Pin(type))
private static generatePins(options: PinCount, type: number, gate: Gate) {
return [...Array(options.count)]
.fill(true)
.map(() => new Pin(type, gate))
}
}

View file

@ -1,5 +1,6 @@
import { SubscriptionData } from '../types/SubscriptionData'
import { BehaviorSubject } from 'rxjs'
import { Gate } from './Gate'
/* Types:
@ -9,24 +10,25 @@ Second bit = output
*/
export class Pin {
public state = new BehaviorSubject(false)
public connectedTo = new Set<Pin>()
public pairs = new Set<Pin>()
private pairs = new Set<Pin>()
private subscriptions: SubscriptionData<Pin>[] = []
public constructor(public type = 0b01) {}
public constructor(public type = 0b01, public gate: Gate) {}
public addPair(pin: Pin) {
public addPair(pin: Pin, subscribe = false) {
this.pairs.add(pin)
const rawSubscription = pin.state.subscribe(state => {
this.state.next(state)
})
if (subscribe) {
const rawSubscription = pin.state.subscribe(state => {
this.state.next(state)
})
this.subscriptions.push({
data: pin,
subscription: rawSubscription
})
this.subscriptions.push({
data: pin,
subscription: rawSubscription
})
}
}
public removePair(pin: Pin) {

View file

@ -1,9 +1,14 @@
import { Gate } from './Gate'
import { GateStorage } from './GateStorage'
import { LruCacheNode } from '@eix-js/utils'
import { Wire } from './Wire'
import { simulationMode } from '../../saving/types/SimulationSave'
export class Simulation {
public gates = new GateStorage()
public wires: Wire[] = []
public constructor(public mode: simulationMode = 'project') {}
public push(...gates: Gate[]) {
for (const gate of gates) {

View file

@ -0,0 +1,27 @@
import { idStore } from '../stores/idStore'
import { PinWrapper } from './Gate'
import { SimulationError } from '../../errors/classes/SimulationError'
export class Wire {
public id: number
public constructor(
public start: PinWrapper,
public end: PinWrapper,
id?: number
) {
if (end.value.pairs.size !== 0) {
throw new SimulationError('An input pin can only have 1 pair')
}
end.value.addPair(start.value, true)
start.value.addPair(end.value)
this.id = id !== undefined ? id : idStore.generate()
}
public dispose() {
this.end.value.removePair(this.start.value)
this.start.value.removePair(this.end.value)
}
}

View file

@ -0,0 +1,12 @@
import { LocalStore } from '../../storage/classes/LocalStore'
const store = new LocalStore<number>('id')
export const idStore = {
generate() {
const current = store.get()
store.set(current + 1)
return current + 1
}
}

View file

@ -14,6 +14,7 @@ import { getPinPosition } from '../helpers/pinPosition'
import { pointInCircle } from '../../../common/math/helpers/pointInCircle'
import { SelectedPins } from '../types/SelectedPins'
import { getRendererState } from '../../saving/helpers/getState'
import { Wire } from '../../simulation/classes/Wire'
export class SimulationRenderer {
public mouseDownOutput = new Subject<MouseEventInfo>()
@ -104,11 +105,21 @@ export class SimulationRenderer {
}
}
if (this.selectedPins.start && this.selectedPins.end) {
console.log('Connecting!')
console.log(getRendererState(this))
if (
this.selectedPins.start &&
this.selectedPins.end &&
this.selectedPins.end.wrapper.value.pairs.size === 0
) {
this.simulation.wires.push(
new Wire(
this.selectedPins.start.wrapper,
this.selectedPins.end.wrapper
)
)
this.selectedPins.start = null
this.selectedPins.end = null
console.log(getRendererState(this))
}
}
}

View file

@ -8,9 +8,14 @@ export const defaultSimulationRendererOptions: SimulationRendererOptions = {
connectionLength: 30,
pinRadius: 10,
pinStrokeColor: '#888888',
pinStrokeWidth: 3
pinStrokeWidth: 3,
pinFill: {
open: 'rgb(255,216,20)',
closed: 'rgb(90,90,90)'
}
},
wires: {
temporaryWireColor: `rgba(128,128,128,0.5)`
temporaryWireColor: `rgba(128,128,128,0.5)`,
curvePointOffset: 100
}
}

View file

@ -1,13 +1,14 @@
import { Pin } from '../../simulation/classes/Pin'
import { SimulationRenderer } from '../classes/SimulationRenderer'
export const pinFill = (pin: Pin) => {
export const pinFill = (renderer: SimulationRenderer, pin: Pin) => {
let color = 'rgba(0,0,0,0)'
if (pin.connectedTo.size) {
if (pin.state) {
color = 'yellow'
if (pin.pairs.size) {
if (pin.state.value) {
color = renderer.options.gates.pinFill.open
} else {
color = 'grey'
color = renderer.options.gates.pinFill.closed
}
}

View file

@ -23,7 +23,7 @@ export const renderPins = (
ctx.lineWidth = pinStrokeWidth
for (const pin of gate.pins) {
ctx.fillStyle = pinFill(pin.value)
ctx.fillStyle = pinFill(renderer, pin.value)
// render little connection
const start = calculatePinStart(

View file

@ -3,6 +3,7 @@ import { invert } from '../../vector2/helpers/basic'
import { renderGate } from './renderGate'
import { clearCanvas } from '../../../common/canvas/helpers/clearCanvas'
import { renderClickedPins } from './renderClickedPins'
import { renderWires } from './renderWires'
export const renderSimulation = (
ctx: CanvasRenderingContext2D,
@ -12,7 +13,10 @@ export const renderSimulation = (
ctx.translate(...renderer.camera.transform.position)
// render gates
for (const wire of renderer.simulation.wires) {
renderWires(ctx, renderer, wire)
}
for (const gate of renderer.simulation.gates) {
renderGate(ctx, renderer, gate)
}

View file

@ -0,0 +1,94 @@
import { SimulationRenderer } from '../classes/SimulationRenderer'
import { pinFill } from './pinFill'
import { getPinPosition } from './pinPosition'
import { Wire } from '../../simulation/classes/Wire'
import { wireRadius } from './wireRadius'
import { clamp } from '../../simulation/helpers/clamp'
export const renderWires = (
ctx: CanvasRenderingContext2D,
renderer: SimulationRenderer,
wire: Wire
) => {
const { start, end } = wire
const startPosition = getPinPosition(
renderer,
start.value.gate.transform,
start
)
const endPosition = getPinPosition(renderer, end.value.gate.transform, end)
const length = renderer.options.wires.curvePointOffset
const centerY = (startPosition[1] + endPosition[1]) / 2
const controlPostions = [startPosition[0] + length, endPosition[0] - length]
ctx.strokeStyle = pinFill(renderer, start.value)
ctx.lineWidth = wireRadius(renderer)
ctx.lineCap = 'round'
ctx.beginPath()
ctx.moveTo(...startPosition)
if (startPosition[0] < endPosition[0]) {
ctx.bezierCurveTo(
controlPostions[0],
startPosition[1],
controlPostions[1],
endPosition[1],
...endPosition
)
} else {
const { abs, PI } = Math
const baseFactor = startPosition[1] < endPosition[1] ? 1 : -1
const factors = [baseFactor, baseFactor]
const radiuses = [...Array(2)].fill(
abs((centerY - startPosition[1]) / 2)
)
const limit = 70
if (radiuses[0] < limit) {
factors[0] *= -1
radiuses[0] = limit
radiuses[1] = abs(
(startPosition[1] +
factors[0] * 2 * radiuses[0] -
endPosition[1]) /
2
)
// radiuses[0] =
}
const centerPosition = [
startPosition[1] + factors[0] * radiuses[0],
endPosition[1] - factors[1] * radiuses[1]
]
ctx.arc(
controlPostions[0],
centerPosition[0],
radiuses[0],
(-factors[0] * PI) / 2,
(factors[0] * PI) / 2,
factors[0] !== 1
)
ctx.lineTo(
controlPostions[1],
endPosition[1] - factors[1] * 2 * radiuses[1]
)
ctx.arc(
controlPostions[1],
centerPosition[1],
radiuses[1],
(-factors[1] * PI) / 2,
(factors[1] * PI) / 2,
factors[1] === 1
)
ctx.lineTo(...endPosition)
}
ctx.stroke()
}

View file

@ -0,0 +1,9 @@
import { SimulationRenderer } from '../classes/SimulationRenderer'
export const wireRadius = (renderer: SimulationRenderer) => {
return (
2 *
(renderer.options.gates.pinRadius -
renderer.options.gates.pinStrokeWidth)
)
}

View file

@ -7,8 +7,13 @@ export interface SimulationRendererOptions {
pinRadius: number
pinStrokeColor: string
pinStrokeWidth: number
pinFill: {
open: string
closed: string
}
}
wires: {
temporaryWireColor: string
curvePointOffset: number
}
}

View file

@ -0,0 +1,56 @@
import { CacheInstancesByKey } from '@eix-js/utils'
@CacheInstancesByKey(Infinity)
export class LocalStore<T> {
public constructor(public name: string) {
if (!localStorage.getItem(name)) {
localStorage.setItem(name, '{}')
}
}
public getAll(): Record<string, T> {
const raw = localStorage.getItem(this.name)
if (!raw)
throw new Error(
`An error occured when accesing ${
this.name
} in the local storage!`
)
else {
return JSON.parse(raw)
}
}
public ls(): string[] {
return Object.keys(this.getAll())
}
public *[Symbol.iterator](): Iterable<T> {
for (const item of this.ls()) {
return this.get(item)
}
}
public get(key = 'index') {
return this.getAll()[key]
}
public set(key: string | T = 'index', value?: T) {
if (typeof key !== 'string' || value === undefined) {
localStorage.setItem(
this.name,
JSON.stringify({
index: key
})
)
} else {
localStorage.setItem(
this.name,
JSON.stringify({
[key]: value
})
)
}
}
}

View file

@ -0,0 +1,15 @@
import { ToastOptions } from 'react-toastify'
export const createToastArguments = (
message: string
): [string, ToastOptions] => [
message,
{
position: 'top-left',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true
}
]

View file

@ -17,18 +17,27 @@ const babelRule = {
use: 'babel-loader'
}
const cssAndSass = [
isProduction
? MiniCssExtractPlugin.loader
: {
loader: 'style-loader',
options: {
singleton: true
}
},
'css-loader'
]
const cssRule = {
test: /\.css$/,
use: cssAndSass
}
const sassRule = {
test: /\.scss$/,
use: [
isProduction
? MiniCssExtractPlugin.loader
: {
loader: 'style-loader',
options: {
singleton: true
}
},
{ loader: 'css-loader' },
...cssAndSass,
{
loader: 'sass-loader',
options: {
@ -47,7 +56,7 @@ const baseConfig = {
publicPath: '/'
},
module: {
rules: [babelRule, sassRule]
rules: [babelRule, sassRule, cssRule]
},
resolve: {
extensions: ['.js', '.ts', '.tsx', '.scss']