logic gate button

This commit is contained in:
Matei Adriel 2019-07-22 19:58:26 +03:00
parent 0c76f3cdf6
commit e88244bc9d
29 changed files with 518 additions and 107 deletions

View file

@ -2,4 +2,5 @@ export interface Context {
memory: Record<string, unknown>
get: (index: number) => boolean
set: (index: number, state: boolean) => void
color: (color: string) => void
}

View file

@ -12,6 +12,7 @@ import Theme from '@material-ui/styles/ThemeProvider'
import Sidebar from './Sidebar'
import CreateSimulation from '../../create-simulation/components/CreateSimulation'
import Input from '../../input/components/Input'
import LogicGateModal from '../../logic-gates/components/LogicGateModal'
const App = () => {
return (
@ -22,6 +23,7 @@ const App = () => {
<Sidebar />
<CreateSimulation />
<Input />
<LogicGateModal />
</Theme>
<CssBaseline />
<ToastContainer

View file

@ -0,0 +1,5 @@
@import '../styles/colors.scss';
#language-button {
border: 1px solid white;
}

View file

@ -0,0 +1,31 @@
import React from 'react'
import Icon from '@material-ui/core/Icon'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemIconText from '@material-ui/core/ListItemText'
import { useTranslation } from '../../internalisation/helpers/useLanguage'
import { nextLanguage } from '../../internalisation/helpers/nextLanguage'
import './Language.scss'
/**
* The language component from the sidebar
*/
const Language = () => {
const translation = useTranslation()
return (
<List>
<ListItem button onClick={nextLanguage} id="language-button">
<ListItemIcon>
<Icon>language</Icon>
</ListItemIcon>
<ListItemIconText>
{translation.sidebar.language}: {translation.language}
</ListItemIconText>
</ListItem>
</List>
)
}
export default Language

View file

@ -1,32 +1,11 @@
import React, { useState } from 'react'
import React 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 Typography from '@material-ui/core/Typography'
import { BehaviorSubject } from 'rxjs'
import { useObservable } from 'rxjs-hooks'
import { switchTo } from '../../saving/helpers/switchTo'
import { SimulationError } from '../../errors/classes/SimulationError'
import { templateStore } from '../../saving/stores/templateStore'
import { useTranslation } from '../../internalisation/helpers/useLanguage'
/**
* Subject to make React update the dom when new gates are stored
*/
const allGatesSubject = new BehaviorSubject<string[]>([])
/**
* Triggers a dom update by pushing a new value to the
* useObservable hook inside the React component.
*
* It also has the side effect of sorting the template names.
*/
const updateTemplateList = () => {
allGatesSubject.next(templateStore.ls().sort())
}
import { open } from '../../logic-gates/components/LogicGateModal'
import { updateLogicGateList } from '../../logic-gates/subjects/LogicGateList'
/**
* Component wich contains the sidebar 'Open simulation' button
@ -34,22 +13,14 @@ const updateTemplateList = () => {
* @throws SimulationError if the data about a simulation cant be found in localStorage
*/
const LogicGates = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const simulations = useObservable(() => allGatesSubject, [])
const translation = useTranslation()
const handleClose = () => {
setAnchorEl(null)
}
return (
<>
<ListItem
button
onClick={event => {
updateTemplateList()
setAnchorEl(event.currentTarget)
onClick={() => {
updateLogicGateList()
open.next(true)
}}
>
<ListItemIcon>
@ -57,36 +28,6 @@ const LogicGates = () => {
</ListItemIcon>
<ListItemText>{translation.sidebar.logicGates}</ListItemText>
</ListItem>
<Menu
keepMounted
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
{simulations.map((simulationName, index) => {
const simulationData = templateStore.get(simulationName)
if (!simulationData) {
throw new SimulationError(
`Cannot get data for simulation ${simulationName}`
)
}
return (
<MenuItem
key={index}
onClick={() => {
switchTo(simulationName)
handleClose()
}}
>
<Typography>{simulationName}</Typography>
</MenuItem>
)
})}
</Menu>
</>
)
}

View file

@ -5,6 +5,7 @@ import OpenSimulation from './OpenSimulation'
import CreateSimulationButton from './CreateSimulationButton'
import LogicGates from './LogicGates'
import { makeStyles, createStyles } from '@material-ui/core/styles'
import Language from './Language'
/**
* The width of the sidebar
*/
@ -39,6 +40,11 @@ const useStyles = makeStyles(
padding: '4px',
width: sidebarWidth,
zIndex: sidebarZIndex
},
// This is the class for the main button list
list: {
flexGrow: 1
}
})
)
@ -60,11 +66,13 @@ const Sidebar = () => {
paper: classes.drawerPaper
}}
>
<List component="nav">
<List component="nav" className={classes.list}>
<CreateSimulationButton />
<OpenSimulation />
<LogicGates />
</List>
<Language />
</Drawer>
</div>
)

