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

@ -4,37 +4,39 @@ import { Splash } from './modules/splash/classes/Splash'
* The function wich is run when the app is loaded * The function wich is run when the app is loaded
*/ */
async function main() { async function main() {
// Create splash screen variable // Create splash screen variable
let splash: Splash | undefined = undefined let splash: Splash | undefined = undefined
try { try {
// instantiate splash screen // instantiate splash screen
splash = new Splash() splash = new Splash()
} catch {} } catch {}
try { try {
// import main app // import main app
const app = await import('./main') const app = await import('./main')
// wait for app to start // wait for app to start
await app.start() await app.start()
} catch (error) { } catch (error) {
// show the error to the client // show the error to the client
if (splash) splash.setError(error) if (splash) splash.setError(error)
// log the error to the console // log the error to the console
console.error(error.stack || error) console.error(error.stack || error)
return return
} }
// hide splash screen if it exists // hide splash screen if it exists
if (splash) { if (splash) {
splash.fade() splash.fade()
} }
} }
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,24 +1,28 @@
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 {
getProperty: (name: string) => unknown getProperty: (name: string) => unknown
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
getBinary: (index: number) => number getOutput: (index: number) => PinState
setBinary: (index: number, value: number, bits: number) => void getBinary: (index: number) => number
invertBinary: (value: number) => number printBinary: (value: number, bits?: number) => string
color: (color: string) => void printHex: (value: number, length?: number) => string
innerText: (value: string) => void setBinary: (index: number, value: number, bits?: number) => void
update: () => void getOutputBinary: (index: number) => number
toLength: (value: number | PinState, length: number) => string invertBinary: (value: number) => number
maxLength: number color: (color: string) => void
enviroment: SimulationEnv innerText: (value: string) => void
colors: Record<string, string> update: () => void
memory: Record<string, unknown> toLength: (value: number | PinState, length: number) => string
maxLength: number
enviroment: SimulationEnv
colors: Record<string, string>
memory: Record<string, unknown>
} }
export interface InitialisationContext { export interface InitialisationContext {
memory: Record<string, unknown> memory: Record<string, unknown>
} }

View file

