create / open simulations

This commit is contained in:
Matei Adriel 2019-07-22 11:59:10 +03:00
parent 057c2268ac
commit 097c44e86e
43 changed files with 596 additions and 84 deletions

4
docs/dev/z-indexes.md Normal file
View file

@ -0,0 +1,4 @@
# Z-indexes
- 5 = sidebar
- 10 = create-simulation modal

View file

@ -6,7 +6,7 @@
"dev": "webpack-dev-server --open --mode development",
"build": "cross-env NODE_ENV=production webpack",
"deploy": "ts-node deploy",
"show": "gource -f --start-date \"2019-07-01 12:00\" --key --hide dirnames,filenames,bloom"
"show": "gource -f --start-date \"2019-07-01 12:00\" --key --hide dirnames,filenames,bloom -s 3"
},
"devDependencies": {
"@babel/core": "^7.5.5",

View file

@ -15,6 +15,11 @@
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<link
href="https://fonts.googleapis.com/css?family=Righteous&display=swap"
rel="stylesheet"
/>
</head>
<body

View file

@ -1,11 +0,0 @@
import { BehaviorSubject } from 'rxjs'
export type Question = null | {
text: string
options: {
text: string
icon: string
}[]
}
export const QuestionSubject = new BehaviorSubject<Question>(null)

View file

@ -1,3 +1,5 @@
@import '../styles/global-styles/global-styles.scss';
html,
body {
height: 100%;

View file

@ -10,7 +10,8 @@ import Canvas from './Canvas'
import CssBaseline from '@material-ui/core/CssBaseline'
import Theme from '@material-ui/styles/ThemeProvider'
import Sidebar from './Sidebar'
import QuestionModal from './QuestionModal'
import CreateSimulation from '../../create-simulation/components/CreateSimulation'
import Input from '../../input/components/Input'
const App = () => {
return (
@ -19,7 +20,8 @@ const App = () => {
<CssBaseline />
<Canvas />
<Sidebar />
<QuestionModal />
<CreateSimulation />
<Input />
</Theme>
<CssBaseline />
<ToastContainer

View file

@ -6,6 +6,7 @@ import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationR
import { renderSimulation } from '../../simulationRenderer/helpers/renderSimulation'
import { updateSimulation } from '../../simulationRenderer/helpers/updateSimulation'
import { addGate } from '../../simulation/helpers/addGate'
import { rendererSubject } from '../subjects/rendererSubject'
class Canvas extends Component {
private canvasRef: RefObject<HTMLCanvasElement> = createRef()
@ -15,6 +16,8 @@ class Canvas extends Component {
public constructor(props: {}) {
super(props)
rendererSubject.next(this.renderer)
addGate(this.renderer.simulation, 'not')
loop.setDraw(() => {

View file

@ -0,0 +1,73 @@
import React, { useState } from 'react'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem'
import Icon from '@material-ui/core/Icon'
import { saveStore } from '../../saving/stores/saveStore'
import { BehaviorSubject } from 'rxjs'
import { useObservable } from 'rxjs-hooks'
import { rendererSubject } from '../subjects/rendererSubject'
import { currentStore } from '../../saving/stores/currentStore'
const allSimulations = () => {
return saveStore.ls()
}
const allSimulationSubject = new BehaviorSubject<string[]>([])
const updateSimulationList = () => {
allSimulationSubject.next(allSimulations())
}
const OpenSimulation = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const simulations = useObservable(() => allSimulationSubject, [])
const handleClose = () => {
setAnchorEl(null)
}
return (
<>
<ListItem
button
onClick={event => {
updateSimulationList()
setAnchorEl(event.currentTarget)
}}
>
<ListItemIcon>
<Icon>folder_open</Icon>
</ListItemIcon>
<ListItemText>Open simulation</ListItemText>
</ListItem>
<Menu
keepMounted
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
{simulations.map((simulation, index) => (
<MenuItem
key={index}
onClick={() => {
if (rendererSubject.value) {
const renderer = rendererSubject.value
currentStore.set(simulation)
renderer.reloadSave()
}
handleClose()
}}
>
{simulation}
</MenuItem>
))}
</Menu>
</>
)
}
export default OpenSimulation

View file

@ -1,14 +0,0 @@
import './QuestionModal.scss'
import { useObservable } from 'rxjs-hooks'
import { QuestionSubject } from '../QuestionModalSubjects'
import React from 'react'
const QuestionModal = () => {
const question = useObservable(() => QuestionSubject)
if (!question) return <></>
return <div className="questionModal">{question.text}</div>
}
export default QuestionModal

View file

@ -1,29 +1,31 @@
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import React from 'react'
import React, { useState } from 'react'
import Drawer from '@material-ui/core/Drawer'
import Button from '@material-ui/core/Button'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import Icon from '@material-ui/core/Icon'
import { handleCreating } from '../../create-simulation/helpers/handleCreating'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import OpenSimulation from './OpenSimulation'
const drawerWidth = 240
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex'
display: 'flex',
zIndex: 5
},
drawer: {
width: drawerWidth,
flexShrink: 0
flexShrink: 0,
zIndex: 5
},
drawerPaper: {
padding: '4px',
width: drawerWidth,
background: `#111111`
},
drawerHeader: {
display: 'flex',
alignItems: 'center',
padding: '0 8px',
...theme.mixins.toolbar,
justifyContent: 'flex-start'
background: `#111111`,
zIndex: 5
}
})
)
@ -42,9 +44,19 @@ const Sidebar = () => {
paper: classes.drawerPaper
}}
>
<Button variant={'contained'} color="primary">
New Simulation
</Button>
<List component="nav" aria-label="Main mailbox folders">
<ListItem
button
className="contained"
onClick={handleCreating}
>
<ListItemIcon>
<Icon>note_add</Icon>
</ListItemIcon>
<ListItemText>Create simulation</ListItemText>
</ListItem>
<OpenSimulation />
</List>
</Drawer>
</div>
)

View file

@ -0,0 +1,2 @@
$modal-bg-color: rgba(0, 0, 0, 0.7);
$primary: #673ab7;

View file

@ -0,0 +1,2 @@
@import './mui-overrides.scss';
@import './toasts.scss';

View file

@ -0,0 +1,6 @@
@import '../colors.scss';
.MuiListItem-root.contained {
// i spent hours trying to find a better solution
background-color: $primary !important;
}

View file

@ -0,0 +1,6 @@
div.Toastify__toast {
background: black;
}
div.Toastify__taast-body {
color: white;
}

View file

@ -0,0 +1 @@
$modal-index: 10;

View file

@ -0,0 +1,6 @@
@mixin flex {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}

View file

@ -1,7 +1,7 @@
.questionModal {
@mixin full-screen {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

View file

@ -0,0 +1,10 @@
@mixin hidden {
visibility: hidden;
opacity: 0;
transition: all 0.6s ease-in-out 0s;
}
@mixin visible {
visibility: visible;
opacity: 1;
}

View file

@ -0,0 +1,6 @@
import { BehaviorSubject } from 'rxjs'
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
export const rendererSubject = new BehaviorSubject<null | SimulationRenderer>(
null
)

View file

@ -0,0 +1,30 @@
#create-options {
width: 100%;
display: flex;
justify-content: space-evenly;
}
.create-option {
@include flex();
background-color: #444444;
height: 17em;
width: 17em;
border-radius: 1em;
}
.create-option > .create-option-icon > * {
font-size: 7em;
}
.create-option > .create-option-name {
font-size: 2em;
}
#ic {
font-size: 1.7em;
}
.create-option:hover {
border: 0.3em solid white;
}

View file

@ -0,0 +1,17 @@
@import '../../core/styles/mixins/flex.scss';
@import '../../core/styles/mixins/visibility.scss';
@import '../../modals/styles/mixins/modal-container.scss';
@import '../../modals/styles/mixins/modal-title.scss';
@import './CreateOption.scss';
#create-content {
@include modal-container();
}
.shown#create-content {
@include visible();
}
#create-title {
@include modal-title();
}

