Add support for constants

This commit is contained in:
prescientmoon 2024-11-27 10:41:36 +01:00
parent 7d9d2a2d78
commit 96e2184a24
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
19 changed files with 1086 additions and 1004 deletions

View file

@ -43,7 +43,11 @@ const ctx = await esbuild.context({
}) })
if (serve) { if (serve) {
const { port, host } = await ctx.serve({ servedir: 'dist' }) await ctx.watch()
const { port, host } = await ctx.serve({
servedir: 'dist',
fallback: 'dist/index.html'
})
console.log(`Serving on ${host}:${port}`) console.log(`Serving on ${host}:${port}`)
} else { } else {
await ctx.rebuild() await ctx.rebuild()

View file

@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "ESBUILD_SERVE=1 node ./build.js",
"build": "node ./build.js", "build": "node ./build.js",
"check": "tsc" "check": "tsc"
}, },

View file

@ -33,8 +33,10 @@ async function main() {
} }
} }
new EventSource('/esbuild').addEventListener('change', () => location.reload())
// Call entry // Call entry
main().catch(error => { main().catch((error) => {
// if the error handling error has an error, log that error // if the error handling error has an error, log that error
console.error('Error loading app', error) console.error('Error loading app', error)
}) })

View file

@ -1,4 +1,4 @@
import { Simulation, SimulationEnv } from '../../simulation/classes/Simulation' import { SimulationEnv } from '../../simulation/classes/Simulation'
import { PinState } from '../../simulation/classes/Pin' import { PinState } from '../../simulation/classes/Pin'
export interface Context { export interface Context {
@ -6,8 +6,12 @@ export interface Context {
setProperty: (name: string, value: unknown) => void setProperty: (name: string, value: unknown) => void
get: (index: number) => PinState get: (index: number) => PinState
set: (index: number, state: PinState) => void set: (index: number, state: PinState) => void
getOutput: (index: number) => PinState
getBinary: (index: number) => number getBinary: (index: number) => number
setBinary: (index: number, value: number, bits: number) => void printBinary: (value: number, bits?: number) => string
printHex: (value: number, length?: number) => string
setBinary: (index: number, value: number, bits?: number) => void
getOutputBinary: (index: number) => number
invertBinary: (value: number) => number invertBinary: (value: number) => number
color: (color: string) => void color: (color: string) => void
innerText: (value: string) => void innerText: (value: string) => void

View file

@ -46,6 +46,10 @@ div .gate-prop-container {
&.visible { &.visible {
margin: 1rem; margin: 1rem;
} }
&>* {
width: 100%;
}
} }
div .gate-prop-group-container { div .gate-prop-group-container {

View file

@ -1,9 +1,8 @@
import './GateProperties.scss' import './GateProperties.scss'
import React, { ChangeEvent } from 'react' import { ChangeEvent } from 'react'
import { getRendererSafely } from '../helpers/getRendererSafely' import { getRendererSafely } from '../helpers/getRendererSafely'
import { Property, RawProp, isGroup } from '../../simulation/types/GateTemplate' import { Property, RawProp, isGroup } from '../../simulation/types/GateTemplate'
import { useObservable } from 'rxjs-hooks' import { useObservable } from 'rxjs-hooks'
import Divider from '@material-ui/core/Divider'
import TextField from '@material-ui/core/TextField' import TextField from '@material-ui/core/TextField'
import CheckBox from '@material-ui/core/Checkbox' import CheckBox from '@material-ui/core/Checkbox'
import { open, id } from '../subjects/LogicGatePropsSubjects' import { open, id } from '../subjects/LogicGatePropsSubjects'
@ -70,15 +69,13 @@ const GateRawProperty = ({
// rerender when the external checkbox changes // rerender when the external checkbox changes
const external = useObservable( const external = useObservable(
() => () =>
gate.props.external.pipe( gate.props.external.pipe(map((value) => value && name !== 'external')),
map((value) => value && name !== 'external')
),
false false
) )
const displayableName = `${name[0].toUpperCase()}${name.slice(1)}${ const displayableName = `${name[0].toUpperCase()}${name.slice(1)}${
external && name !== 'label' ? ' (default value)' : '' external && name !== 'label' ? ' (default value)' : ''
}:` }${raw.description == undefined ? '' : ` ${raw.description}`}:`
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement const target = e.target as HTMLInputElement
@ -112,11 +109,7 @@ const GateRawProperty = ({
return emptyInput return emptyInput
} }
if ( if (raw.type === 'number' || raw.type === 'text' || raw.type === 'string') {
raw.type === 'number' ||
raw.type === 'text' ||
raw.type === 'string'
) {
return ( return (
<TextField <TextField
onChange={handleChange} onChange={handleChange}
@ -124,7 +117,6 @@ const GateRawProperty = ({
value={outputSnapshot} value={outputSnapshot}
type={raw.type} type={raw.type}
multiline={raw.type === 'string'} multiline={raw.type === 'string'}
rowsMax={7}
/> />
) )
} else if (raw.type === 'boolean') { } else if (raw.type === 'boolean') {
@ -145,9 +137,7 @@ const GateRawProperty = ({
return ( return (
<div <div
className={`gate-prop-container ${ className={`gate-prop-container ${input !== emptyInput ? 'visible' : ''}`}
input !== emptyInput ? 'visible' : ''
}`}
> >
{input} {input}
</div> </div>

View file

@ -20,6 +20,7 @@ import comparatorTemplate from './templates/comparator'
import bitMergerTemplate from './templates/bitMerger' import bitMergerTemplate from './templates/bitMerger'
import bitSplitterTemplate from './templates/bitSplitter' import bitSplitterTemplate from './templates/bitSplitter'
import incrementorTemplate from './templates/incrementor' import incrementorTemplate from './templates/incrementor'
import constantTemplate from './templates/constant'
export const defaultSimulationName = 'default' export const defaultSimulationName = 'default'
export const baseTemplates: DeepPartial<GateTemplate>[] = [ export const baseTemplates: DeepPartial<GateTemplate>[] = [
@ -42,7 +43,8 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
comparatorTemplate, comparatorTemplate,
bitMergerTemplate, bitMergerTemplate,
bitSplitterTemplate, bitSplitterTemplate,
incrementorTemplate incrementorTemplate,
constantTemplate
] ]
export const baseSave: RendererState = { export const baseSave: RendererState = {

View file

@ -0,0 +1,60 @@
import { PartialTemplate } from '../types/PartialTemplate'
import { categories } from '../data/categories'
/**
* The template of the button gate
*/
const constTemplate: PartialTemplate = {
metadata: {
name: 'constant'
},
material: {
fill: '#673AB7',
stroke: {
normal: '#EDC6FB'
}
},
code: {
activation: `
const state = context.getProperty('value')
const bits = context.getProperty('output bits')
const length = state.toString(2).length
const text = length > 10
? "0x" + context.printHex(state, Math.ceil(length/4))
: context.printBinary(state, length)
context.setBinary(0, state, bits === 0 ? length : bits)
context.innerText(text)
`
},
pins: {
inputs: {
count: 0
}
},
integration: {
input: true
},
info: [],
properties: {
enabled: true,
data: [
{
base: 0,
name: 'output bits',
description: '(0 for auto)',
type: 'number',
needsUpdate: true
},
{
base: 0,
name: 'value',
type: 'number',
needsUpdate: true
}
]
},
category: categories.io
}
export default constTemplate

View file

@ -9,12 +9,7 @@ import {
import { idStore } from '../stores/idStore' import { idStore } from '../stores/idStore'
import { Context, InitialisationContext } from '../../activation/types/Context' import { Context, InitialisationContext } from '../../activation/types/Context'
import { toFunction } from '../../activation/helpers/toFunction' import { toFunction } from '../../activation/helpers/toFunction'
import { import { Subscription, BehaviorSubject } from 'rxjs'
Subscription,
BehaviorSubject,
asapScheduler,
animationFrameScheduler
} from 'rxjs'
import { SimulationError } from '../../errors/classes/SimulationError' import { SimulationError } from '../../errors/classes/SimulationError'
import { getGateTimePipes } from '../helpers/getGateTimePipes' import { getGateTimePipes } from '../helpers/getGateTimePipes'
import { ImageStore } from '../../simulationRenderer/stores/imageStore' import { ImageStore } from '../../simulationRenderer/stores/imageStore'
@ -25,7 +20,6 @@ import { saveStore } from '../../saving/stores/saveStore'
import { Wire } from './Wire' import { Wire } from './Wire'
import { cleanSimulation } from '../../simulation-actions/helpers/clean' import { cleanSimulation } from '../../simulation-actions/helpers/clean'
import { ExecutionQueue } from '../../activation/classes/ExecutionQueue' import { ExecutionQueue } from '../../activation/classes/ExecutionQueue'
import { tap, observeOn } from 'rxjs/operators'
import { PropsSave } from '../../saving/types/SimulationSave' import { PropsSave } from '../../saving/types/SimulationSave'
import { ValueOf } from '../../../common/lang/record/types/ValueOf' import { ValueOf } from '../../../common/lang/record/types/ValueOf'
@ -139,7 +133,7 @@ export class Gate {
* Holds all the gate-related text * Holds all the gate-related text
*/ */
public text = { public text = {
inner: new BehaviorSubject('text goes here') inner: new BehaviorSubject<string | null>(null)
} }
/** /**
@ -170,21 +164,10 @@ export class Gate {
'context' 'context'
) )
this.functions.onClick = toFunction( this.functions.onClick = toFunction(this.template.code.onClick, 'context')
this.template.code.onClick,
'context'
)
this._pins.inputs = Gate.generatePins( this._pins.inputs = Gate.generatePins(this.template.pins.inputs, 1, this)
this.template.pins.inputs, this._pins.outputs = Gate.generatePins(this.template.pins.outputs, 2, this)
1,
this
)
this._pins.outputs = Gate.generatePins(
this.template.pins.outputs,
2,
this
)
if (this.template.material.type === 'image') { if (this.template.material.type === 'image') {
ImageStore.set(this.template.material.fill) ImageStore.set(this.template.material.fill)
@ -198,7 +181,7 @@ export class Gate {
const subscription = pin.state.pipe(...pipes).subscribe(() => { const subscription = pin.state.pipe(...pipes).subscribe(() => {
if (this.template.code.async) { if (this.template.code.async) {
this.executionQueue.push(async () => { this.executionQueue.push(async () => {
return await this.update() return this.update()
}) })
} else { } else {
this.update() this.update()
@ -305,7 +288,7 @@ export class Gate {
} }
/** /**
* Assign the props passed to the gate and mere them with the base ones * Assign the props passed to the gate and merge them with the base ones
*/ */
private assignProps( private assignProps(
source: PropsSave, source: PropsSave,
@ -313,14 +296,15 @@ export class Gate {
target: GateProps = this.props, target: GateProps = this.props,
path: string[] = [] path: string[] = []
) { ) {
let shouldUpdate = false // We don't want to update until every prop has been created
let lockUpdates = true
if (this.template.properties.enabled) { if (this.template.properties.enabled) {
for (const prop of props) { for (const prop of props) {
if (isGroup(prop)) { if (isGroup(prop)) {
const { groupName } = prop const { groupName } = prop
target[groupName] = {} as GateProps target[groupName] = {} as GateProps
const needsUpdate = this.assignProps( this.assignProps(
typeof source[groupName] === 'object' typeof source[groupName] === 'object'
? (source[groupName] as PropsSave) ? (source[groupName] as PropsSave)
: {}, : {},
@ -329,10 +313,6 @@ export class Gate {
[...path, groupName] [...path, groupName]
) )
if (needsUpdate) {
shouldUpdate = true
}
continue continue
} }
@ -346,7 +326,7 @@ export class Gate {
this.subscriptions.push( this.subscriptions.push(
subject.subscribe((value) => { subject.subscribe((value) => {
if (needsUpdate && path.length === 0) { if (!lockUpdates && needsUpdate && path.length === 0) {
return this.update() return this.update()
} }
@ -357,14 +337,11 @@ export class Gate {
this.updateNestedProp([...path, name], value) this.updateNestedProp([...path, name], value)
}) })
) )
if (needsUpdate) {
shouldUpdate = true
}
} }
} }
return shouldUpdate lockUpdates = false
this.update()
} }
/** /**
@ -446,7 +423,8 @@ export class Gate {
const toLength = ( const toLength = (
original: string | number, original: string | number,
length: number = maxLength length: number = maxLength,
paddingChar = '0'
) => { ) => {
const value = original.toString(2) const value = original.toString(2)
@ -455,31 +433,37 @@ export class Gate {
} else if (value.length > length) { } else if (value.length > length) {
const difference = value.length - length const difference = value.length - length
return value.substr(difference) return value.slice(difference)
} else { } else {
return `${'0'.repeat(length - value.length)}${value}` return `${paddingChar.repeat(length - value.length)}${value}`
} }
} }
return { return {
printBinary: (value: number, bits: number = maxLength) =>
toLength(value.toString(2), bits),
printHex: (value: number, bits: number = maxLength) =>
toLength(value.toString(16), bits),
get: (index: number) => { get: (index: number) => {
return this._pins.inputs[index].state.value return this._pins.inputs[index].state.value
}, },
set: (index: number, state) => { set: (index: number, state) => {
return this._pins.outputs[index].state.next(state) return this._pins.outputs[index].state.next(state)
}, },
getOutput: (index: number) => {
return this._pins.outputs[index].state.value
},
getBinary: (index: number) => { getBinary: (index: number) => {
return parseInt(this._pins.inputs[index].state.value, 2) return parseInt(this._pins.inputs[index].state.value, 2)
}, },
setBinary: ( setBinary: (index: number, value: number, bits: number = maxLength) => {
index: number,
value: number,
bits: number = maxLength
) => {
return this._pins.outputs[index].state.next( return this._pins.outputs[index].state.next(
toLength(value.toString(2), bits) toLength(value.toString(2), bits)
) )
}, },
getOutputBinary: (index: number) => {
return parseInt(this._pins.outputs[index].state.value, 2)
},
invertBinary: (value: number) => { invertBinary: (value: number) => {
return value ^ ((1 << maxLength) - 1) return value ^ ((1 << maxLength) - 1)
}, },
@ -489,7 +473,16 @@ export class Gate {
} }
}, },
getProperty: (name: string) => { getProperty: (name: string) => {
if (this.props[name] === undefined) {
throw new Error(
[
`Cannot find property ${name} on gate ${this.template.metadata.name}.`,
`Current values: ${Object.keys(this.props)}`
].join('\n')
)
} else {
return this.props[name].value return this.props[name].value
}
}, },
setProperty: (name: string, value: string | number | boolean) => { setProperty: (name: string, value: string | number | boolean) => {
const subject = this.props[name] const subject = this.props[name]
@ -551,6 +544,6 @@ export class Gate {
private static generatePins(options: PinCount, type: number, gate: Gate) { private static generatePins(options: PinCount, type: number, gate: Gate) {
return [...Array(options.count)] return [...Array(options.count)]
.fill(true) .fill(true)
.map((v, index) => new Pin(type, gate)) .map((_v, _index) => new Pin(type, gate))
} }
} }

View file

@ -21,6 +21,7 @@ export type RawProp<
type: 'number' | 'string' | 'text' | 'boolean' type: 'number' | 'string' | 'text' | 'boolean'
base: T base: T
name: string name: string
description?: string
needsUpdate?: boolean needsUpdate?: boolean
} }
export type Property< export type Property<

View file

@ -1,5 +1,5 @@
export const textSettings = { export const textSettings = {
font: '30px Roboto', font: '30px monospace',
offset: 35, offset: 35,
fill: `rgba(256,256,256,0.75)` fill: `rgba(256,256,256,0.75)`
} }

View file

@ -16,7 +16,7 @@ export const pinFill = (renderer: SimulationRenderer, pin: Pin) => {
.map((key) => .map((key) =>
chunked chunked
.flat() .flat()
.filter((v, index) => index % 3 === key) .filter((_v, index) => index % 3 === key)
.reduce((acc, curr) => acc + curr, 0) .reduce((acc, curr) => acc + curr, 0)
) )
.map((value) => Math.floor(value / digits.length)) .map((value) => Math.floor(value / digits.length))

View file

@ -61,6 +61,7 @@ export const renderGate = (
ctx.stroke() ctx.stroke()
if (gate.template.tags.includes('integrated')) { if (gate.template.tags.includes('integrated')) {
ctx.textBaseline = 'top'
ctx.fillStyle = textSettings.fill ctx.fillStyle = textSettings.fill
ctx.fillText( ctx.fillText(
gate.template.metadata.name, gate.template.metadata.name,
@ -69,5 +70,25 @@ export const renderGate = (
) )
} }
const text = gate.text.inner.value
if (text !== null) {
ctx.textBaseline = 'middle'
ctx.fillStyle = textSettings.fill
let size = 30
if (text.length >= 8) {
size = 15
} else if (text.length >= 6) {
size = 20
}
ctx.font = `${size}px monospace`
ctx.fillText(
text,
relativeTransform.center[0],
relativeTransform.center[1] + 2
)
}
ctx.restore() ctx.restore()
} }