@ -7,60 +7,64 @@
$gate-props-margin: 1rem; $gate-props-margin: 1rem;
#gate-properties-modal { #gate-properties-modal {
@include modal-container(); @include modal-container();
justify-content: center; justify-content: center;
} }
.visible#gate-properties-modal { .visible#gate-properties-modal {
@include visible(); @include visible();
} }
div #gate-properties-container { div #gate-properties-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: $grey; background-color: $grey;
border-radius: 1em; border-radius: 1em;
padding: $gate-props-margin * 4; padding: $gate-props-margin * 4;
box-sizing: border-box; box-sizing: border-box;
max-height: 80vh; max-height: 80vh;
overflow: auto; overflow: auto;
} }
div #gate-props-title { div #gate-props-title {
color: white; color: white;
font-size: 3em; font-size: 3em;
margin-bottom: 2 * $gate-props-margin; margin-bottom: 2 * $gate-props-margin;
} }
div .gate-prop-container { div .gate-prop-container {
@include flex(); @include flex();
flex-direction: row; flex-direction: row;
justify-content: start; justify-content: start;
&.visible { &.visible {
margin: 1rem; margin: 1rem;
} }
&>* {
width: 100%;
}
} }
div .gate-prop-group-container { div .gate-prop-group-container {
@include flex; @include flex;
margin-left: 1rem; margin-left: 1rem;
} }
div #save-props { div #save-props {
width: 50%; width: 50%;
margin: $gate-props-margin * 2; margin: $gate-props-margin * 2;
margin-bottom: 0; margin-bottom: 0;
} }
.checkbox-label { .checkbox-label {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
} }

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'
@ -16,41 +15,41 @@ import Typography from '@material-ui/core/Typography'
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails' import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'
import Icon from '@material-ui/core/Icon' import Icon from '@material-ui/core/Icon'
interface GatePropertyProps<T extends Property = Property> { interface GatePropertyProps<T extends Property = Property> {
raw: T raw: T
gate: Gate gate: Gate
props: GateProps props: GateProps
} }
const emptyInput = <></> const emptyInput = <></>
const GateProperty = ({ raw, props, gate }: GatePropertyProps) => { const GateProperty = ({ raw, props, gate }: GatePropertyProps) => {
if (isGroup(raw)) { if (isGroup(raw)) {
return ( return (
<ExpansionPanel> <ExpansionPanel>
<ExpansionPanelSummary <ExpansionPanelSummary
expandIcon={<Icon> expand_more</Icon>} expandIcon={<Icon> expand_more</Icon>}
aria-controls={raw.groupName} aria-controls={raw.groupName}
id={raw.groupName} id={raw.groupName}
> >
<Typography>{raw.groupName}</Typography> <Typography>{raw.groupName}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails> <ExpansionPanelDetails>
<div className="gate-prop-group-container"> <div className="gate-prop-group-container">
{raw.props.map((propTemplate, index) => ( {raw.props.map((propTemplate, index) => (
<GateProperty <GateProperty
key={index} key={index}
raw={propTemplate} raw={propTemplate}
gate={gate} gate={gate}
props={props[raw.groupName] as GateProps} props={props[raw.groupName] as GateProps}
/> />
))} ))}
</div> </div>
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
) )
} }
return <GateRawProperty raw={raw} gate={gate} props={props} /> return <GateRawProperty raw={raw} gate={gate} props={props} />
} }
/** /**
@ -59,99 +58,90 @@ const GateProperty = ({ raw, props, gate }: GatePropertyProps) => {
* @param param0 The props passed to the component * @param param0 The props passed to the component
*/ */
const GateRawProperty = ({ const GateRawProperty = ({
props, props,
raw, raw,
gate gate
}: GatePropertyProps & { raw: RawProp }) => { }: GatePropertyProps & { raw: RawProp }) => {
const { name } = raw const { name } = raw
const prop = props[raw.name] as BehaviorSubject<string | number | boolean> const prop = props[raw.name] as BehaviorSubject<string | number | boolean>
const outputSnapshot = useObservable(() => prop, '') const outputSnapshot = useObservable(() => prop, '')
// 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
let value: boolean | string | number = target.value let value: boolean | string | number = target.value
if (raw.type === 'boolean') { if (raw.type === 'boolean') {
value = target.checked value = target.checked
} else if (raw.type === 'number') { } else if (raw.type === 'number') {
value = Number(target.value) value = Number(target.value)
}
if (raw.type !== 'boolean') {
prop.next(value)
}
} }
let input = (() => { if (raw.type !== 'boolean') {
const root = gate.props[name] === prop prop.next(value)
const renderer = getRendererSafely() }
const displayExternal = () => }
renderer.simulation.mode === 'ic' &&
root &&
!gate.template.properties.data.some(
(prop) => (prop as RawProp).needsUpdate
)
if ( let input = (() => {
(raw.name === 'external' && !displayExternal()) || const root = gate.props[name] === prop
(raw.name === 'label' && (!external || !root)) const renderer = getRendererSafely()
) { const displayExternal = () =>
return emptyInput renderer.simulation.mode === 'ic' &&
} root &&
!gate.template.properties.data.some(
(prop) => (prop as RawProp).needsUpdate
)
if ( if (
raw.type === 'number' || (raw.name === 'external' && !displayExternal()) ||
raw.type === 'text' || (raw.name === 'label' && (!external || !root))
raw.type === 'string' ) {
) { return emptyInput
return ( }
<TextField
onChange={handleChange}
label={displayableName}
value={outputSnapshot}
type={raw.type}
multiline={raw.type === 'string'}
rowsMax={7}
/>
)
} else if (raw.type === 'boolean') {
return (
<>
<span className="checkbox-label">{displayableName}</span>
<CheckBox
onClick={() => {
prop.next(!outputSnapshot)
}}
onChange={handleChange}
checked={!!outputSnapshot}
/>{' '}
</>
)
}
})()
return ( if (raw.type === 'number' || raw.type === 'text' || raw.type === 'string') {
<div return (
className={`gate-prop-container ${ <TextField
input !== emptyInput ? 'visible' : '' onChange={handleChange}
}`} label={displayableName}
> value={outputSnapshot}
{input} type={raw.type}
</div> multiline={raw.type === 'string'}
) />
)
} else if (raw.type === 'boolean') {
return (
<>
<span className="checkbox-label">{displayableName}</span>
<CheckBox
onClick={() => {
prop.next(!outputSnapshot)
}}
onChange={handleChange}
checked={!!outputSnapshot}
/>{' '}
</>
)
}
})()
return (
<div
className={`gate-prop-container ${input !== emptyInput ? 'visible' : ''}`}
>
{input}
</div>
)
} }
/** /**
@ -160,47 +150,47 @@ const GateRawProperty = ({
* @param props The react props of the component * @param props The react props of the component
*/ */
const GateProperties = () => { const GateProperties = () => {
const openSnapshot = useObservable(() => open, false) const openSnapshot = useObservable(() => open, false)
const renderer = getRendererSafely() const renderer = getRendererSafely()
const node = renderer.simulation.gates.get(id.value) const node = renderer.simulation.gates.get(id.value)
if (!(node && node.data && node.data.template.properties.enabled)) { if (!(node && node.data && node.data.template.properties.enabled)) {
open.next(false)
return <></>
}
const gate = node.data
return (
<div
id="gate-properties-modal"
className={openSnapshot ? 'visible' : ''}
onClick={() => {
open.next(false) open.next(false)
return <></> }}
} >
<div
id="gate-properties-container"
onClick={(e) => {
e.stopPropagation()
}}
>
<div id="gate-props-title">Gate properties</div>
const gate = node.data {gate.template.properties.data.map((prop, index) => {
return (
return ( <GateProperty
<div props={gate.props}
id="gate-properties-modal" raw={prop}
className={openSnapshot ? 'visible' : ''} gate={gate}
onClick={() => { key={`${index}-${id.value}`}
open.next(false) />
}} )
> })}
<div </div>
id="gate-properties-container" </div>
onClick={(e) => { )
e.stopPropagation()
}}
>
<div id="gate-props-title">Gate properties</div>
{gate.template.properties.data.map((prop, index) => {
return (
<GateProperty
props={gate.props}
raw={prop}
gate={gate}
key={`${index}-${id.value}`}
/>
)
})}
</div>
</div>
)
} }
export default GateProperties export default GateProperties