View file

@ -0,0 +1,69 @@
import React from 'react'
import './CreateSimulation.scss'
import { useObservable } from 'rxjs-hooks'
import { CreateSimulationStore } from '../stores/CreateSimulationStore'
import { simulationMode } from '../../saving/types/SimulationSave'
import Icon from '@material-ui/core/Icon'
export interface CreateSimulationOption {
mode: simulationMode
icon: string
name: string
}
export const createSimulationOptions: CreateSimulationOption[] = [
{
name: 'project',
mode: 'project',
icon: 'gamepad'
},
{
name: 'integrated circuit',
icon: 'memory',
mode: 'ic'
}
]
const CreateSimulation = () => {
const open = useObservable(() => CreateSimulationStore.data.open, false)
const closeModal = () => {
CreateSimulationStore.actions.next('quit')
}
return (
<div
className={open ? 'shown' : ''}
id="create-content"
onClick={closeModal}
>
<div id="create-title">
What kind of simulation do you want to create?
</div>
<div id="create-options">
{createSimulationOptions.map((option, index) => (
<div
key={index}
className="create-option"
onClick={e => {
e.stopPropagation()
CreateSimulationStore.data.output.next(option.mode)
CreateSimulationStore.actions.next('submit')
}}
>
<div className="create-option-icon">
<Icon>{option.icon}</Icon>
</div>
<div className="create-option-name" id={option.mode}>
{option.name}
</div>
</div>
))}
</div>
</div>
)
}
export default CreateSimulation

