feat: configurable ics:)

This commit is contained in:
Matei Adriel 2020-04-13 23:39:00 +03:00
parent 7df80284f9
commit a9c9ba0b5f
16 changed files with 327 additions and 111 deletions

View file

@ -1,5 +1,6 @@
{
"editor.formatOnSave": true,
"prettier.eslintIntegration": true,
"explorer.autoReveal": false
"explorer.autoReveal": false,
"typescript.tsdk": "node_modules/typescript/lib"
}

View file

@ -5,6 +5,7 @@ module.exports = {
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }]

37
package-lock.json generated
View file

@ -672,6 +672,22 @@
"@babel/plugin-syntax-optional-catch-binding": "^7.2.0"
}
},
"@babel/plugin-proposal-optional-chaining": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz",
"integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==",
"requires": {
"@babel/helper-plugin-utils": "^7.8.3",
"@babel/plugin-syntax-optional-chaining": "^7.8.0"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
"integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ=="
}
}
},
"@babel/plugin-proposal-unicode-property-regex": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz",
@ -746,6 +762,21 @@
"@babel/helper-plugin-utils": "^7.0.0"
}
},
"@babel/plugin-syntax-optional-chaining": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
"integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
"requires": {
"@babel/helper-plugin-utils": "^7.8.0"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
"integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ=="
}
}
},
"@babel/plugin-syntax-typescript": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz",
@ -10461,9 +10492,9 @@
"dev": true
},
"typescript": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz",
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==",
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
},
"uglify-js": {

View file

@ -38,7 +38,7 @@
"optimize-css-assets-webpack-plugin": "^5.0.3",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"typescript": "^3.5.2",
"typescript": "^3.8.3",
"webpack": "^4.36.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2",
@ -46,6 +46,7 @@
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@eix-js/utils": "0.0.6",
"@material-ui/core": "^4.2.1",
"deepmerge": "^4.0.0",

View file

@ -0,0 +1 @@
export type ValueOf<T> = T[keyof T]

View file

@ -0,0 +1,17 @@
export class Lazy<T> {
private value: T | null = null
/**
* SImple encoding of lazy values
* @param getter a function o get the lazy value
*/
public constructor(private getter: () => T) {}
public get(value) {
if (this.value === null) {
this.value = this.getter()
}
return this.value
}
}

View file

@ -1,6 +1,11 @@
import { SimulationState } from '../../saving/types/SimulationSave'
import { SimulationError } from '../../errors/classes/SimulationError'
import { GateTemplate } from '../../simulation/types/GateTemplate'
import {
GateTemplate,
Property,
PropGroup,
isGroup
} from '../../simulation/types/GateTemplate'
import {
simulationInputCount,
simulationOutputCount
@ -13,6 +18,8 @@ import { fromSimulationState } from '../../saving/helpers/fromState'
import { cleanSimulation } from '../../simulation-actions/helpers/clean'
import { getSimulationState } from '../../saving/helpers/getState'
import { categories } from '../../saving/data/categories'
import { getTemplateSafely } from '../../logic-gates/helpers/getTemplateSafely'
import { reservedPropNames } from '../../simulation/constants'
/**
* Compiles a simulation into a logicGate
@ -35,6 +42,8 @@ export const compileIc = (state: SimulationState) => {
const inputCount = simulationInputCount(cleanState.gates)
const outputCount = simulationOutputCount(cleanState.gates)
const props = cleanState.gates.filter(({ props }) => props.external)
const result: DeepPartial<GateTemplate> = {
metadata: {
name
@ -52,6 +61,24 @@ export const compileIc = (state: SimulationState) => {
material: {
type: 'image',
fill: require('../../../assets/ic')
},
properties: {
enabled: !!props.length,
data: props.map((gate) => {
const template = getTemplateSafely(gate.template)
return {
groupName: gate.props.label,
props: template.properties.data.map((prop) => {
if (isGroup(prop)) {
return prop
}
return {
...prop,
base: gate.props[prop.name]
}
})
} as PropGroup
})
}
}

View file

@ -17,21 +17,24 @@ $gate-props-margin: 1rem;
}
div #gate-properties-container {
@include flex;
display: flex;
flex-direction: column;
background-color: $grey;
padding: $gate-props-margin * 4;
border-radius: 1em;
}
#gate-props-divider {
margin-top: $gate-props-margin;
margin-bottom: $gate-props-margin;
padding: $gate-props-margin * 4;
box-sizing: border-box;
max-height: 80vh;
overflow: auto;
}
div #gate-props-title {
color: white;
font-size: 3em;
margin-bottom: 2 * $gate-props-margin;
}
div .gate-prop-container {
@ -45,6 +48,12 @@ div .gate-prop-container {
}
}
div .gate-prop-group-container {
@include flex;
margin-left: 1rem;
}
div #save-props {
width: 50%;
margin: $gate-props-margin * 2;

View file

@ -1,45 +1,83 @@
import './GateProperties.scss'
import React, { ChangeEvent, MouseEvent } from 'react'
import React, { ChangeEvent } from 'react'
import { getRendererSafely } from '../helpers/getRendererSafely'
import { Property } from '../../simulation/types/GateTemplate'
import { Property, RawProp, isGroup } from '../../simulation/types/GateTemplate'
import { useObservable } from 'rxjs-hooks'
import Divider from '@material-ui/core/Divider'
import TextField from '@material-ui/core/TextField'
import CheckBox from '@material-ui/core/Checkbox'
import { open, id } from '../subjects/LogicGatePropsSubjects'
import { Gate } from '../../simulation/classes/Gate'
import { mapRecord } from '../../../common/lang/record/map'
import { BehaviorSubject } from 'rxjs'
import { Gate, GateProps } from '../../simulation/classes/Gate'
import { map } from 'rxjs/operators'
export interface GatePropertyProps {
raw: Property
import { BehaviorSubject } from 'rxjs'
import ExpansionPanel from '@material-ui/core/ExpansionPanel'
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary'
import Typography from '@material-ui/core/Typography'
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'
import Icon from '@material-ui/core/Icon'
interface GatePropertyProps<T extends Property = Property> {
raw: T
gate: Gate
props: GateProps
}
const emptyInput = <></>
const GateProperty = ({ raw, props, gate }: GatePropertyProps) => {
if (isGroup(raw)) {
return (
<ExpansionPanel>
<ExpansionPanelSummary
expandIcon={<Icon> expand_more</Icon>}
aria-controls={raw.groupName}
id={raw.groupName}
>
<Typography>{raw.groupName}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<div className="gate-prop-group-container">
{raw.props.map((propTemplate, index) => (
<GateProperty
key={index}
raw={propTemplate}
gate={gate}
props={props[raw.groupName] as GateProps}
/>
))}
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
)
}
return <GateRawProperty raw={raw} gate={gate} props={props} />
}
/**
* Renders a single props of the gate
*
* @param param0 The props passed to the component
*/
export const GatePropery = ({ raw, gate }: GatePropertyProps) => {
const GateRawProperty = ({
props,
raw,
gate
}: GatePropertyProps & { raw: RawProp }) => {
const { name } = raw
const prop = gate.props[name]
const prop = props[raw.name] as BehaviorSubject<string | number | boolean>
const outputSnapshot = useObservable(() => prop, '')
// rerender when the internal checkbox changes
const internal = useObservable(
// rerender when the external checkbox changes
const external = useObservable(
() =>
gate.props.internal.pipe(
map((value) => value && name !== 'internal')
gate.props.external.pipe(
map((value) => value && name !== 'external')
),
false
)
const displayableName = `${name[0].toUpperCase()}${name.slice(1)} ${
internal ? '(default value)' : ''
external && name !== 'label' ? '(default value)' : ''
}:`
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
@ -55,19 +93,19 @@ export const GatePropery = ({ raw, gate }: GatePropertyProps) => {
if (raw.type !== 'boolean') {
prop.next(value)
}
if (raw.needsUpdate === true) {
gate.update()
}
}
let input = (() => {
if (
raw.show &&
!raw.show(
mapRecord(gate.props, (subject) => subject.value),
gate
const displayExternal = () =>
getRendererSafely().simulation.mode === 'ic' &&
gate.env === 'global' &&
!gate.template.properties.data.some(
(prop) => (prop as RawProp).needsUpdate
)
if (
(raw.name === 'external' && !displayExternal()) ||
(raw.name === 'label' && !external)
) {
return emptyInput
}
@ -147,12 +185,12 @@ const GateProperties = () => {
}}
>
<div id="gate-props-title">Gate properties</div>
<Divider id="gate-props-divider" />
{gate.template.properties.data.map((raw, index) => {
{gate.template.properties.data.map((prop, index) => {
return (
<GatePropery
raw={raw}
<GateProperty
props={gate.props}
raw={prop}
gate={gate}
key={`${index}-${id.value}`}
/>

View file

@ -8,11 +8,15 @@ export interface TransformState {
rotation: number
}
export type PropsSave = {
[K in string]: string | number | boolean | PropsSave
}
export interface GateState {
transform: TransformState
id: number
template: string
props: Record<string, string | number | boolean>
props: PropsSave
}
export interface CameraState {

View file

@ -2,26 +2,25 @@ import { Transform } from '../../../common/math/classes/Transform'
import { BehaviorSubject, fromEvent } from 'rxjs'
import { map } from 'rxjs/operators'
import { getWidth } from '../helpers/getWidth'
import { sidebarWidth } from '../../core/constants'
const width = new BehaviorSubject(getWidth())
const height = new BehaviorSubject(window.innerHeight)
const resize = fromEvent(window, 'resize')
resize.pipe(map(getWidth)).subscribe(val => width.next(val))
resize.pipe(map(() => window.innerHeight)).subscribe(val => height.next(val))
resize.pipe(map(getWidth)).subscribe((val) => width.next(val))
resize.pipe(map(() => window.innerHeight)).subscribe((val) => height.next(val))
/**
* The main screen transform
*/
const Screen = new Transform()
width.subscribe(currentWidth => {
width.subscribe((currentWidth) => {
Screen.width = currentWidth
})
height.subscribe(currentHeight => {
height.subscribe((currentHeight) => {
Screen.height = currentHeight
})

View file

@ -1,10 +1,20 @@
import { Transform } from '../../../common/math/classes/Transform'
import { Pin } from './Pin'
import { GateTemplate, PinCount } from '../types/GateTemplate'
import {
GateTemplate,
PinCount,
isGroup,
Property
} from '../types/GateTemplate'
import { idStore } from '../stores/idStore'
import { Context, InitialisationContext } from '../../activation/types/Context'
import { toFunction } from '../../activation/helpers/toFunction'
import { Subscription, BehaviorSubject, asapScheduler, animationFrameScheduler } from 'rxjs'
import {
Subscription,
BehaviorSubject,
asapScheduler,
animationFrameScheduler
} from 'rxjs'
import { SimulationError } from '../../errors/classes/SimulationError'
import { getGateTimePipes } from '../helpers/getGateTimePipes'
import { ImageStore } from '../../simulationRenderer/stores/imageStore'
@ -16,6 +26,8 @@ import { Wire } from './Wire'
import { cleanSimulation } from '../../simulation-actions/helpers/clean'
import { ExecutionQueue } from '../../activation/classes/ExecutionQueue'
import { tap, observeOn } from 'rxjs/operators'
import { PropsSave } from '../../saving/types/SimulationSave'
import { ValueOf } from '../../../common/lang/record/types/ValueOf'
/**
* The interface for the pins of a gate
@ -47,6 +59,13 @@ export interface GateFunctions {
onClick: GateFunction
}
export type GateProps = {
[K in keyof PropsSave]: BehaviorSubject<PropsSave[K]> | GateProps
} & {
external: BehaviorSubject<boolean>
label: BehaviorSubject<string>
}
export class Gate {
/**
* The transform of the gate
@ -126,10 +145,7 @@ export class Gate {
/**
* The props used by the activation function (the same as memory but presists)
*/
public props: Record<
string,
BehaviorSubject<string | number | boolean>
> = {}
public props: GateProps = {} as GateProps
/**
* The main logic gate class
@ -140,10 +156,9 @@ export class Gate {
public constructor(
template: DeepPartial<GateTemplate> = {},
id?: number,
props: Record<string, string | number | boolean> = {}
props: PropsSave = {}
) {
this.template = completeTemplate(template)
this.transform.scale = this.template.shape.scale
if (this.template.material.type === 'color') {
@ -204,9 +219,7 @@ export class Gate {
if (!state) {
throw new SimulationError(
`Cannot run ic ${
this.template.metadata.name
} - save not found`
`Cannot run ic ${this.template.metadata.name} - save not found`
)
}
@ -219,30 +232,26 @@ export class Gate {
const gates = Array.from(this.ghostSimulation.gates)
const inputs = gates
.filter(gate => gate.template.integration.input)
.filter((gate) => gate.template.integration.input)
.sort(sortByPosition)
.map(gate => gate.wrapPins(gate._pins.outputs))
.map((gate) => gate.wrapPins(gate._pins.outputs))
.flat()
const outputs = gates
.filter(gate => gate.template.integration.output)
.filter((gate) => gate.template.integration.output)
.sort(sortByPosition)
.map(gate => gate.wrapPins(gate._pins.inputs))
.map((gate) => gate.wrapPins(gate._pins.inputs))
.flat()
if (inputs.length !== this._pins.inputs.length) {
throw new SimulationError(
`Input count needs to match with the container gate: ${
inputs.length
} !== ${this._pins.inputs.length}`
`Input count needs to match with the container gate: ${inputs.length} !== ${this._pins.inputs.length}`
)
}
if (outputs.length !== this._pins.outputs.length) {
throw new SimulationError(
`Output count needs to match with the container gate: ${
outputs.length
} !== ${this._pins.outputs.length}`
`Output count needs to match with the container gate: ${outputs.length} !== ${this._pins.outputs.length}`
)
}
@ -267,28 +276,95 @@ export class Gate {
this.assignProps(props)
}
private updateNestedProp(
path: string[],
value: ValueOf<PropsSave>,
gate: Gate = this
) {
if (!path.length) {
return
}
if (path.length === 1) {
const subject = gate.props[path[0]]
if (subject instanceof BehaviorSubject) {
subject.next(value)
}
return
}
const nextGates = [...gate.ghostSimulation.gates].filter(
(gate) => gate.props?.label?.value === path[0]
)
for (const nextGate of nextGates) {
this.updateNestedProp(path.slice(1), value, nextGate)
}
}
/**
* Assign the props passed to the gate and mere them with the base ones
*/
private assignProps(props: Record<string, string | boolean | number>) {
private assignProps(
source: PropsSave,
props: Property[] = this.template.properties.data,
target: GateProps = this.props,
path: string[] = []
) {
let shouldUpdate = false
if (this.template.properties.enabled) {
for (const { base, name, needsUpdate } of this.template.properties
.data) {
this.props[name] = new BehaviorSubject(
props.hasOwnProperty(name) ? props[name] : base
for (const prop of props) {
if (isGroup(prop)) {
const { groupName } = prop
target[groupName] = {} as GateProps
const needsUpdate = this.assignProps(
typeof source[groupName] === 'object'
? (source[groupName] as PropsSave)
: {},
prop.props,
target[groupName] as GateProps,
[...path, groupName]
)
if (!shouldUpdate && needsUpdate) {
if (needsUpdate) {
shouldUpdate = true
}
continue
}
const { name, base, needsUpdate } = prop
const subject = new BehaviorSubject(
source.hasOwnProperty(name) ? source[name] : base
)
target[name] = subject
this.subscriptions.push(
subject.subscribe((value) => {
if (needsUpdate && path.length === 0) {
return this.update()
}
if (path.length === 0) {
return
}
this.updateNestedProp([...path, name], value)
})
)
if (needsUpdate) {
shouldUpdate = true
}
}
}
if (shouldUpdate) {
this.update()
}
return shouldUpdate
}
/**
@ -315,11 +391,15 @@ export class Gate {
/**
* Used to get the props as an object
*/
public getProps() {
const props: Record<string, string | boolean | number> = {}
public getProps(target = this.props) {
const props: PropsSave = {}
for (const key in this.props) {
props[key] = this.props[key].value
for (const [key, value] of Object.entries(target)) {
if (value instanceof BehaviorSubject) {
props[key] = value.value
} else {
props[key] = this.getProps(value)
}
}
return props
@ -361,7 +441,7 @@ export class Gate {
*/
public getContext(): Context {
const maxLength = Math.max(
...this._pins.inputs.map(pin => pin.state.value.length)
...this._pins.inputs.map((pin) => pin.state.value.length)
)
const toLength = (
@ -412,7 +492,10 @@ export class Gate {
return this.props[name].value
},
setProperty: (name: string, value: string | number | boolean) => {
this.props[name].next(value)
const subject = this.props[name]
if (subject instanceof BehaviorSubject) {
subject.next(value)
}
},
innerText: (value: string) => {
this.text.inner.next(value)

View file

@ -1,5 +1,6 @@
import { GateTemplate } from './types/GateTemplate'
import { GateTemplate, Property, RawProp } from './types/GateTemplate'
import { categories } from '../saving/data/categories'
import { getRendererSafely } from '../logic-gates/helpers/getRendererSafely'
export const DefaultGateTemplate: GateTemplate = {
metadata: {
@ -49,7 +50,6 @@ export const DefaultGateTemplate: GateTemplate = {
input: false,
output: false
},
info: [],
tags: ['base'],
properties: {
enabled: false,
@ -57,18 +57,12 @@ export const DefaultGateTemplate: GateTemplate = {
{
type: 'boolean',
base: false,
name: 'internal',
show: (_, gate) =>
gate.env === 'global' &&
!gate.template.properties.data.some(
(prop) => prop.needsUpdate
)
name: 'external'
},
{
type: 'string',
base: 'my-logic-gate',
name: 'label',
show: ({ internal }: { internal: boolean }) => internal
name: 'label'
}
]
},
@ -78,3 +72,10 @@ export const DefaultGateTemplate: GateTemplate = {
},
category: categories.basic
}
/**
* Prop names which need to not be overriten
*/
export const reservedPropNames = DefaultGateTemplate.properties.data.map(
({ name }: RawProp) => name
)

View file

@ -1,23 +1,31 @@
import { vector2 } from '../../../common/math/types/vector2'
import { Gate } from '../classes/Gate'
export interface PinCount {
variable: boolean
count: number
}
export interface Property<
export type PropGroup<
T extends boolean | number | string = boolean | number | string
> {
> = {
groupName: string
props: Property<T>[]
}
export const isGroup = (prop: Property): prop is PropGroup =>
(prop as PropGroup).groupName !== undefined
export type RawProp<
T extends boolean | number | string = boolean | number | string
> = {
type: 'number' | 'string' | 'text' | 'boolean'
base: T
name: string
needsUpdate?: boolean
show?: (
obj: Record<string, string | boolean | number>,
gate: Gate
) => boolean
}
export type Property<
T extends boolean | number | string = boolean | number | string
> = PropGroup<T> | RawProp<T>
export interface Material {
type: 'color' | 'image'
@ -74,7 +82,6 @@ export interface GateTemplate {
input: boolean
output: boolean
}
info: string[]
tags: GateTag[]
properties: {
enabled: boolean

View file

@ -80,7 +80,7 @@ export class SimulationRenderer {
public updateWheelListener(ref: RefObject<HTMLCanvasElement>) {
if (ref.current) {
ref.current.addEventListener('wheel', event => {
ref.current.addEventListener('wheel', (event) => {
if (!modalIsOpen() && location.pathname === '/') {
event.preventDefault()
@ -119,7 +119,7 @@ export class SimulationRenderer {
* @throws SimulationError if the id doesnt have a data prop
*/
public getSelected(): Gate[] {
return setToArray(this.allSelectedIds()).map(id => {
return setToArray(this.allSelectedIds()).map((id) => {
const gate = this.simulation.gates.get(id)
if (!gate) {

View file

@ -4,15 +4,11 @@
"esModuleInterop": true,
"jsx": "preserve",
"experimentalDecorators": true,
"target": "esnext",
"target": "ESNext",
"downlevelIteration": true,
"strictNullChecks": true,
"module": "esnext"
},
"exclude": [
"node_modules"
],
"include": [
"src"
]
"exclude": ["node_modules"],
"include": ["src"]
}