View file

@ -3,7 +3,7 @@ import { GateTemplate } from '../../simulation/types/GateTemplate'
import { DefaultGateTemplate } from '../../simulation/constants' import { DefaultGateTemplate } from '../../simulation/constants'
export const completeTemplate = (template: DeepPartial<GateTemplate>) => { export const completeTemplate = (template: DeepPartial<GateTemplate>) => {
return merge(DefaultGateTemplate, template, { return merge(DefaultGateTemplate, template, {
arrayMerge: (a: unknown[], b: unknown[]) => a.concat(b) arrayMerge: (a: unknown[], b: unknown[]) => a.concat(b)
}) as GateTemplate }) as GateTemplate
} }

View file

@ -20,43 +20,45 @@ 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>[] = [
andTemplate, andTemplate,
buttonTemplate, buttonTemplate,
lightTemplate, lightTemplate,
nandTemplate, nandTemplate,
norTemplate, norTemplate,
notTemplate, notTemplate,
orTemplate, orTemplate,
parallelDelayerTemplate, parallelDelayerTemplate,
rgbLightTemplate, rgbLightTemplate,
sequentialDelayerTemplate, sequentialDelayerTemplate,
xnorTemplate, xnorTemplate,
xorTemplate, xorTemplate,
halfAdderTemplate, halfAdderTemplate,
fullAdderTemplate, fullAdderTemplate,
_4bitEncoderTemplate, _4bitEncoderTemplate,
_4bitDecoderTemplate, _4bitDecoderTemplate,
comparatorTemplate, comparatorTemplate,
bitMergerTemplate, bitMergerTemplate,
bitSplitterTemplate, bitSplitterTemplate,
incrementorTemplate incrementorTemplate,
constantTemplate
] ]
export const baseSave: RendererState = { export const baseSave: RendererState = {
camera: { camera: {
transform: { transform: {
position: [0, 0], position: [0, 0],
scale: [1, 1], scale: [1, 1],
rotation: 0 rotation: 0
}
},
simulation: {
gates: [],
mode: 'project',
wires: [],
name: 'default'
} }
},
simulation: {
gates: [],
mode: 'project',
wires: [],
name: 'default'
}
} }