View file

@ -0,0 +1,12 @@
import { CreateSimulationStore } from '../stores/CreateSimulationStore'
import { initSimulation } from '../../saving/helpers/initSimulation'
export const handleCreating = async () => {
const options = await CreateSimulationStore.create()
if (!options) return null
const simulation = initSimulation(options.name, options.mode)
return simulation
}

View file

@ -0,0 +1,46 @@
import { BehaviorSubject, Subject } from 'rxjs'
import { take } from 'rxjs/operators'
import { simulationMode } from '../../saving/types/SimulationSave'
import { InputStore } from '../../input/stores/InputStore'
export type CreateSimulationStoreAction = 'quit' | 'submit'
export const CreateSimulationStore = {
create: async () => {
CreateSimulationStore.open()
const action = await CreateSimulationStore.actions
.pipe(take(1))
.toPromise()
CreateSimulationStore.close()
if (action === 'quit') {
return null
}
const name = await InputStore.get(
'What do you want your simulation to be called?'
)
if (!name) {
return null
}
return {
mode: CreateSimulationStore.data.output.value,
name
}
},
open() {
CreateSimulationStore.data.open.next(true)
},
close() {
CreateSimulationStore.data.open.next(false)
},
data: {
open: new BehaviorSubject(false),
output: new BehaviorSubject<simulationMode>('project')
},
actions: new Subject<CreateSimulationStoreAction>()
}

View file

@ -0,0 +1,35 @@
@import '../../core/styles/mixins/flex.scss';
@import '../../core/styles/mixins/visibility.scss';
@import '../../modals/styles/mixins/modal-container.scss';
@import '../../modals/styles/mixins/modal-title.scss';
#input-container {
@include modal-container();
justify-content: center;
}
.visible#input-container {
@include visible();
}
#input-container > #input-title {
@include modal-title();
margin-bottom: 1rem;
}
#input-container > #actual-input {
background-color: transparent;
color: inherit;
text-align: center;
font-size: 2.5em;
font-family: inherit;
height: 4rem;
width: 80%;
border: none;
border-bottom: 5px solid white;
}

View file

@ -0,0 +1,48 @@
import React from 'react'
import keycode from 'keycode'
import './Input.scss'
import { useObservable } from 'rxjs-hooks'
import { InputStore } from '../stores/InputStore'
const Input = () => {
const open = useObservable(() => InputStore.data.open, false)
const question = useObservable(() => InputStore.data.question, '')
const output = useObservable(() => InputStore.data.output, '')
const handleQuit = () => {
InputStore.actions.next('quit')
}
return (
<div
id="input-container"
onClick={handleQuit}
className={open ? 'visible' : ''}
>
<div id="input-title">{question}</div>
<input
autoFocus={true}
value={output}
onClick={e => {
e.stopPropagation()
}}
type="text"
id="actual-input"
onChange={e => {
const element = e.target as HTMLInputElement
InputStore.data.output.next(element.value)
}}
onKeyDown={e => {
if (keycode('enter') === e.keyCode) {
e.preventDefault()
return InputStore.actions.next('submit')
}
}}
/>
</div>
)
}
export default Input

