integrated circuits

This commit is contained in:
Matei Adriel 2019-07-23 14:56:11 +03:00
parent e88244bc9d
commit 639f4b5aa0
30 changed files with 497 additions and 126 deletions

22
package-lock.json generated
View file

@ -4268,6 +4268,28 @@
"integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
"dev": true
},
"file-loader": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.1.0.tgz",
"integrity": "sha512-ajDk1nlByoalZAGR4b0H6oD+EGlWnyW1qbSxzaUc7RFiqmn+RbXQQRbTc72jsiUIlVusJ4Et58ltds8ZwTfnAw==",
"dev": true,
"requires": {
"loader-utils": "^1.2.3",
"schema-utils": "^2.0.0"
},
"dependencies": {
"schema-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.0.1.tgz",
"integrity": "sha512-HJFKJ4JixDpRur06QHwi8uu2kZbng318ahWEKgBjc0ZklcE4FDvmm2wghb448q0IRaABxIESt8vqPFvwgMB80A==",
"dev": true,
"requires": {
"ajv": "^6.1.0",
"ajv-keywords": "^3.1.0"
}
}
}
},
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",

View file

@ -6,7 +6,7 @@
"dev": "webpack-dev-server --open --mode development",
"build": "cross-env NODE_ENV=production webpack",
"deploy": "ts-node deploy",
"show": "gource -f --start-date \"2019-07-01 12:00\" --key --hide dirnames,filenames,bloom -s 3"
"show": "gource -f --start-date \"2019-07-01 12:00\" --key --hide dirnames,filenames,bloom -s 1"
},
"devDependencies": {
"@babel/core": "^7.5.5",
@ -24,6 +24,7 @@
"babel-plugin-transform-runtime": "^6.23.0",
"babel-regenerator-runtime": "^6.5.0",
"css-loader": "^3.0.0",
"file-loader": "^4.1.0",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",

View file

@ -1,8 +1,15 @@
import { SimulationRenderer } from '../../../modules/simulationRenderer/classes/SimulationRenderer'
import { Screen } from '../../../modules/core/classes/Screen'
/**
* A screen instance used for the canvas clearing
*/
const screen = new Screen()
/**
* Clears the used portion of the canvas
*
* @param ctx the context to clear
*/
export const clearCanvas = (ctx: CanvasRenderingContext2D) => {
ctx.clearRect(0, 0, screen.x, screen.y)
}

View file

@ -1,5 +1,12 @@
import { vector2 } from '../../math/types/vector2'
/**
*
* @param ctx The context to draw on
* @param points an array of points to draw
* @param fill if true the polygon will be filled
* @param stroke if true the polygno will be stroked
*/
export const drawPolygon = (
ctx: CanvasRenderingContext2D,
points: vector2[],

View file

@ -1,33 +1,34 @@
import { Transform } from '../../math/classes/Transform'
import { Material, Shape } from '../../../modules/simulation/types/GateTemplate'
import { Shape } from '../../../modules/simulation/types/GateTemplate'
import { roundRect } from './drawRoundedSquare'
import { useTransform } from './useTransform'
/**
* Draws a square from a transform
*
* @param ctx The context to draw on
* @param transform -The transform to use
* @param shape - The shae object to use
*/
export const drawRotatedSquare = (
ctx: CanvasRenderingContext2D,
{ position, scale, rotation }: Transform,
transform: Transform,
shape: Shape
) => {
ctx.save()
ctx.translate(...position)
ctx.translate(scale[0] / 2, scale[1] / 2)
const relative = useTransform(ctx, transform)
ctx.rotate(rotation)
roundRect(
ctx,
relative.x,
relative.y,
relative.width,
relative.height,
shape.radius ? shape.radius : 0
)
if (shape.rounded) {
roundRect(
ctx,
-scale[0] / 2,
-scale[1] / 2,
scale[0],
scale[1],
shape.radius
)
ctx.fill()
} else {
ctx.fillRect(scale[0] / -2, scale[1] / -2, ...scale)
}
ctx.fill()
ctx.restore()
}

View file

@ -0,0 +1,35 @@
/**
*
* @param ctx The context to draw on
* @param x the x of the rect
* @param y the y of the rect
* @param width the width of the rect
* @param height the height of the rect
* @param radius the radius of the corners
*/
export function roundImage(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
x: number = 0,
y: number = 0,
width: number = 100,
height: number = 100,
radius: number = 5
) {
ctx.beginPath()
ctx.moveTo(x + radius, y)
ctx.lineTo(x + width - radius, y)
ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
ctx.lineTo(x + width, y + height - radius)
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
ctx.lineTo(x + radius, y + height)
ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
ctx.lineTo(x, y + radius)
ctx.quadraticCurveTo(x, y, x + radius, y)
ctx.closePath()
ctx.save()
ctx.clip()
ctx.drawImage(image, x, y, width, height)
ctx.restore()
}

View file

@ -3,10 +3,10 @@
*/
export function roundRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
x: number = 0,
y: number = 0,
width: number = 100,
height: number = 100,
radius: number = 5
) {
ctx.beginPath()

View file

@ -1,6 +1,11 @@
import { Transform } from '../../math/classes/Transform'
import { multiply } from '../../../modules/vector2/helpers/basic'
/**
*
* @param ctx The context to use
* @param transform The transform to move relative to
*/
export const useTransform = (
ctx: CanvasRenderingContext2D,
{ position, rotation, scale }: Transform

View file

@ -1,6 +1,15 @@
import { Simulation, SimulationEnv } from '../../simulation/classes/Simulation'
import { SimulationError } from '../../errors/classes/SimulationError'
import { fromSimulationState } from '../../saving/helpers/fromState'
export interface Context {
memory: Record<string, unknown>
get: (index: number) => boolean
set: (index: number, state: boolean) => void
color: (color: string) => void
enviroment: SimulationEnv
}
export interface InitialisationContext {
memory: Record<string, unknown>
}

View file

@ -0,0 +1,40 @@
import { SimulationState } from '../../saving/types/SimulationSave'
import { SimulationError } from '../../errors/classes/SimulationError'
import { GateTemplate } from '../../simulation/types/GateTemplate'
import {
simulationInputCount,
simulationOutputCount
} from './simulationIoCount'
import { InitialisationContext } from '../../activation/types/Context'
import { templateStore } from '../../saving/stores/templateStore'
/**
* Compiles a simulation into a logicGate
*
* @param simulaton The simulation to compile
*/
export const compileIc = ({ mode, name, gates }: SimulationState) => {
if (mode === 'project') {
throw new SimulationError('Cannot compile project')
}
const inputCount = simulationInputCount(gates)
const outputCount = simulationOutputCount(gates)
const result: DeepPartial<GateTemplate> = {
metadata: {
name
},
tags: ['integrated'],
pins: {
inputs: {
count: inputCount
},
outputs: {
count: outputCount
}
}
}
templateStore.set(name, result)
}

View file

@ -0,0 +1,34 @@
import { GateState } from '../../saving/types/SimulationSave'
import { GateTemplate } from '../../simulation/types/GateTemplate'
import { templateStore } from '../../saving/stores/templateStore'
/**
* Any type of gate wich has a template
*/
export type hasTemplate = { template: string }
/**
* Counts the number of ic inputs inside an array of gate states
*
* @param gates The state to count the inputs in
*/
export const simulationInputCount = (gates: hasTemplate[]) => {
return gates.filter(gate => {
const template = templateStore.get(gate.template)
return template && template.integration && template.integration.input
}).length
}
/**
* Counts the number of ic outputs inside an array of gate states
*
* @param gates The state to count the outputs for
*/
export const simulationOutputCount = (gates: hasTemplate[]) => {
return gates.filter(gate => {
const template = templateStore.get(gate.template)
return template && template.integration && template.integration.output
}).length
}

View file

@ -10,6 +10,8 @@ import { rendererSubject } from '../../core/subjects/rendererSubject'
import { SimulationError } from '../../errors/classes/SimulationError'
import { templateStore } from '../../saving/stores/templateStore'
import { randomItem } from '../../internalisation/helpers/randomItem'
import { completeTemplate } from '../helpers/completeTemplate'
import { gateIcons } from '../constants'
/**
* Subject containing the open state of the modal
@ -43,12 +45,11 @@ const LogicGateModal = () => {
throw new SimulationError(`Renderer not found`)
}
const template =
gate.source === 'base' ? templateStore.get(gate.name) : ''
const template = completeTemplate(templateStore.get(gate) || {})
if (gate.source === 'base' && !template) {
if (!template) {
throw new SimulationError(
`Template ${gate.name} cannot be found`
`Template ${gate} cannot be found`
)
}
@ -57,29 +58,28 @@ const LogicGateModal = () => {
key={index}
className="logic-gate-item"
onClick={e => {
addGate(renderer.simulation, gate.name)
addGate(renderer.simulation, gate)
}}
>
<Icon className="lgi-icon logic-gate-item-type">
{gate.source === 'base' ? 'sd_storage' : 'memory'}
{gateIcons[template.tags[0]]}
</Icon>
<Typography className="logic-gate-item-name">
{gate.name}
{gate}
</Typography>
{template && template.info && template.info.length && (
{template.info.length && (
<a
target="_blank"
className="logic-gate-item-info"
href={randomItem(template.info)}
onClick={e => {
e.stopPropagation()
e.preventDefault()
}}
>
<Icon className="lgi-icon">info</Icon>
</a>
)}
{gate.source === 'ic' && (
<Icon className="lgi-icon logic-gate-item-delete">
delete
</Icon>
)}
</div>
)
})}

View file

@ -0,0 +1,7 @@
import { GateTag } from '../simulation/types/GateTemplate'
export const gateIcons: Record<GateTag, string> = {
base: 'house',
imported: 'share',
integrated: 'memory'
}

View file

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

View file

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

View file

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

View file

@ -8,7 +8,8 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
name: 'and'
},
material: {
value: 'green'
type: 'image',
value: require('../../assets/and_gate')
},
code: {
activation: `context.set(0, context.get(0) && context.get(1))`
@ -42,7 +43,8 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
name: 'xor'
},
material: {
value: 'white'
type: 'image',
value: require('../../assets/xor_gate')
},
code: {
activation: `
@ -94,6 +96,9 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
count: 0
}
},
integration: {
input: true
},
info: ['https://en.wikipedia.org/wiki/Push-button']
},
{
@ -111,6 +116,9 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
context.color(context.get(0) ? 'yellow' : 'white')
`
},
integration: {
output: true
},
info: ['https://en.wikipedia.org/wiki/Incandescent_light_bulb'],
pins: {
outputs: {

View file

@ -6,7 +6,7 @@ import {
} from '../types/SimulationSave'
import { Transform } from '../../../common/math/classes/Transform'
import { Camera } from '../../simulationRenderer/classes/Camera'
import { Simulation } from '../../simulation/classes/Simulation'
import { Simulation, SimulationEnv } from '../../simulation/classes/Simulation'
import { Wire } from '../../simulation/classes/Wire'
import { templateStore } from '../stores/templateStore'
@ -26,8 +26,11 @@ export const fromCameraState = (state: CameraState): Camera => {
return camera
}
export const fromSimulationState = (state: SimulationState): Simulation => {
const simulation = new Simulation(state.mode, state.name)
export const fromSimulationState = (
state: SimulationState,
env: SimulationEnv = 'global'
): Simulation => {
const simulation = new Simulation(state.mode, state.name, env)
for (const gateState of state.gates) {
const gate = new Gate(
@ -60,7 +63,7 @@ export const fromSimulationState = (state: SimulationState): Simulation => {
value: endGateNode.data._pins.inputs[wireState.to.index]
}
const wire = new Wire(start, end, wireState.id)
const wire = new Wire(start, end, false, wireState.id)
simulation.wires.push(wire)
}

View file

@ -6,6 +6,7 @@ import { saveStore } from '../stores/saveStore'
import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments'
import { CurrentLanguage } from '../../internalisation/stores/currentLanguage'
import { compileIc } from '../../integrated-circuits/helpers/compileIc'
/**
* Saves the state from a renderer in localStorage,
@ -25,6 +26,10 @@ export const save = (renderer: SimulationRenderer) => {
saveStore.set(current, state)
if (state.simulation.mode === 'ic') {
compileIc(state.simulation)
}
toast(
...createToastArguments(
translation.messages.savedSimulation(current),

View file

@ -1,15 +1,18 @@
import { Transform } from '../../../common/math/classes/Transform'
import { Pin } from './Pin'
import merge from 'deepmerge'
import { GateTemplate, PinCount } from '../types/GateTemplate'
import { DefaultGateTemplate } from '../constants'
import { idStore } from '../stores/idStore'
import { Context } from '../../activation/types/Context'
import { Context, InitialisationContext } from '../../activation/types/Context'
import { toFunction } from '../../activation/helpers/toFunction'
import { Subscription, combineLatest } from 'rxjs'
import { Subscription } from 'rxjs'
import { SimulationError } from '../../errors/classes/SimulationError'
import { throttleTime, debounce, debounceTime } from 'rxjs/operators'
import { getGateTimePipes } from '../helpers/getGateTimePipes'
import { ImageStore } from '../../simulationRenderer/stores/imageStore'
import { completeTemplate } from '../../logic-gates/helpers/completeTemplate'
import { Simulation, SimulationEnv } from './Simulation'
import { fromSimulationState } from '../../saving/helpers/fromState'
import { saveStore } from '../../saving/stores/saveStore'
import { Wire } from './Wire'
export interface GatePins {
inputs: Pin[]
@ -47,8 +50,14 @@ export class Gate {
private subscriptions: Subscription[] = []
private memory: Record<string, unknown> = {}
// Related to integration
private ghostSimulation: Simulation
private ghostWires: Wire[] = []
private isIntegrated = false
public env: SimulationEnv = 'global'
public constructor(template: DeepPartial<GateTemplate> = {}, id?: number) {
this.template = merge(DefaultGateTemplate, template) as GateTemplate
this.template = completeTemplate(template)
this.transform.scale = this.template.shape.scale
@ -73,6 +82,10 @@ export class Gate {
this
)
if (this.template.material.type === 'image') {
ImageStore.set(this.template.material.value)
}
this.id = id !== undefined ? id : idStore.generate()
for (const pin of this._pins.inputs) {
@ -84,6 +97,81 @@ export class Gate {
this.subscriptions.push(subscription)
}
this.init()
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')
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`
)
}
if (outputs.length !== this._pins.outputs.length) {
throw new SimulationError(
`Output count needs to match with the container gate`
)
}
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)
}
}
private init() {
toFunction<[InitialisationContext]>(
this.template.code.initialisation,
'context'
)({
memory: this.memory
})
}
public onClick() {
@ -100,15 +188,22 @@ export class Gate {
for (const subscription of this.subscriptions) {
subscription.unsubscribe()
}
if (this.isIntegrated) {
this.ghostSimulation.dispose()
}
}
public update() {
const context = this.getContext()
if (this.template.tags.includes('integrated')) {
} else {
const context = this.getContext()
if (!this.functions.activation)
throw new SimulationError('Activation function is missing')
if (!this.functions.activation)
throw new SimulationError('Activation function is missing')
this.functions.activation(context)
this.functions.activation(context)
}
}
public getContext(): Context {
@ -124,14 +219,11 @@ export class Gate {
if (this.template.material.type === 'color') {
this.template.material.value = color
}
}
},
enviroment: this.env
}
}
private getInputsStates() {
return this._pins.inputs.map(pin => pin.state)
}
private wrapPins(pins: Pin[]) {
const result: PinWrapper[] = []
const length = pins.length

View file

@ -16,8 +16,10 @@ export class Pin {
public constructor(public type = 0b01, public gate: Gate) {}
public addPair(pin: Pin, subscribe = false) {
this.pairs.add(pin)
public addPair(pin: Pin, subscribe = false, remember = true) {
if (remember) {
this.pairs.add(pin)
}
if (subscribe) {
const rawSubscription = pin.state.subscribe(state => {

View file

@ -4,17 +4,25 @@ import { LruCacheNode } from '@eix-js/utils'
import { Wire } from './Wire'
import { simulationMode } from '../../saving/types/SimulationSave'
/**
* The env a simulation can run in
*/
export type SimulationEnv = 'gate' | 'global'
export class Simulation {
public gates = new GateStorage()
public wires: Wire[] = []
public constructor(
public mode: simulationMode = 'project',
public name: string
public name: string,
public env: SimulationEnv = 'global'
) {}
public push(...gates: Gate[]) {
for (const gate of gates) {
gate.env = this.env
const node = new LruCacheNode<Gate>(gate.id, gate)
this.gates.set(gate.id, node)

View file

@ -9,14 +9,20 @@ export class Wire {
public constructor(
public start: PinWrapper,
public end: PinWrapper,
ic: boolean = false,
id?: number
) {
if (end.value.pairs.size !== 0) {
if (!ic && end.value.pairs.size !== 0) {
throw new SimulationError('An input pin can only have 1 pair')
}
end.value.addPair(start.value, true)
start.value.addPair(end.value)
end.value.addPair(start.value, true, !ic)
start.value.addPair(end.value, false, !ic)
// if (ic) {
// start.value.state.subscribe(console.log)
// end.value.state.subscribe(console.log)
// }
this.id = id !== undefined ? id : idStore.generate()
}

View file

@ -24,8 +24,9 @@ export const DefaultGateTemplate: GateTemplate = {
scale: [100, 100]
},
code: {
activation: 'context.set(0,true)',
onClick: ''
activation: '',
onClick: '',
initialisation: ''
},
simulation: {
debounce: {
@ -36,5 +37,11 @@ export const DefaultGateTemplate: GateTemplate = {
enabled: false
}
},
info: []
integration: {
allowed: true,
input: false,
output: false
},
info: [],
tags: ['base']
}

View file

@ -6,7 +6,7 @@ export interface PinCount {
}
export interface Material {
type: 'color'
type: 'color' | 'image'
value: string
}
@ -28,6 +28,8 @@ export type TimePipe = Enabled<{
time: number
}>
export type GateTag = 'base' | 'imported' | 'integrated'
export interface GateTemplate {
material: Material
shape: Shape
@ -39,6 +41,7 @@ export interface GateTemplate {
name: string
}
code: {
initialisation: string
activation: string
onClick: string
}
@ -46,5 +49,11 @@ export interface GateTemplate {
throttle: TimePipe
debounce: TimePipe
}
integration: {
allowed: boolean
input: boolean
output: boolean
}
info: string[]
tags: GateTag[]
}

View file

@ -115,17 +115,22 @@ export class SimulationRenderer {
pin.value === this.selectedPins.start.wrapper.value
) {
this.selectedPins.start = null
this.selectedPins.end = null
} else if (
this.selectedPins.end &&
pin.value === this.selectedPins.end.wrapper.value
) {
this.selectedPins.start = null
this.selectedPins.end = null
} else if ((pin.value.type & 0b10) >> 1) {
this.selectedPins.start = {
wrapper: pin,
transform
}
} else if (pin.value.type & 1) {
} else if (
pin.value.type & 1 &&
pin.value.pairs.size === 0
) {
this.selectedPins.end = {
wrapper: pin,
transform

View file

@ -1,4 +1,5 @@
import { SimulationRendererOptions } from './types/SimulationRendererOptions'
import { vector2 } from '../../common/math/classes/Transform'
export const defaultSimulationRendererOptions: SimulationRendererOptions = {
dnd: {
@ -24,3 +25,5 @@ export const defaultSimulationRendererOptions: SimulationRendererOptions = {
curvePointOffset: 100
}
}
export const imageQuality: vector2 = [100, 100]

View file

@ -1,7 +1,10 @@
import { Gate } from '../../simulation/classes/Gate'
import { drawRotatedSquare } from '../../../common/canvas/helpers/drawRotatedSquare'
import { renderPins } from './renderPins'
import { SimulationRenderer } from '../classes/SimulationRenderer'
import { useTransform } from '../../../common/canvas/helpers/useTransform'
import { roundRect } from '../../../common/canvas/helpers/drawRoundedSquare'
import { roundImage } from '../../../common/canvas/helpers/drawRoundedImage'
import { ImageStore } from '../stores/imageStore'
export const renderGate = (
ctx: CanvasRenderingContext2D,
@ -18,9 +21,33 @@ export const renderGate = (
ctx.lineWidth = renderer.options.gates.gateStroke.width
ctx.save()
const r = useTransform(ctx, gate.transform)
const renderingParameters = [
r.x,
r.y,
r.width,
r.height,
gate.template.shape.rounded ? gate.template.shape.radius : 0
]
if (gate.template.material.type === 'image') {
roundImage(
ctx,
ImageStore.get(gate.template.material.value),
...renderingParameters
)
}
roundRect(ctx, ...renderingParameters)
ctx.stroke()
if (gate.template.material.type === 'color') {
ctx.fillStyle = gate.template.material.value
drawRotatedSquare(ctx, gate.transform, gate.template.shape)
ctx.stroke()
ctx.fill()
}
ctx.restore()
}

View file

@ -0,0 +1,54 @@
import { imageQuality } from '../constants'
import { SimulationError } from '../../errors/classes/SimulationError'
/**
* Creates an image from a given url
*
* @param url The url of the image
*/
export const toImage = (url: string) => {
const image = new Image(...imageQuality)
image.src = url
return image
}
/**
* Store to be sure no more than 1 image oer url is created
*/
export const ImageStore = {
/**
* Map holding the url - image pairs
*/
memory: new Map<string, HTMLImageElement>(),
/**
* If the image doesnt exist it'll create it from the url.
* If it does the method does nothing.
*
* @param url the url of the image
*/
set(url: string) {
if (!ImageStore.memory.has(url)) {
ImageStore.memory.set(url, toImage(url))
}
},
/**
* Returns the image object from the given url
*
*
* @throws SimulationError if the image doesnt exist
*
* @param url The url of the image
*/
get(url: string) {
const image = ImageStore.memory.get(url)
if (!image) {
throw new SimulationError(`Cannot get image ${url}`)
}
return image
}
}

View file

@ -17,6 +17,11 @@ const babelRule = {
use: 'babel-loader'
}
const fileRule = {
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
const cssAndSass = [
isProduction
? MiniCssExtractPlugin.loader
@ -56,10 +61,19 @@ const baseConfig = {
publicPath: '/'
},
module: {
rules: [babelRule, sassRule, cssRule]
rules: [babelRule, sassRule, cssRule, fileRule]
},
resolve: {
extensions: ['.js', '.ts', '.tsx', '.scss']
extensions: [
'.js',
'.ts',
'.tsx',
'.scss',
'.png',
'.svg',
'.jpg',
'.gif'
]
},
plugins: []
}