View file

@ -1,9 +1,9 @@
export const categories = { export const categories = {
basic: 0, basic: 0,
math: 1, math: 1,
time: 2, time: 2,
compressing: 3, compressing: 3,
io: 4, io: 4,
advancedIo: 5, advancedIo: 5,
ic: 6 ic: 6
} }

View file

@ -8,13 +8,13 @@ import { SimulationError } from '../../errors/classes/SimulationError'
* @throws SimulationError if something is wrong with the template * @throws SimulationError if something is wrong with the template
*/ */
export const initBaseTemplates = () => { export const initBaseTemplates = () => {
for (const template of baseTemplates) { for (const template of baseTemplates) {
if (template.metadata && template.metadata.name) { if (template.metadata && template.metadata.name) {
templateStore.set(template.metadata.name, template) templateStore.set(template.metadata.name, template)
} else { } else {
throw new SimulationError( throw new SimulationError(
`Template ${JSON.stringify(template)} cannot be stored.` `Template ${JSON.stringify(template)} cannot be stored.`
) )
}
} }
}
} }

View file

@ -4,44 +4,44 @@ import { PartialTemplate } from '../types/PartialTemplate'
* The template of the comment gate * The template of the comment gate
*/ */
const commentTemplate: PartialTemplate = { const commentTemplate: PartialTemplate = {
metadata: { metadata: {
name: 'comment' name: 'comment'
},
pins: {
inputs: {
count: 0
}, },
pins: { outputs: {
inputs: { count: 0
count: 0
},
outputs: {
count: 0
}
},
material: {
fill: '#007A72'
},
shape: {
scale: [300, 100]
},
code: {
activation: `
context.innerText(context.getProperty('content'))
`
},
info: ['https://en.wikipedia.org/wiki/Comment_(computer_programming)'],
properties: {
enabled: true,
data: [
{
needsUpdate: true,
base: 'Your comment here',
name: 'content',
type: 'string'
}
]
},
innerText: {
enabled: true,
color: '#ADFFFA'
} }
},
material: {
fill: '#007A72'
},
shape: {
scale: [300, 100]
},
code: {
activation: `
context.innerText(context.getProperty('content'))
`
},
info: ['https://en.wikipedia.org/wiki/Comment_(computer_programming)'],
properties: {
enabled: true,
data: [
{
needsUpdate: true,
base: 'Your comment here',
name: 'content',
type: 'string'
}
]
},
innerText: {
enabled: true,
color: '#ADFFFA'
}
} }
export default commentTemplate export default commentTemplate

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