View file

@ -0,0 +1,34 @@
import { Subject, BehaviorSubject } from 'rxjs'
import { take } from 'rxjs/operators'
export type InputAction = 'quit' | 'submit'
export const InputStore = {
async get(text: string) {
InputStore.open(text)
const action = await InputStore.actions.pipe(take(1)).toPromise()
InputStore.close()
if (action === 'quit') {
return null
}
return InputStore.data.output.value
},
open(text: string) {
InputStore.data.open.next(true)
InputStore.data.output.next('')
InputStore.data.question.next(text)
},
close() {
InputStore.data.open.next(false)
},
data: {
question: new BehaviorSubject(''),
output: new BehaviorSubject(''),
open: new BehaviorSubject(false)
},
actions: new Subject<InputAction>()
}

View file

@ -0,0 +1,17 @@
@import '../../core/styles/indexes.scss';
@import '../../core/styles/colors.scss';
@import '../../core/styles/mixins/flex.scss';
@import '../../core/styles/mixins/full-screen.scss';
@import '../../core/styles/mixins/visibility.scss';
@mixin modal-container {
@include flex();
@include full-screen();
@include hidden();
justify-content: space-evenly;
z-index: $modal-index;
color: white;
background-color: $modal-bg-color;
font-family: 'Righteous';
}

View file

@ -0,0 +1,5 @@
@mixin modal-title {
font-size: 2.75em;
width: 80%;
text-align: center;
}

View file

@ -1,4 +1,5 @@
import { GateTemplate } from '../simulation/types/GateTemplate'
import { RendererState } from './types/SimulationSave'
export const defaultSimulationName = 'default'
export const baseTemplates: DeepPartial<GateTemplate>[] = [
@ -25,3 +26,19 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
}
}
]
export const baseSave: RendererState = {
camera: {
transform: {
position: [0, 0],
scale: [1, 1],
rotation: 0
}
},
simulation: {
gates: [],
mode: 'project',
wires: [],
name: 'default'
}
}

View file

@ -0,0 +1 @@
export const cloneState = <T>(state: T): T => JSON.parse(JSON.stringify(state))

View file

@ -48,7 +48,8 @@ export const getSimulationState = (simulation: Simulation): SimulationState => {
return {
gates: Array.from(simulation.gates).map(getGateState),
wires: simulation.wires.map(getWireState),
mode: simulation.mode
mode: simulation.mode,
name: simulation.name
}
}

View file

@ -0,0 +1,24 @@
import { simulationMode } from '../types/SimulationSave'
import { baseSave } from '../constants'
import { cloneState } from './cloneState'
import { saveStore } from '../stores/saveStore'
import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments'
export const initSimulation = (name: string, mode: simulationMode) => {
const state = cloneState(baseSave)
state.simulation.name = name
state.simulation.mode = mode
saveStore.set(name, state)
toast.success(
...createToastArguments(
`Successfully created simulation ${name}`,
'check'
)
)
return state
}

View file