View file

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

View file

@ -0,0 +1,16 @@
@keyframes glow {
from {
text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #e60073,
0 0 40px #e60073, 0 0 50px #e60073, 0 0 60px #e60073,
0 0 70px #e60073;
}
to {
text-shadow: 0 0 20px #fff, 0 0 30px #ff4da6, 0 0 40px #ff4da6,
0 0 50px #ff4da6, 0 0 60px #ff4da6, 0 0 70px #ff4da6,
0 0 80px #ff4da6;
}
}
@mixin glow {
animation: glow 1s ease-in-out infinite alternate;
}

View file

@ -444,3 +444,8 @@ textarea {
color: #000;
padding: 0.2em 0;
}
a {
color: white;
text-decoration: none;
}

View file

@ -1,3 +1,5 @@
@import '../../core/styles/colors.scss';
#create-options {
width: 100%;
display: flex;
@ -7,7 +9,7 @@
.create-option {
@include flex();
background-color: #444444;
background-color: $grey;
height: 17em;
width: 17em;
border-radius: 1em;

View file

@ -1,12 +1,20 @@
import { supportedLanguages } from './types/supportedLanguages'
import { supportedLanguage } from './types/supportedLanguages'
import { Translation } from './types/TranslationInterface'
import { EnglishTranslation } from './translations/english'
import { RomanianTranslation } from './translations/romanian'
import { DutchTranslation } from './translations/nederlands'
/**
* Object with all translations
*/
export const translations: Record<supportedLanguages, Translation> = {
export const translations: Record<supportedLanguage, Translation> = {
english: EnglishTranslation,
['română']: RomanianTranslation
['română']: RomanianTranslation,
dutch: DutchTranslation
}
export const allSupportedLanguages: supportedLanguage[] = [
'english',
'română',
'dutch'
]

View file

@ -0,0 +1,17 @@
import {
CurrentLanguage,
CurrentLanguageLocalStore
} from '../stores/currentLanguage'
import { allSupportedLanguages } from '../constants'
/**
* Changes the language to the next one avabile
*/
export const nextLanguage = () => {
const current = CurrentLanguage.get()
const index = allSupportedLanguages.indexOf(current)
CurrentLanguage.set(
allSupportedLanguages[(index + 1) % allSupportedLanguages.length]
)
}

View file

@ -0,0 +1,7 @@
/**
* Returns a random element of the aray
*
* @param arr - the array to choose from
*/
export const randomItem = <T>(arr: T[]): T =>
arr[Math.floor(Math.random() * arr.length)]

View file

@ -1,4 +1,4 @@
import { supportedLanguages } from '../types/supportedLanguages'
import { supportedLanguage } from '../types/supportedLanguages'
import { LocalStore } from '../../storage/classes/LocalStore'
import currentLanguageSubject from './../subjects/currentLanguageSubject'
import { SimulationError } from '../../errors/classes/SimulationError'
@ -7,7 +7,7 @@ import { translations } from '../constants'
/**
* Local store containing the current selected language
*/
export const CurrentLanguageLocalStore = new LocalStore<supportedLanguages>(
export const CurrentLanguageLocalStore = new LocalStore<supportedLanguage>(
'language'
)
@ -22,7 +22,7 @@ currentLanguageSubject.next(CurrentLanguageLocalStore.get() || 'english')
* The preffered interface for interacting with CurrentLanguageLocalStore
*/
const CurrentLanguage = {
set(name: supportedLanguages) {
set(name: supportedLanguage) {
CurrentLanguageLocalStore.set(name)
currentLanguageSubject.next(name)
},

View file

@ -1,7 +1,7 @@
import { BehaviorSubject } from 'rxjs'
import { supportedLanguages } from '../types/supportedLanguages'
import { supportedLanguage } from '../types/supportedLanguages'
/**
* Subject with the current language
*/
export default new BehaviorSubject<supportedLanguages>('english')
export default new BehaviorSubject<supportedLanguage>('english')

View file

@ -8,7 +8,8 @@ export const EnglishTranslation: Translation = {
sidebar: {
createSimulation: 'Create simulation',
logicGates: 'Logic gates',
openSimulation: 'Open simulations'
openSimulation: 'Open simulations',
language: 'Language'
},
createSimulation: {
mode: {

View file

@ -0,0 +1,33 @@
import { Translation } from '../types/TranslationInterface'
/**
* The dutch translation
*/
export const DutchTranslation: Translation = {
language: 'dutch',
sidebar: {
createSimulation: 'Maak simulatie',
logicGates: 'Logische poorten',
openSimulation: 'Open simulatie',
language: 'Taal'
},
createSimulation: {
mode: {
question: 'Wat voor simulatie wil je maken?',
options: {
// ic: 'Geïntegreerde schakeling',
ic: 'IC',
project: 'Project'
}
},
name: {
question: 'Hoe wil je je simulatie noemen?'
}
},
messages: {
createdSimulation: name => `Simulatie '${name}' succesvol gecreerd`,
switchedToSimulation: name =>
`Succesvol veranderd naar simulatie '${name}'`,
savedSimulation: name => `Simulatie succesvol opgeslagen '${name}'`
}
}

View file

@ -8,11 +8,12 @@ export const RomanianTranslation: Translation = {
sidebar: {
createSimulation: 'Creează o simulație',
openSimulation: 'Deschide o simulație',
logicGates: 'Porți logice'
logicGates: 'Porți logice',
language: 'Limba'
},
createSimulation: {
mode: {
question: 'Ce fel de simulație vrei să creiezi?',
question: 'Ce fel de simulație vrei să creezi?',
options: {
ic: 'Circuit integrat',
project: 'Proiect'
@ -24,7 +25,7 @@ export const RomanianTranslation: Translation = {
},
messages: {
createdSimulation: name =>
`Simulația '${name}' a fost creiată cu succes`,
`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`

View file

@ -1,4 +1,4 @@
import { supportedLanguages } from './supportedLanguages'
import { supportedLanguage } from './supportedLanguages'
import { simulationMode } from '../../saving/types/SimulationSave'
export type SentenceFactory<T extends string[]> = (...names: T) => string
@ -8,11 +8,12 @@ export type NameSentence = SentenceFactory<[string]>
* The interface all translations need to follow
*/
export interface Translation {
language: supportedLanguages
language: supportedLanguage
sidebar: {
createSimulation: string
openSimulation: string
logicGates: string
language: string
}
createSimulation: {
mode: {

View file

@ -1,4 +1,4 @@
/**
* Type containing the names of all supported languages
*/
export type supportedLanguages = 'română' | 'english'
export type supportedLanguage = 'română' | 'english' | 'dutch'

View file

@ -0,0 +1,62 @@
@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 '../../core/styles/colors.scss';
@import '../../core/styles/mixins/glow.scss';
// Opacities
$normal-opacity: 0.6;
$hover-opacity: 0.9;
// Colors
$item-color: $grey;
#logic-gate-modal-container {
@include modal-container();
justify-content: center;
}
.visible#logic-gate-modal-container {
@include visible();
}
#logic-gate-modal-container > .logic-gate-item {
@include flex();
justify-content: left;
flex-direction: row;
width: 80%;
height: 4em;
border: 3px solid white;
margin: 0.5em;
background-color: rgba($item-color, $normal-opacity);
}
#logic-gate-modal-container > .logic-gate-item:hover {
background-color: rgba($item-color, $hover-opacity);
}
#logic-gate-modal-container > .logic-gate-item > * {
font-size: 3em;
}
#logic-gate-modal-container > .logic-gate-item > .logic-gate-item-type {
margin: 0.5em;
}
#logic-gate-modal-container > .logic-gate-item > *:last-child {
margin-right: 0.5em;
}
.lgi-icon:hover:not(.logic-gate-item-type) {
border: 1px solid white;
}
#logic-gate-modal-container > .logic-gate-item > .logic-gate-item-name {
flex-grow: 1;
}

View file

@ -0,0 +1,90 @@
import './LogicGateModal.scss'
import React from 'react'
import { BehaviorSubject } from 'rxjs'
import { useObservable } from 'rxjs-hooks'
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'
/**
* Subject containing the open state of the modal
*/
export const open = new BehaviorSubject(false)
/**
* helper to close the modal
*/
export const handleClose = () => {
open.next(false)
}
/**
* The component containing the info / actions about all logic gates
*/
const LogicGateModal = () => {
const openSnapshot = useObservable(() => open, false)
const gates = useObservable(() => LogicGateList, [])
return (
<div
className={openSnapshot ? 'visible' : ''}
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 =
gate.source === 'base' ? templateStore.get(gate.name) : ''
if (gate.source === 'base' && !template) {
throw new SimulationError(
`Template ${gate.name} cannot be found`
)
}
return (
<div
key={index}
className="logic-gate-item"
onClick={e => {
addGate(renderer.simulation, gate.name)
}}
>
<Icon className="lgi-icon logic-gate-item-type">
{gate.source === 'base' ? 'sd_storage' : 'memory'}
</Icon>
<Typography className="logic-gate-item-name">
{gate.name}
</Typography>
{template && template.info && template.info.length && (
<a
target="_blank"
className="logic-gate-item-info"
href={randomItem(template.info)}
>
<Icon className="lgi-icon">info</Icon>
</a>
)}
{gate.source === 'ic' && (
<Icon className="lgi-icon logic-gate-item-delete">
delete
</Icon>
)}
</div>
)
})}
</div>
)
}
export default LogicGateModal

View file

@ -0,0 +1,26 @@
import { saveStore } from '../../saving/stores/saveStore'
import { SimulationError } from '../../errors/classes/SimulationError'
/**
* Helper to get the names of all integrated circuits
*
* @throws SimulationError if a save cannot be found in localsStorage
*/
export const getAllics = () => {
const saves = saveStore.ls()
const result: string[] = []
for (const save of saves) {
const saveState = saveStore.get(save)
if (saveState) {
if (saveState.simulation.mode === 'ic') {
result.push(saveState.simulation.name)
}
} else {
throw new SimulationError(`Cannot find save ${save}`)
}
}
return result
}

View file

@ -0,0 +1,37 @@
import { BehaviorSubject } from 'rxjs'
import { templateStore } from '../../saving/stores/templateStore'
import { getAllics } from '../helpers/getAllIcs'
/**
* The interface for the items in the list
*/
export interface LogicGateNameWrapper {
source: 'base' | 'ic'
name: string
}
/**
* Subject containing a list with the names of all logic gate templates
*/
export const LogicGateList = new BehaviorSubject<LogicGateNameWrapper[]>([])
/**
* Helper method to update the list of logic gate templates.
*/
export const updateLogicGateList = () => {
const ics = getAllics().map(
(name): LogicGateNameWrapper => ({
source: 'ic',
name
})
)
const templates = templateStore.ls().map(
(name): LogicGateNameWrapper => ({
source: 'base',
name
})
)
LogicGateList.next([...ics, ...templates])
}

View file

@ -3,25 +3,118 @@ import { RendererState } from './types/SimulationSave'
export const defaultSimulationName = 'default'
export const baseTemplates: DeepPartial<GateTemplate>[] = [
{
metadata: {
name: 'and'
},
material: {
value: 'green'
},
code: {
activation: `context.set(0, context.get(0) && context.get(1))`
},
pins: {
inputs: {
count: 2
}
},
info: ['https://en.wikipedia.org/wiki/AND_gate']
},
{
metadata: {
name: 'or'
},
material: {
value: 'yellow'
},
code: {
activation: `context.set(0, context.get(0) || context.get(1))`
},
pins: {
inputs: {
count: 2
}
},
info: ['https://en.wikipedia.org/wiki/OR_gate']
},
{
metadata: {
name: 'xor'
},
material: {
value: 'white'
},
code: {
activation: `
const a = context.get(0)
const b = context.get(1)
const c = (a || b) && (!a || !b)
context.set(0, c)`
},
info: ['https://en.wikipedia.org/wiki/XOR_gate'],
pins: {
inputs: {
count: 2
}
}
},
{
metadata: {
name: 'not'
},
material: {
value: 'red',
type: 'color'
value: 'red'
},
code: {
activation: `context.set(0, !context.get(0))`
},
info: ['https://en.wikipedia.org/wiki/Inverter_(logic_gate)']
},
{
metadata: {
name: 'button'
},
material: {
value: 'red'
},
code: {
onClick: `
const old = context.memory.state
const state = !old
context.set(0, state)
context.color(old ? 'red' : '#550000')
context.memory.state = state
`
},
pins: {
inputs: {
count: 1,
variable: false
count: 0
}
},
info: ['https://en.wikipedia.org/wiki/Push-button']
},
{
metadata: {
name: 'light bulb'
},
shape: {
radius: 50
},
material: {
value: 'white'
},
code: {
activation: `
context.color(context.get(0) ? 'yellow' : 'white')
`
},
info: ['https://en.wikipedia.org/wiki/Incandescent_light_bulb'],
pins: {
outputs: {
count: 1,
variable: false
count: 0
}
}
}

View file

@ -22,8 +22,11 @@ export interface PinWrapper {
value: Pin
}
export type GateFunction = null | ((ctx: Context) => void)
export interface GateFunctions {
activation: null | ((ctx: Context) => void)
activation: GateFunction
onClick: GateFunction
}
export class Gate {
@ -37,7 +40,8 @@ export class Gate {
public template: GateTemplate
private functions: GateFunctions = {
activation: null
activation: null,
onClick: null
}
private subscriptions: Subscription[] = []
@ -53,6 +57,11 @@ export class Gate {
'context'
)
this.functions.onClick = toFunction(
this.template.code.onClick,
'context'
)
this._pins.inputs = Gate.generatePins(
this.template.pins.inputs,
1,
@ -77,6 +86,12 @@ export class Gate {
}
}
public onClick() {
if (this.functions.onClick) {
this.functions.onClick(this.getContext())
}
}
public dispose() {
for (const pin of this.pins) {
pin.value.dispose()
@ -104,7 +119,12 @@ export class Gate {
set: (index: number, state: boolean = false) => {
return this._pins.outputs[index].state.next(state)
},
memory: this.memory
memory: this.memory,
color: (color: string) => {
if (this.template.material.type === 'color') {
this.template.material.value = color
}
}
}
}

View file

@ -10,7 +10,7 @@ export const DefaultGateTemplate: GateTemplate = {
},
pins: {
inputs: {
count: 2,
count: 1,
variable: false
},
outputs: {
@ -25,8 +25,7 @@ export const DefaultGateTemplate: GateTemplate = {
},
code: {
activation: 'context.set(0,true)',
start: '',
stop: ''
onClick: ''
},
simulation: {
debounce: {
@ -36,5 +35,6 @@ export const DefaultGateTemplate: GateTemplate = {
throttle: {
enabled: false
}
}
},
info: []
}

View file

@ -39,12 +39,12 @@ export interface GateTemplate {
name: string
}
code: {
start: string
activation: string
stop: string
onClick: string
}
simulation: {
throttle: TimePipe
debounce: TimePipe
}
info: string[]
}

View file

@ -77,6 +77,9 @@ export class SimulationRenderer {
const { transform, id, pins } = gates[index]
if (pointInSquare(worldPosition, transform)) {
// run function
gates[index].onClick()
this.mouseManager.clear(worldPosition[0])
this.mouseState |= 1