@ -5,40 +5,40 @@ import { categories } from '../data/categories'
* The template of the light gate * The template of the light gate
*/ */
const lightTemplate: PartialTemplate = { const lightTemplate: PartialTemplate = {
metadata: { metadata: {
name: 'light bulb' name: 'light bulb'
},
shape: {
radius: 50
},
material: {
fill: '#1C1C1C',
stroke: {
normal: '#3C3C3C'
}, },
shape: { colors: {
radius: 50 active: '#C6FF00'
}, }
material: { },
fill: '#1C1C1C', code: {
stroke: { activation: `
normal: '#3C3C3C'
},
colors: {
active: '#C6FF00'
}
},
code: {
activation: `
const { main, active } = context.colors const { main, active } = context.colors
const bits = context.get(0) const bits = context.get(0)
context.color(parseInt(context.get(0),2) ? active : main) context.color(parseInt(context.get(0),2) ? active : main)
` `
}, },
integration: { integration: {
output: true output: true
}, },
info: ['https://en.wikipedia.org/wiki/Incandescent_light_bulb'], info: ['https://en.wikipedia.org/wiki/Incandescent_light_bulb'],
pins: { pins: {
outputs: { outputs: {
count: 0 count: 0
} }
}, },
category: categories.io category: categories.io
} }
export default lightTemplate export default lightTemplate

File diff suppressed because it is too large Load diff

View file

@ -3,80 +3,80 @@ import { categories } from '../saving/data/categories'
import { getRendererSafely } from '../logic-gates/helpers/getRendererSafely' import { getRendererSafely } from '../logic-gates/helpers/getRendererSafely'
export const DefaultGateTemplate: GateTemplate = { export const DefaultGateTemplate: GateTemplate = {
metadata: { metadata: {
name: 'Default template' name: 'Default template'
},
material: {
type: 'color',
fill: 'blue',
stroke: {
active: '#76FF02',
normal: '#3FC4FF'
}, },
material: { colors: {}
type: 'color', },
fill: 'blue', pins: {
stroke: { inputs: {
active: '#76FF02', count: 1,
normal: '#3FC4FF' variable: false
},
colors: {}
}, },
pins: { outputs: {
inputs: { count: 1,
count: 1, variable: false
variable: false }
}, },
outputs: { shape: {
count: 1, radius: 10,
variable: false rounded: true,
} scale: [100, 100]
},
code: {
async: false,
activation: '',
onClick: '',
initialisation: ''
},
simulation: {
debounce: {
enabled: true,
time: 1000 / 60
}, },
shape: { throttle: {
radius: 10, enabled: false
rounded: true, }
scale: [100, 100] },
}, integration: {
code: { allowed: true,
async: false, input: false,
activation: '', output: false
onClick: '', },
initialisation: '' tags: ['base'],
}, properties: {
simulation: { enabled: false,
debounce: { data: [
enabled: true, {
time: 1000 / 60 type: 'boolean',
}, base: false,
throttle: { name: 'external'
enabled: false },
} {
}, type: 'string',
integration: { base: 'my-logic-gate',
allowed: true, name: 'label'
input: false, }
output: false ]
}, },
tags: ['base'], innerText: {
properties: { enabled: false,
enabled: false, color: 'white'
data: [ },
{ category: categories.basic,
type: 'boolean', info: []
base: false,
name: 'external'
},
{
type: 'string',
base: 'my-logic-gate',
name: 'label'
}
]
},
innerText: {
enabled: false,
color: 'white'
},
category: categories.basic,
info: []
} }
/** /**
* Prop names which need to not be overriten * Prop names which need to not be overriten
*/ */
export const reservedPropNames = DefaultGateTemplate.properties.data.map( export const reservedPropNames = DefaultGateTemplate.properties.data.map(
({ name }: RawProp) => name ({ name }: RawProp) => name
) )

View file

@ -1,96 +1,97 @@
import { vector2 } from '../../../common/math/types/vector2' import { vector2 } from '../../../common/math/types/vector2'
export interface PinCount { export interface PinCount {
variable: boolean variable: boolean
count: number count: number
} }
export type PropGroup< export type PropGroup<
T extends boolean | number | string = boolean | number | string T extends boolean | number | string = boolean | number | string
> = { > = {
groupName: string groupName: string
props: Property<T>[] props: Property<T>[]
} }
export const isGroup = (prop: Property): prop is PropGroup => export const isGroup = (prop: Property): prop is PropGroup =>
(prop as PropGroup).groupName !== undefined (prop as PropGroup).groupName !== undefined
export type RawProp< export type RawProp<
T extends boolean | number | string = boolean | number | string T extends boolean | number | string = boolean | number | string
> = { > = {
type: 'number' | 'string' | 'text' | 'boolean' type: 'number' | 'string' | 'text' | 'boolean'
base: T base: T
name: string name: string
needsUpdate?: boolean description?: string
needsUpdate?: boolean
} }
export type Property< export type Property<
T extends boolean | number | string = boolean | number | string T extends boolean | number | string = boolean | number | string
> = PropGroup<T> | RawProp<T> > = PropGroup<T> | RawProp<T>
export interface Material { export interface Material {
type: 'color' | 'image' type: 'color' | 'image'
fill: string fill: string
stroke: { stroke: {
active: string active: string
normal: string normal: string
} }
colors: Record<string, string> colors: Record<string, string>
} }
export interface Shape { export interface Shape {
rounded: boolean rounded: boolean
radius: number radius: number
scale: vector2 scale: vector2
} }
export type Enabled<T> = export type Enabled<T> =
| { | {
enabled: false enabled: false
} }
| ({ | ({
enabled: true enabled: true
} & T) } & T)
export type TimePipe = Enabled<{ export type TimePipe = Enabled<{
time: number time: number
}> }>
export type GateTag = 'base' | 'imported' | 'integrated' export type GateTag = 'base' | 'imported' | 'integrated'
export interface GateTemplate { export interface GateTemplate {
material: Material material: Material
shape: Shape shape: Shape
pins: { pins: {
inputs: PinCount inputs: PinCount
outputs: PinCount outputs: PinCount
} }
metadata: { metadata: {
name: string name: string
} }
code: { code: {
async: boolean async: boolean
initialisation: string initialisation: string
activation: string activation: string
onClick: string onClick: string
} }
simulation: { simulation: {
throttle: TimePipe throttle: TimePipe
debounce: TimePipe debounce: TimePipe
} }
integration: { integration: {
allowed: boolean allowed: boolean
input: boolean input: boolean
output: boolean output: boolean
} }
tags: GateTag[] tags: GateTag[]
properties: { properties: {
enabled: boolean enabled: boolean
data: Property[] data: Property[]
} }
innerText: { innerText: {
color: string color: string
enabled: boolean enabled: boolean
} }
category: number // for better sorting category: number // for better sorting
info: string[] info: string[]
} }

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

@ -10,64 +10,85 @@ import { idIsSelected } from './idIsSelected'
import { textSettings } from '../data/textSettings' import { textSettings } from '../data/textSettings'
export const renderGate = ( export const renderGate = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
renderer: SimulationRenderer, renderer: SimulationRenderer,
gate: Gate gate: Gate
) => { ) => {
const { active, normal } = gate.template.material.stroke const { active, normal } = gate.template.material.stroke
const selected = const selected =
(renderer.mouseState >> 2 && (renderer.mouseState >> 2 &&
!!gatesInSelection(renderer.selectedArea, [gate]).length) || !!gatesInSelection(renderer.selectedArea, [gate]).length) ||
idIsSelected(renderer, gate.id) idIsSelected(renderer, gate.id)
renderPins(ctx, renderer, gate, selected) renderPins(ctx, renderer, gate, selected)
if (selected) { if (selected) {
ctx.strokeStyle = active ctx.strokeStyle = active
} else { } else {
ctx.strokeStyle = normal ctx.strokeStyle = normal
}
ctx.lineWidth = renderer.options.gates.gateStroke.width
ctx.save()
const relativeTransform = useTransform(ctx, gate.transform)
const renderingParameters = [
relativeTransform.x,
relativeTransform.y,
relativeTransform.width,
relativeTransform.height,
gate.template.shape.rounded ? gate.template.shape.radius : 0
]
if (gate.template.material.type === 'image') {
roundImage(
ctx,
ImageStore.get(gate.template.material.fill),
...renderingParameters
)
}
roundRect(ctx, ...renderingParameters)
if (gate.template.material.type === 'color') {
ctx.fillStyle = gate.template.material.fill
ctx.fill()
}
ctx.stroke()
if (gate.template.tags.includes('integrated')) {
ctx.textBaseline = 'top'
ctx.fillStyle = textSettings.fill
ctx.fillText(
gate.template.metadata.name,
relativeTransform.center[0],
relativeTransform.maxY + textSettings.offset
)
}
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.lineWidth = renderer.options.gates.gateStroke.width ctx.font = `${size}px monospace`
ctx.fillText(
text,
relativeTransform.center[0],
relativeTransform.center[1] + 2
)
}
ctx.save() ctx.restore()
const relativeTransform = useTransform(ctx, gate.transform)
const renderingParameters = [
relativeTransform.x,
relativeTransform.y,
relativeTransform.width,
relativeTransform.height,
gate.template.shape.rounded ? gate.template.shape.radius : 0
]
if (gate.template.material.type === 'image') {
roundImage(
ctx,
ImageStore.get(gate.template.material.fill),
...renderingParameters
)
}
roundRect(ctx, ...renderingParameters)
if (gate.template.material.type === 'color') {
ctx.fillStyle = gate.template.material.fill
ctx.fill()
}
ctx.stroke()
if (gate.template.tags.includes('integrated')) {
ctx.fillStyle = textSettings.fill
ctx.fillText(
gate.template.metadata.name,
relativeTransform.center[0],
relativeTransform.maxY + textSettings.offset
)
}
ctx.restore()
} }