@ -14,7 +14,7 @@ export const save = (renderer: SimulationRenderer) => {
saveStore.set(current, state)
toast.info(...createToastArguments(`Succesfully saved ${current}`))
toast(...createToastArguments(`Succesfully saved ${current}`, 'save'))
} else {
throw new SimulationError(
'Cannot save without knowing the name of the active simulation'

View file

@ -35,6 +35,7 @@ export interface SimulationState {
wires: WireState[]
mode: simulationMode
name: string
}
export interface RendererState {

View file

@ -8,7 +8,10 @@ export class Simulation {
public gates = new GateStorage()
public wires: Wire[] = []
public constructor(public mode: simulationMode = 'project') {}
public constructor(
public mode: simulationMode = 'project',
public name: string
) {}
public push(...gates: Gate[]) {
for (const gate of gates) {

View file

@ -5,6 +5,12 @@ const store = new LocalStore<number>('id')
export const idStore = {
generate() {
const current = store.get()
if (current === undefined) {
store.set(1)
return 1
}
store.set(current + 1)
return current + 1

View file

@ -1,6 +1,6 @@
import { Camera } from './Camera'
import { Simulation } from '../../simulation/classes/Simulation'
import { Subject, fromEvent } from 'rxjs'
import { Subject } from 'rxjs'
import { MouseEventInfo } from '../../core/components/FluidCanvas'
import { pointInSquare } from '../../../common/math/helpers/pointInSquare'
import { vector2 } from '../../../common/math/types/vector2'
@ -12,14 +12,12 @@ import { defaultSimulationRendererOptions } from '../constants'
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'
import { KeyBindingMap } from '../../keybindings/types/KeyBindingMap'
import { save } from '../../saving/helpers/save'
import { initKeyBindings } from '../../keybindings/helpers/initialiseKeyBindings'
import { currentStore } from '../../saving/stores/currentStore'
import { saveStore } from '../../saving/stores/saveStore'
import { SimulationError } from '../../errors/classes/SimulationError'
import {
fromSimulationState,
fromCameraState
@ -28,7 +26,7 @@ import merge from 'deepmerge'
import { wireConnectedToGate } from '../helpers/wireConnectedToGate'
import { updateMouse, handleScroll } from '../helpers/scaleCanvas'
import { RefObject } from 'react'
// import { WheelEvent } from 'react'
import { Singleton } from '@eix-js/utils'
export class SimulationRenderer {
public mouseDownOutput = new Subject<MouseEventInfo>()
@ -57,7 +55,7 @@ export class SimulationRenderer {
public constructor(
public ref: RefObject<HTMLCanvasElement>,
options: Partial<SimulationRendererOptions> = {},
public simulation = new Simulation()
public simulation = new Simulation('project', 'default')
) {
this.options = merge(defaultSimulationRendererOptions, options)

View file

@ -36,21 +36,18 @@ export class LocalStore<T> {
return this.getAll()[key]
}
public set(key: string | T = 'index', value?: T) {
public set(key: string | T, value?: T) {
let finalKey = key as string
let finalValue = value as T
if (typeof key !== 'string' || value === undefined) {
localStorage.setItem(
this.name,
JSON.stringify({
index: key
})
)
} else {
localStorage.setItem(
this.name,
JSON.stringify({
[key]: value
})
)
finalKey = 'index'
finalValue = key as T
}
const currentData = this.getAll()
currentData[finalKey] = finalValue
localStorage.setItem(this.name, JSON.stringify(currentData))
}
}

View file

@ -0,0 +1,14 @@
@import '../../core/styles/mixins/flex.scss';
.toast-content-container {
@include flex();
justify-content: space-evenly;
flex-direction: row;
font-size: 1em;
color: white;
}
.toast-content-container > #toast-content-icon {
font-size: 2em;
}

View file

@ -0,0 +1,19 @@
import React from 'react'
import './ToastContent.scss'
import Icon from '@material-ui/core/Icon'
export interface ToastContentProps {
message: string
icon: string
}
const ToastContent = (props: ToastContentProps) => {
return (
<div className="toast-content-container">
<Icon id="toast-content-icon">{props.icon}</Icon>
<div id="toast-content-message">{props.message}</div>
</div>
)
}
export default ToastContent

View file

@ -1,15 +0,0 @@
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

@ -0,0 +1,18 @@
import { ToastOptions } from 'react-toastify'
import React from 'react'
import ToastContent from '../components/ToastContent'
export const createToastArguments = (
message: string,
icon?: string
): [string | JSX.Element, ToastOptions] => [
icon ? <ToastContent message={message} icon={icon} /> : message,
{
position: 'bottom-right',
autoClose: 5000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true
}
]