diff --git a/build.js b/build.js index 99c7b2a..5809460 100644 --- a/build.js +++ b/build.js @@ -43,7 +43,11 @@ const ctx = await esbuild.context({ }) 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}`) } else { await ctx.rebuild() diff --git a/package.json b/package.json index 8479a85..ba2264d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { + "dev": "ESBUILD_SERVE=1 node ./build.js", "build": "node ./build.js", "check": "tsc" }, diff --git a/src/index.ts b/src/index.ts index e46a089..df2c8fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,37 +4,39 @@ import { Splash } from './modules/splash/classes/Splash' * The function wich is run when the app is loaded */ async function main() { - // Create splash screen variable - let splash: Splash | undefined = undefined + // Create splash screen variable + let splash: Splash | undefined = undefined - try { - // instantiate splash screen - splash = new Splash() - } catch {} + try { + // instantiate splash screen + splash = new Splash() + } catch {} - try { - // import main app - const app = await import('./main') + try { + // import main app + const app = await import('./main') - // wait for app to start - await app.start() - } catch (error) { - // show the error to the client - if (splash) splash.setError(error) + // wait for app to start + await app.start() + } catch (error) { + // show the error to the client + if (splash) splash.setError(error) - // log the error to the console - console.error(error.stack || error) - return - } + // log the error to the console + console.error(error.stack || error) + return + } - // hide splash screen if it exists - if (splash) { - splash.fade() - } + // hide splash screen if it exists + if (splash) { + splash.fade() + } } +new EventSource('/esbuild').addEventListener('change', () => location.reload()) + // Call entry -main().catch(error => { - // if the error handling error has an error, log that error - console.error('Error loading app', error) +main().catch((error) => { + // if the error handling error has an error, log that error + console.error('Error loading app', error) }) diff --git a/src/modules/activation/types/Context.ts b/src/modules/activation/types/Context.ts index 11df2c8..da6dcb1 100644 --- a/src/modules/activation/types/Context.ts +++ b/src/modules/activation/types/Context.ts @@ -1,24 +1,28 @@ -import { Simulation, SimulationEnv } from '../../simulation/classes/Simulation' +import { SimulationEnv } from '../../simulation/classes/Simulation' import { PinState } from '../../simulation/classes/Pin' export interface Context { - getProperty: (name: string) => unknown - setProperty: (name: string, value: unknown) => void - get: (index: number) => PinState - set: (index: number, state: PinState) => void - getBinary: (index: number) => number - setBinary: (index: number, value: number, bits: number) => void - invertBinary: (value: number) => number - color: (color: string) => void - innerText: (value: string) => void - update: () => void - toLength: (value: number | PinState, length: number) => string - maxLength: number - enviroment: SimulationEnv - colors: Record - memory: Record + getProperty: (name: string) => unknown + setProperty: (name: string, value: unknown) => void + get: (index: number) => PinState + set: (index: number, state: PinState) => void + getOutput: (index: number) => PinState + getBinary: (index: number) => number + 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 + color: (color: string) => void + innerText: (value: string) => void + update: () => void + toLength: (value: number | PinState, length: number) => string + maxLength: number + enviroment: SimulationEnv + colors: Record + memory: Record } export interface InitialisationContext { - memory: Record + memory: Record } diff --git a/src/modules/logic-gates/components/GateProperties.scss b/src/modules/logic-gates/components/GateProperties.scss index 22cb619..2d4386e 100644 --- a/src/modules/logic-gates/components/GateProperties.scss +++ b/src/modules/logic-gates/components/GateProperties.scss @@ -7,60 +7,64 @@ $gate-props-margin: 1rem; #gate-properties-modal { - @include modal-container(); + @include modal-container(); - justify-content: center; + justify-content: center; } .visible#gate-properties-modal { - @include visible(); + @include visible(); } div #gate-properties-container { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; - background-color: $grey; - border-radius: 1em; + background-color: $grey; + border-radius: 1em; - padding: $gate-props-margin * 4; - box-sizing: border-box; + padding: $gate-props-margin * 4; + box-sizing: border-box; - max-height: 80vh; - overflow: auto; + max-height: 80vh; + overflow: auto; } div #gate-props-title { - color: white; - font-size: 3em; + color: white; + font-size: 3em; - margin-bottom: 2 * $gate-props-margin; + margin-bottom: 2 * $gate-props-margin; } div .gate-prop-container { - @include flex(); + @include flex(); - flex-direction: row; - justify-content: start; + flex-direction: row; + justify-content: start; - &.visible { - margin: 1rem; - } + &.visible { + margin: 1rem; + } + + &>* { + width: 100%; + } } div .gate-prop-group-container { - @include flex; + @include flex; - margin-left: 1rem; + margin-left: 1rem; } div #save-props { - width: 50%; - margin: $gate-props-margin * 2; - margin-bottom: 0; + width: 50%; + margin: $gate-props-margin * 2; + margin-bottom: 0; } .checkbox-label { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, - Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } diff --git a/src/modules/logic-gates/components/GatePropertiesModal.tsx b/src/modules/logic-gates/components/GatePropertiesModal.tsx index 8e9991e..c758be0 100644 --- a/src/modules/logic-gates/components/GatePropertiesModal.tsx +++ b/src/modules/logic-gates/components/GatePropertiesModal.tsx @@ -1,9 +1,8 @@ import './GateProperties.scss' -import React, { ChangeEvent } from 'react' +import { ChangeEvent } from 'react' import { getRendererSafely } from '../helpers/getRendererSafely' 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' @@ -16,41 +15,41 @@ import Typography from '@material-ui/core/Typography' import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails' import Icon from '@material-ui/core/Icon' interface GatePropertyProps { - raw: T - gate: Gate - props: GateProps + raw: T + gate: Gate + props: GateProps } const emptyInput = <> const GateProperty = ({ raw, props, gate }: GatePropertyProps) => { - if (isGroup(raw)) { - return ( - - expand_more} - aria-controls={raw.groupName} - id={raw.groupName} - > - {raw.groupName} - - -
- {raw.props.map((propTemplate, index) => ( - - ))} -
-
-
- ) - } + if (isGroup(raw)) { + return ( + + expand_more} + aria-controls={raw.groupName} + id={raw.groupName} + > + {raw.groupName} + + +
+ {raw.props.map((propTemplate, index) => ( + + ))} +
+
+
+ ) + } - return + return } /** @@ -59,99 +58,90 @@ const GateProperty = ({ raw, props, gate }: GatePropertyProps) => { * @param param0 The props passed to the component */ const GateRawProperty = ({ - props, - raw, - gate + props, + raw, + gate }: GatePropertyProps & { raw: RawProp }) => { - const { name } = raw - const prop = props[raw.name] as BehaviorSubject - const outputSnapshot = useObservable(() => prop, '') + const { name } = raw + const prop = props[raw.name] as BehaviorSubject + const outputSnapshot = useObservable(() => prop, '') - // rerender when the external checkbox changes - const external = useObservable( - () => - gate.props.external.pipe( - map((value) => value && name !== 'external') - ), - false - ) + // rerender when the external checkbox changes + const external = useObservable( + () => + gate.props.external.pipe(map((value) => value && name !== 'external')), + false + ) - const displayableName = `${name[0].toUpperCase()}${name.slice(1)} ${ - external && name !== 'label' ? '(default value)' : '' - }:` + const displayableName = `${name[0].toUpperCase()}${name.slice(1)}${ + external && name !== 'label' ? ' (default value)' : '' + }${raw.description == undefined ? '' : ` ${raw.description}`}:` - const handleChange = (e: ChangeEvent) => { - const target = e.target as HTMLInputElement - let value: boolean | string | number = target.value + const handleChange = (e: ChangeEvent) => { + const target = e.target as HTMLInputElement + let value: boolean | string | number = target.value - if (raw.type === 'boolean') { - value = target.checked - } else if (raw.type === 'number') { - value = Number(target.value) - } - - if (raw.type !== 'boolean') { - prop.next(value) - } + if (raw.type === 'boolean') { + value = target.checked + } else if (raw.type === 'number') { + value = Number(target.value) } - let input = (() => { - const root = gate.props[name] === prop - const renderer = getRendererSafely() - const displayExternal = () => - renderer.simulation.mode === 'ic' && - root && - !gate.template.properties.data.some( - (prop) => (prop as RawProp).needsUpdate - ) + if (raw.type !== 'boolean') { + prop.next(value) + } + } - if ( - (raw.name === 'external' && !displayExternal()) || - (raw.name === 'label' && (!external || !root)) - ) { - return emptyInput - } + let input = (() => { + const root = gate.props[name] === prop + const renderer = getRendererSafely() + const displayExternal = () => + renderer.simulation.mode === 'ic' && + root && + !gate.template.properties.data.some( + (prop) => (prop as RawProp).needsUpdate + ) - if ( - raw.type === 'number' || - raw.type === 'text' || - raw.type === 'string' - ) { - return ( - - ) - } else if (raw.type === 'boolean') { - return ( - <> - {displayableName} - { - prop.next(!outputSnapshot) - }} - onChange={handleChange} - checked={!!outputSnapshot} - />{' '} - - ) - } - })() + if ( + (raw.name === 'external' && !displayExternal()) || + (raw.name === 'label' && (!external || !root)) + ) { + return emptyInput + } - return ( -
- {input} -
- ) + if (raw.type === 'number' || raw.type === 'text' || raw.type === 'string') { + return ( + + ) + } else if (raw.type === 'boolean') { + return ( + <> + {displayableName} + { + prop.next(!outputSnapshot) + }} + onChange={handleChange} + checked={!!outputSnapshot} + />{' '} + + ) + } + })() + + return ( +
+ {input} +
+ ) } /** @@ -160,47 +150,47 @@ const GateRawProperty = ({ * @param props The react props of the component */ const GateProperties = () => { - const openSnapshot = useObservable(() => open, false) - const renderer = getRendererSafely() + const openSnapshot = useObservable(() => open, false) + 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 ( +
{ open.next(false) - return <> - } + }} + > +
{ + e.stopPropagation() + }} + > +
Gate properties
- const gate = node.data - - return ( -
{ - open.next(false) - }} - > -
{ - e.stopPropagation() - }} - > -
Gate properties
- - {gate.template.properties.data.map((prop, index) => { - return ( - - ) - })} -
-
- ) + {gate.template.properties.data.map((prop, index) => { + return ( + + ) + })} +
+
+ ) } export default GateProperties diff --git a/src/modules/logic-gates/helpers/completeTemplate.ts b/src/modules/logic-gates/helpers/completeTemplate.ts index b2f0e3a..e9da404 100644 --- a/src/modules/logic-gates/helpers/completeTemplate.ts +++ b/src/modules/logic-gates/helpers/completeTemplate.ts @@ -3,7 +3,7 @@ import { GateTemplate } from '../../simulation/types/GateTemplate' import { DefaultGateTemplate } from '../../simulation/constants' export const completeTemplate = (template: DeepPartial) => { - return merge(DefaultGateTemplate, template, { - arrayMerge: (a: unknown[], b: unknown[]) => a.concat(b) - }) as GateTemplate + return merge(DefaultGateTemplate, template, { + arrayMerge: (a: unknown[], b: unknown[]) => a.concat(b) + }) as GateTemplate } diff --git a/src/modules/saving/constants.ts b/src/modules/saving/constants.ts index d8ac7d9..8aaed7c 100644 --- a/src/modules/saving/constants.ts +++ b/src/modules/saving/constants.ts @@ -20,43 +20,45 @@ import comparatorTemplate from './templates/comparator' import bitMergerTemplate from './templates/bitMerger' import bitSplitterTemplate from './templates/bitSplitter' import incrementorTemplate from './templates/incrementor' +import constantTemplate from './templates/constant' export const defaultSimulationName = 'default' export const baseTemplates: DeepPartial[] = [ - andTemplate, - buttonTemplate, - lightTemplate, - nandTemplate, - norTemplate, - notTemplate, - orTemplate, - parallelDelayerTemplate, - rgbLightTemplate, - sequentialDelayerTemplate, - xnorTemplate, - xorTemplate, - halfAdderTemplate, - fullAdderTemplate, - _4bitEncoderTemplate, - _4bitDecoderTemplate, - comparatorTemplate, - bitMergerTemplate, - bitSplitterTemplate, - incrementorTemplate + andTemplate, + buttonTemplate, + lightTemplate, + nandTemplate, + norTemplate, + notTemplate, + orTemplate, + parallelDelayerTemplate, + rgbLightTemplate, + sequentialDelayerTemplate, + xnorTemplate, + xorTemplate, + halfAdderTemplate, + fullAdderTemplate, + _4bitEncoderTemplate, + _4bitDecoderTemplate, + comparatorTemplate, + bitMergerTemplate, + bitSplitterTemplate, + incrementorTemplate, + constantTemplate ] export const baseSave: RendererState = { - camera: { - transform: { - position: [0, 0], - scale: [1, 1], - rotation: 0 - } - }, - simulation: { - gates: [], - mode: 'project', - wires: [], - name: 'default' + camera: { + transform: { + position: [0, 0], + scale: [1, 1], + rotation: 0 } + }, + simulation: { + gates: [], + mode: 'project', + wires: [], + name: 'default' + } } diff --git a/src/modules/saving/data/categories.ts b/src/modules/saving/data/categories.ts index 426c495..d8f0bc8 100644 --- a/src/modules/saving/data/categories.ts +++ b/src/modules/saving/data/categories.ts @@ -1,9 +1,9 @@ export const categories = { - basic: 0, - math: 1, - time: 2, - compressing: 3, - io: 4, - advancedIo: 5, - ic: 6 + basic: 0, + math: 1, + time: 2, + compressing: 3, + io: 4, + advancedIo: 5, + ic: 6 } diff --git a/src/modules/saving/helpers/initBaseTemplates.ts b/src/modules/saving/helpers/initBaseTemplates.ts index 2a2badc..817d861 100644 --- a/src/modules/saving/helpers/initBaseTemplates.ts +++ b/src/modules/saving/helpers/initBaseTemplates.ts @@ -8,13 +8,13 @@ import { SimulationError } from '../../errors/classes/SimulationError' * @throws SimulationError if something is wrong with the template */ export const initBaseTemplates = () => { - for (const template of baseTemplates) { - if (template.metadata && template.metadata.name) { - templateStore.set(template.metadata.name, template) - } else { - throw new SimulationError( - `Template ${JSON.stringify(template)} cannot be stored.` - ) - } + for (const template of baseTemplates) { + if (template.metadata && template.metadata.name) { + templateStore.set(template.metadata.name, template) + } else { + throw new SimulationError( + `Template ${JSON.stringify(template)} cannot be stored.` + ) } + } } diff --git a/src/modules/saving/templates/comment.ts b/src/modules/saving/templates/comment.ts index 48decbd..4113bb5 100644 --- a/src/modules/saving/templates/comment.ts +++ b/src/modules/saving/templates/comment.ts @@ -4,44 +4,44 @@ import { PartialTemplate } from '../types/PartialTemplate' * The template of the comment gate */ const commentTemplate: PartialTemplate = { - metadata: { - name: 'comment' + metadata: { + name: 'comment' + }, + pins: { + inputs: { + count: 0 }, - pins: { - inputs: { - 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' + 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' + } } export default commentTemplate diff --git a/src/modules/saving/templates/constant.ts b/src/modules/saving/templates/constant.ts new file mode 100644 index 0000000..94e6f55 --- /dev/null +++ b/src/modules/saving/templates/constant.ts @@ -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 diff --git a/src/modules/saving/templates/light.ts b/src/modules/saving/templates/light.ts index ab273f9..8b3a89a 100644 --- a/src/modules/saving/templates/light.ts +++ b/src/modules/saving/templates/light.ts @@ -5,40 +5,40 @@ import { categories } from '../data/categories' * The template of the light gate */ const lightTemplate: PartialTemplate = { - metadata: { - name: 'light bulb' + metadata: { + name: 'light bulb' + }, + shape: { + radius: 50 + }, + material: { + fill: '#1C1C1C', + stroke: { + normal: '#3C3C3C' }, - shape: { - radius: 50 - }, - material: { - fill: '#1C1C1C', - stroke: { - normal: '#3C3C3C' - }, - colors: { - active: '#C6FF00' - } - }, - code: { - activation: ` + colors: { + active: '#C6FF00' + } + }, + code: { + activation: ` const { main, active } = context.colors const bits = context.get(0) context.color(parseInt(context.get(0),2) ? active : main) ` - }, - integration: { - output: true - }, - info: ['https://en.wikipedia.org/wiki/Incandescent_light_bulb'], - pins: { - outputs: { - count: 0 - } - }, - category: categories.io + }, + integration: { + output: true + }, + info: ['https://en.wikipedia.org/wiki/Incandescent_light_bulb'], + pins: { + outputs: { + count: 0 + } + }, + category: categories.io } export default lightTemplate diff --git a/src/modules/simulation/classes/Gate.ts b/src/modules/simulation/classes/Gate.ts index 50e2850..332bcac 100644 --- a/src/modules/simulation/classes/Gate.ts +++ b/src/modules/simulation/classes/Gate.ts @@ -1,20 +1,15 @@ import { Transform } from '../../../common/math/classes/Transform' import { Pin } from './Pin' import { - GateTemplate, - PinCount, - isGroup, - Property + 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 } from 'rxjs' import { SimulationError } from '../../errors/classes/SimulationError' import { getGateTimePipes } from '../helpers/getGateTimePipes' import { ImageStore } from '../../simulationRenderer/stores/imageStore' @@ -25,7 +20,6 @@ import { saveStore } from '../../saving/stores/saveStore' 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' @@ -33,17 +27,17 @@ import { ValueOf } from '../../../common/lang/record/types/ValueOf' * The interface for the pins of a gate */ export interface GatePins { - inputs: Pin[] - outputs: Pin[] + inputs: Pin[] + outputs: Pin[] } /** * Wrapper around a pin so it can be rendered at the right place */ export interface PinWrapper { - total: number - index: number - value: Pin + total: number + index: number + value: Pin } /** @@ -55,502 +49,501 @@ export type GateFunction = null | ((ctx: Context) => void) * All functions a gate must remember */ export interface GateFunctions { - activation: GateFunction - onClick: GateFunction + activation: GateFunction + onClick: GateFunction } export type GateProps = { - [K in keyof PropsSave]: BehaviorSubject | GateProps + [K in keyof PropsSave]: BehaviorSubject | GateProps } & { - external: BehaviorSubject - label: BehaviorSubject + external: BehaviorSubject + label: BehaviorSubject } export class Gate { - /** - * The transform of the gate - */ - public transform = new Transform() + /** + * The transform of the gate + */ + public transform = new Transform() - /** - * The object holding all the pins the gate curently has - */ - public _pins: GatePins = { - inputs: [], - outputs: [] + /** + * The object holding all the pins the gate curently has + */ + public _pins: GatePins = { + inputs: [], + outputs: [] + } + + /** + * The id of the gate + */ + public id: number + + /** + * The template the gate needs to follow + */ + public template: GateTemplate + + /** + * All the functions created from the template strings + */ + private functions: GateFunctions = { + activation: null, + onClick: null + } + + /** + * Used only if the gate is async + */ + private executionQueue = new ExecutionQueue() + + /** + * All rxjs subscriptions the gate created + * (if they are not manually cleared it can lead to memory leaks) + */ + private subscriptions: Subscription[] = [] + + /** + * The state the activation functions have aces to + */ + private memory: Record = {} + + /** + * The inner simulaton used by integrated circuits + */ + private ghostSimulation: Simulation + + /** + * The wires connecting the outer simulation to the inner one + */ + private ghostWires: Wire[] = [] + + /** + * Boolean keeping track if the component is an ic + */ + private isIntegrated = false + + /** + * Used to know if the component runs in the global scope (rendered) + * or insie an integrated circuit + */ + public env: SimulationEnv = 'global' + + /** + * Holds all the gate-related text + */ + public text = { + inner: new BehaviorSubject(null) + } + + /** + * The props used by the activation function (the same as memory but presists) + */ + public props: GateProps = {} as GateProps + + /** + * The main logic gate class + * + * @param template The template the gate needs to follow + * @param id The id of the gate + */ + public constructor( + template: DeepPartial = {}, + id?: number, + props: PropsSave = {} + ) { + this.template = completeTemplate(template) + this.transform.scale = this.template.shape.scale + + if (this.template.material.type === 'color') { + this.template.material.colors.main = this.template.material.fill } - /** - * The id of the gate - */ - public id: number + this.functions.activation = toFunction( + this.template.code.activation, + 'context' + ) - /** - * The template the gate needs to follow - */ - public template: GateTemplate + this.functions.onClick = toFunction(this.template.code.onClick, 'context') - /** - * All the functions created from the template strings - */ - private functions: GateFunctions = { - activation: null, - onClick: null + this._pins.inputs = Gate.generatePins(this.template.pins.inputs, 1, this) + this._pins.outputs = Gate.generatePins(this.template.pins.outputs, 2, this) + + if (this.template.material.type === 'image') { + ImageStore.set(this.template.material.fill) } - /** - * Used only if the gate is async - */ - private executionQueue = new ExecutionQueue() + this.id = id !== undefined ? id : idStore.generate() - /** - * All rxjs subscriptions the gate created - * (if they are not manually cleared it can lead to memory leaks) - */ - private subscriptions: Subscription[] = [] + for (const pin of this._pins.inputs) { + const pipes = getGateTimePipes(this.template) - /** - * The state the activation functions have aces to - */ - private memory: Record = {} + const subscription = pin.state.pipe(...pipes).subscribe(() => { + if (this.template.code.async) { + this.executionQueue.push(async () => { + return this.update() + }) + } else { + this.update() + } + }) - /** - * The inner simulaton used by integrated circuits - */ - private ghostSimulation: Simulation - - /** - * The wires connecting the outer simulation to the inner one - */ - private ghostWires: Wire[] = [] - - /** - * Boolean keeping track if the component is an ic - */ - private isIntegrated = false - - /** - * Used to know if the component runs in the global scope (rendered) - * or insie an integrated circuit - */ - public env: SimulationEnv = 'global' - - /** - * Holds all the gate-related text - */ - public text = { - inner: new BehaviorSubject('text goes here') + this.subscriptions.push(subscription) } - /** - * The props used by the activation function (the same as memory but presists) - */ - public props: GateProps = {} as GateProps + this.init() - /** - * The main logic gate class - * - * @param template The template the gate needs to follow - * @param id The id of the gate - */ - public constructor( - template: DeepPartial = {}, - id?: number, - props: PropsSave = {} - ) { - this.template = completeTemplate(template) - this.transform.scale = this.template.shape.scale + if (this.template.tags.includes('integrated')) { + this.isIntegrated = true + } + if (this.isIntegrated) { + const state = saveStore.get(this.template.metadata.name) + + if (!state) { + throw new SimulationError( + `Cannot run ic ${this.template.metadata.name} - save not found` + ) + } + + this.ghostSimulation = fromSimulationState(state.simulation, 'gate') + cleanSimulation(this.ghostSimulation) + + const sortByPosition = (x: Gate, y: Gate) => + x.transform.position[1] - y.transform.position[1] + + const gates = Array.from(this.ghostSimulation.gates) + + const inputs = gates + .filter((gate) => gate.template.integration.input) + .sort(sortByPosition) + .map((gate) => gate.wrapPins(gate._pins.outputs)) + .flat() + + const outputs = gates + .filter((gate) => gate.template.integration.output) + .sort(sortByPosition) + .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}` + ) + } + + 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}` + ) + } + + const wrappedInputs = this.wrapPins(this._pins.inputs) + const wrappedOutputs = this.wrapPins(this._pins.outputs) + + for (let index = 0; index < inputs.length; index++) { + this.ghostWires.push( + new Wire(wrappedInputs[index], inputs[index], true) + ) + } + + for (let index = 0; index < outputs.length; index++) { + this.ghostWires.push( + new Wire(outputs[index], wrappedOutputs[index], true) + ) + } + + this.ghostSimulation.wires.push(...this.ghostWires) + } + + this.assignProps(props) + } + + private updateNestedProp( + path: string[], + value: ValueOf, + 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 merge them with the base ones + */ + private assignProps( + source: PropsSave, + props: Property[] = this.template.properties.data, + target: GateProps = this.props, + path: string[] = [] + ) { + // We don't want to update until every prop has been created + let lockUpdates = true + + if (this.template.properties.enabled) { + for (const prop of props) { + if (isGroup(prop)) { + const { groupName } = prop + target[groupName] = {} as GateProps + this.assignProps( + typeof source[groupName] === 'object' + ? (source[groupName] as PropsSave) + : {}, + prop.props, + target[groupName] as GateProps, + [...path, groupName] + ) + + 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 (!lockUpdates && needsUpdate && path.length === 0) { + return this.update() + } + + if (path.length === 0) { + return + } + + this.updateNestedProp([...path, name], value) + }) + ) + } + } + + lockUpdates = false + this.update() + } + + /** + * Runs the init function from the template + */ + private init() { + toFunction<[InitialisationContext]>( + this.template.code.initialisation, + 'context' + )({ + memory: this.memory + }) + } + + /** + * Runs the onClick function from the template + */ + public onClick() { + if (this.functions.onClick) { + this.functions.onClick(this.getContext()) + } + } + + /** + * Used to get the props as an object + */ + public getProps(target = this.props) { + const props: PropsSave = {} + + for (const [key, value] of Object.entries(target)) { + if (value instanceof BehaviorSubject) { + props[key] = value.value + } else { + props[key] = this.getProps(value) + } + } + + return props + } + + /** + * Clears subscriptions to prevent memory leaks + */ + public dispose() { + for (const pin of this.pins) { + pin.value.dispose() + } + + for (const subscription of this.subscriptions) { + subscription.unsubscribe() + } + + if (this.isIntegrated) { + this.ghostSimulation.dispose() + } + } + + /** + * Runs the activation function from the template + */ + public update() { + if (!this.template.tags.includes('integrated')) { + const context = this.getContext() + + if (!this.functions.activation) + throw new SimulationError('Activation function is missing') + + return this.functions.activation(context) + } + } + + /** + * Generates the activation context + */ + public getContext(): Context { + const maxLength = Math.max( + ...this._pins.inputs.map((pin) => pin.state.value.length) + ) + + const toLength = ( + original: string | number, + length: number = maxLength, + paddingChar = '0' + ) => { + const value = original.toString(2) + + if (value.length === length) { + return value + } else if (value.length > length) { + const difference = value.length - length + + return value.slice(difference) + } else { + return `${paddingChar.repeat(length - value.length)}${value}` + } + } + + 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) => { + return this._pins.inputs[index].state.value + }, + set: (index: number, state) => { + return this._pins.outputs[index].state.next(state) + }, + getOutput: (index: number) => { + return this._pins.outputs[index].state.value + }, + getBinary: (index: number) => { + return parseInt(this._pins.inputs[index].state.value, 2) + }, + setBinary: (index: number, value: number, bits: number = maxLength) => { + return this._pins.outputs[index].state.next( + toLength(value.toString(2), bits) + ) + }, + getOutputBinary: (index: number) => { + return parseInt(this._pins.outputs[index].state.value, 2) + }, + invertBinary: (value: number) => { + return value ^ ((1 << maxLength) - 1) + }, + color: (color: string) => { if (this.template.material.type === 'color') { - this.template.material.colors.main = this.template.material.fill + this.template.material.fill = color } - - this.functions.activation = toFunction( - this.template.code.activation, - 'context' - ) - - this.functions.onClick = toFunction( - this.template.code.onClick, - 'context' - ) - - this._pins.inputs = Gate.generatePins( - this.template.pins.inputs, - 1, - this - ) - this._pins.outputs = Gate.generatePins( - this.template.pins.outputs, - 2, - this - ) - - if (this.template.material.type === 'image') { - ImageStore.set(this.template.material.fill) + }, + 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 } - - this.id = id !== undefined ? id : idStore.generate() - - for (const pin of this._pins.inputs) { - const pipes = getGateTimePipes(this.template) - - const subscription = pin.state.pipe(...pipes).subscribe(() => { - if (this.template.code.async) { - this.executionQueue.push(async () => { - return await this.update() - }) - } else { - this.update() - } - }) - - this.subscriptions.push(subscription) + }, + setProperty: (name: string, value: string | number | boolean) => { + const subject = this.props[name] + if (subject instanceof BehaviorSubject) { + subject.next(value) } + }, + innerText: (value: string) => { + this.text.inner.next(value) + }, + update: () => { + this.update() + }, + toLength, + maxLength, + enviroment: this.env, + memory: this.memory, + colors: { + ...this.template.material.colors + } + } + } - this.init() + /** + * Generates pin wrappers from an array of pins + * + * @param pins The pins to wwap + */ + private wrapPins(pins: Pin[]) { + const result: PinWrapper[] = [] + const length = pins.length - if (this.template.tags.includes('integrated')) { - this.isIntegrated = true - } - - if (this.isIntegrated) { - const state = saveStore.get(this.template.metadata.name) - - if (!state) { - throw new SimulationError( - `Cannot run ic ${this.template.metadata.name} - save not found` - ) - } - - this.ghostSimulation = fromSimulationState(state.simulation, 'gate') - cleanSimulation(this.ghostSimulation) - - const sortByPosition = (x: Gate, y: Gate) => - x.transform.position[1] - y.transform.position[1] - - const gates = Array.from(this.ghostSimulation.gates) - - const inputs = gates - .filter((gate) => gate.template.integration.input) - .sort(sortByPosition) - .map((gate) => gate.wrapPins(gate._pins.outputs)) - .flat() - - const outputs = gates - .filter((gate) => gate.template.integration.output) - .sort(sortByPosition) - .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}` - ) - } - - 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}` - ) - } - - const wrappedInputs = this.wrapPins(this._pins.inputs) - const wrappedOutputs = this.wrapPins(this._pins.outputs) - - for (let index = 0; index < inputs.length; index++) { - this.ghostWires.push( - new Wire(wrappedInputs[index], inputs[index], true) - ) - } - - for (let index = 0; index < outputs.length; index++) { - this.ghostWires.push( - new Wire(outputs[index], wrappedOutputs[index], true) - ) - } - - this.ghostSimulation.wires.push(...this.ghostWires) - } - - this.assignProps(props) + for (let index = 0; index < length; index++) { + result.push({ + index, + total: length, + value: pins[index] + }) } - private updateNestedProp( - path: string[], - value: ValueOf, - gate: Gate = this - ) { - if (!path.length) { - return - } + return result + } - if (path.length === 1) { - const subject = gate.props[path[0]] + /** + * Returns all pins (input + output) + */ + public get pins() { + const result = [ + ...this.wrapPins(this._pins.inputs), + ...this.wrapPins(this._pins.outputs) + ] - if (subject instanceof BehaviorSubject) { - subject.next(value) - } + return result + } - 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( - source: PropsSave, - props: Property[] = this.template.properties.data, - target: GateProps = this.props, - path: string[] = [] - ) { - let shouldUpdate = false - - if (this.template.properties.enabled) { - 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 (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 - } - } - } - - return shouldUpdate - } - - /** - * Runs the init function from the template - */ - private init() { - toFunction<[InitialisationContext]>( - this.template.code.initialisation, - 'context' - )({ - memory: this.memory - }) - } - - /** - * Runs the onClick function from the template - */ - public onClick() { - if (this.functions.onClick) { - this.functions.onClick(this.getContext()) - } - } - - /** - * Used to get the props as an object - */ - public getProps(target = this.props) { - const props: PropsSave = {} - - for (const [key, value] of Object.entries(target)) { - if (value instanceof BehaviorSubject) { - props[key] = value.value - } else { - props[key] = this.getProps(value) - } - } - - return props - } - - /** - * Clears subscriptions to prevent memory leaks - */ - public dispose() { - for (const pin of this.pins) { - pin.value.dispose() - } - - for (const subscription of this.subscriptions) { - subscription.unsubscribe() - } - - if (this.isIntegrated) { - this.ghostSimulation.dispose() - } - } - - /** - * Runs the activation function from the template - */ - public update() { - if (!this.template.tags.includes('integrated')) { - const context = this.getContext() - - if (!this.functions.activation) - throw new SimulationError('Activation function is missing') - - return this.functions.activation(context) - } - } - - /** - * Generates the activation context - */ - public getContext(): Context { - const maxLength = Math.max( - ...this._pins.inputs.map((pin) => pin.state.value.length) - ) - - const toLength = ( - original: string | number, - length: number = maxLength - ) => { - const value = original.toString(2) - - if (value.length === length) { - return value - } else if (value.length > length) { - const difference = value.length - length - - return value.substr(difference) - } else { - return `${'0'.repeat(length - value.length)}${value}` - } - } - - return { - get: (index: number) => { - return this._pins.inputs[index].state.value - }, - set: (index: number, state) => { - return this._pins.outputs[index].state.next(state) - }, - getBinary: (index: number) => { - return parseInt(this._pins.inputs[index].state.value, 2) - }, - setBinary: ( - index: number, - value: number, - bits: number = maxLength - ) => { - return this._pins.outputs[index].state.next( - toLength(value.toString(2), bits) - ) - }, - invertBinary: (value: number) => { - return value ^ ((1 << maxLength) - 1) - }, - color: (color: string) => { - if (this.template.material.type === 'color') { - this.template.material.fill = color - } - }, - getProperty: (name: string) => { - return this.props[name].value - }, - setProperty: (name: string, value: string | number | boolean) => { - const subject = this.props[name] - if (subject instanceof BehaviorSubject) { - subject.next(value) - } - }, - innerText: (value: string) => { - this.text.inner.next(value) - }, - update: () => { - this.update() - }, - toLength, - maxLength, - enviroment: this.env, - memory: this.memory, - colors: { - ...this.template.material.colors - } - } - } - - /** - * Generates pin wrappers from an array of pins - * - * @param pins The pins to wwap - */ - private wrapPins(pins: Pin[]) { - const result: PinWrapper[] = [] - const length = pins.length - - for (let index = 0; index < length; index++) { - result.push({ - index, - total: length, - value: pins[index] - }) - } - - return result - } - - /** - * Returns all pins (input + output) - */ - public get pins() { - const result = [ - ...this.wrapPins(this._pins.inputs), - ...this.wrapPins(this._pins.outputs) - ] - - return result - } - - /** - * Generates empty pins for any gate - */ - private static generatePins(options: PinCount, type: number, gate: Gate) { - return [...Array(options.count)] - .fill(true) - .map((v, index) => new Pin(type, gate)) - } + /** + * Generates empty pins for any gate + */ + private static generatePins(options: PinCount, type: number, gate: Gate) { + return [...Array(options.count)] + .fill(true) + .map((_v, _index) => new Pin(type, gate)) + } } diff --git a/src/modules/simulation/constants.ts b/src/modules/simulation/constants.ts index 726e2f1..5d2f1df 100644 --- a/src/modules/simulation/constants.ts +++ b/src/modules/simulation/constants.ts @@ -3,80 +3,80 @@ import { categories } from '../saving/data/categories' import { getRendererSafely } from '../logic-gates/helpers/getRendererSafely' export const DefaultGateTemplate: GateTemplate = { - metadata: { - name: 'Default template' + metadata: { + name: 'Default template' + }, + material: { + type: 'color', + fill: 'blue', + stroke: { + active: '#76FF02', + normal: '#3FC4FF' }, - material: { - type: 'color', - fill: 'blue', - stroke: { - active: '#76FF02', - normal: '#3FC4FF' - }, - colors: {} + colors: {} + }, + pins: { + inputs: { + count: 1, + variable: false }, - pins: { - inputs: { - count: 1, - variable: false - }, - outputs: { - count: 1, - variable: false - } + outputs: { + count: 1, + variable: false + } + }, + shape: { + radius: 10, + rounded: true, + scale: [100, 100] + }, + code: { + async: false, + activation: '', + onClick: '', + initialisation: '' + }, + simulation: { + debounce: { + enabled: true, + time: 1000 / 60 }, - shape: { - radius: 10, - rounded: true, - scale: [100, 100] - }, - code: { - async: false, - activation: '', - onClick: '', - initialisation: '' - }, - simulation: { - debounce: { - enabled: true, - time: 1000 / 60 - }, - throttle: { - enabled: false - } - }, - integration: { - allowed: true, - input: false, - output: false - }, - tags: ['base'], - properties: { - enabled: false, - data: [ - { - type: 'boolean', - base: false, - name: 'external' - }, - { - type: 'string', - base: 'my-logic-gate', - name: 'label' - } - ] - }, - innerText: { - enabled: false, - color: 'white' - }, - category: categories.basic, - info: [] + throttle: { + enabled: false + } + }, + integration: { + allowed: true, + input: false, + output: false + }, + tags: ['base'], + properties: { + enabled: false, + data: [ + { + type: 'boolean', + 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 */ export const reservedPropNames = DefaultGateTemplate.properties.data.map( - ({ name }: RawProp) => name + ({ name }: RawProp) => name ) diff --git a/src/modules/simulation/types/GateTemplate.ts b/src/modules/simulation/types/GateTemplate.ts index 53b5718..7888c0b 100644 --- a/src/modules/simulation/types/GateTemplate.ts +++ b/src/modules/simulation/types/GateTemplate.ts @@ -1,96 +1,97 @@ import { vector2 } from '../../../common/math/types/vector2' export interface PinCount { - variable: boolean - count: number + variable: boolean + count: number } export type PropGroup< - T extends boolean | number | string = boolean | number | string + T extends boolean | number | string = boolean | number | string > = { - groupName: string - props: Property[] + groupName: string + props: Property[] } export const isGroup = (prop: Property): prop is PropGroup => - (prop as PropGroup).groupName !== undefined + (prop as PropGroup).groupName !== undefined export type RawProp< - T extends boolean | number | string = boolean | number | string + T extends boolean | number | string = boolean | number | string > = { - type: 'number' | 'string' | 'text' | 'boolean' - base: T - name: string - needsUpdate?: boolean + type: 'number' | 'string' | 'text' | 'boolean' + base: T + name: string + description?: string + needsUpdate?: boolean } export type Property< - T extends boolean | number | string = boolean | number | string + T extends boolean | number | string = boolean | number | string > = PropGroup | RawProp export interface Material { - type: 'color' | 'image' - fill: string - stroke: { - active: string - normal: string - } - colors: Record + type: 'color' | 'image' + fill: string + stroke: { + active: string + normal: string + } + colors: Record } export interface Shape { - rounded: boolean - radius: number - scale: vector2 + rounded: boolean + radius: number + scale: vector2 } export type Enabled = - | { - enabled: false - } - | ({ - enabled: true - } & T) + | { + enabled: false + } + | ({ + enabled: true + } & T) export type TimePipe = Enabled<{ - time: number + time: number }> export type GateTag = 'base' | 'imported' | 'integrated' export interface GateTemplate { - material: Material - shape: Shape - pins: { - inputs: PinCount - outputs: PinCount - } - metadata: { - name: string - } - code: { - async: boolean - initialisation: string - activation: string - onClick: string - } - simulation: { - throttle: TimePipe - debounce: TimePipe - } - integration: { - allowed: boolean - input: boolean - output: boolean - } - tags: GateTag[] - properties: { - enabled: boolean - data: Property[] - } - innerText: { - color: string - enabled: boolean - } - category: number // for better sorting - info: string[] + material: Material + shape: Shape + pins: { + inputs: PinCount + outputs: PinCount + } + metadata: { + name: string + } + code: { + async: boolean + initialisation: string + activation: string + onClick: string + } + simulation: { + throttle: TimePipe + debounce: TimePipe + } + integration: { + allowed: boolean + input: boolean + output: boolean + } + tags: GateTag[] + properties: { + enabled: boolean + data: Property[] + } + innerText: { + color: string + enabled: boolean + } + category: number // for better sorting + info: string[] } diff --git a/src/modules/simulationRenderer/data/textSettings.ts b/src/modules/simulationRenderer/data/textSettings.ts index 684182a..45d4084 100644 --- a/src/modules/simulationRenderer/data/textSettings.ts +++ b/src/modules/simulationRenderer/data/textSettings.ts @@ -1,5 +1,5 @@ export const textSettings = { - font: '30px Roboto', - offset: 35, - fill: `rgba(256,256,256,0.75)` + font: '30px monospace', + offset: 35, + fill: `rgba(256,256,256,0.75)` } diff --git a/src/modules/simulationRenderer/helpers/pinFill.ts b/src/modules/simulationRenderer/helpers/pinFill.ts index 0b1f456..bd1a32c 100644 --- a/src/modules/simulationRenderer/helpers/pinFill.ts +++ b/src/modules/simulationRenderer/helpers/pinFill.ts @@ -16,7 +16,7 @@ export const pinFill = (renderer: SimulationRenderer, pin: Pin) => { .map((key) => chunked .flat() - .filter((v, index) => index % 3 === key) + .filter((_v, index) => index % 3 === key) .reduce((acc, curr) => acc + curr, 0) ) .map((value) => Math.floor(value / digits.length)) diff --git a/src/modules/simulationRenderer/helpers/renderGate.ts b/src/modules/simulationRenderer/helpers/renderGate.ts index aa91ead..35f2f8b 100644 --- a/src/modules/simulationRenderer/helpers/renderGate.ts +++ b/src/modules/simulationRenderer/helpers/renderGate.ts @@ -10,64 +10,85 @@ import { idIsSelected } from './idIsSelected' import { textSettings } from '../data/textSettings' export const renderGate = ( - ctx: CanvasRenderingContext2D, - renderer: SimulationRenderer, - gate: Gate + ctx: CanvasRenderingContext2D, + renderer: SimulationRenderer, + gate: Gate ) => { - const { active, normal } = gate.template.material.stroke + const { active, normal } = gate.template.material.stroke - const selected = - (renderer.mouseState >> 2 && - !!gatesInSelection(renderer.selectedArea, [gate]).length) || - idIsSelected(renderer, gate.id) + const selected = + (renderer.mouseState >> 2 && + !!gatesInSelection(renderer.selectedArea, [gate]).length) || + idIsSelected(renderer, gate.id) - renderPins(ctx, renderer, gate, selected) + renderPins(ctx, renderer, gate, selected) - if (selected) { - ctx.strokeStyle = active - } else { - ctx.strokeStyle = normal + if (selected) { + ctx.strokeStyle = active + } else { + 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() - - 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() + ctx.restore() }