added half and full adders

This commit is contained in:
Matei Adriel 2019-07-30 23:58:21 +03:00
parent 285248435a
commit 120e4116b2
37 changed files with 606 additions and 308 deletions

10
src/assets/full-adder.svg Normal file
View file

@ -0,0 +1,10 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M800 0H0V800H800V0Z" fill="#00B512"/>
<path d="M398 295L396.522 504C322.104 504 289 469.556 289 401.327C289 333.097 324.731 295 398 295Z" fill="white" stroke="white" stroke-width="10"/>
<path d="M400 295L401.142 504C458.653 504 511 471.73 511 403.5C511 335.27 456.624 295 400 295Z" fill="white" stroke="white" stroke-width="10"/>
<path d="M416 333H386V390H315V416.5H386V466H416V416.5H481.5V390H416V333Z" fill="#00B512" stroke="white" stroke-width="10"/>
<path d="M511 400H597.5" stroke="white" stroke-width="10"/>
<path d="M228 333H314.5" stroke="white" stroke-width="10"/>
<path d="M202 400H288.5" stroke="white" stroke-width="10"/>
<path d="M227 464H313.5" stroke="white" stroke-width="10"/>
</svg>

After

Width:  |  Height:  |  Size: 810 B

View file

@ -0,0 +1,9 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="800" fill="#00B512"/>
<path d="M398 295L396.522 504C322.104 504 289 469.556 289 401.327C289 333.097 324.731 295 398 295Z" fill="white" stroke="white" stroke-width="10"/>
<path d="M400 295L401.142 504C458.653 504 511 471.73 511 403.5C511 335.27 456.624 295 400 295Z" stroke="white" stroke-width="10"/>
<path d="M416 333H386V390H315V416.5H386V466H416V416.5H481.5V390H416V333Z" fill="#00B512" stroke="white" stroke-width="10"/>
<path d="M511 400H597.5" stroke="white" stroke-width="10"/>
<path d="M228 333H314.5" stroke="white" stroke-width="10"/>
<path d="M227 464H313.5" stroke="white" stroke-width="10"/>
</svg>

After

Width:  |  Height:  |  Size: 738 B

View file

@ -1,12 +1,17 @@
import { allCombinations } from '../../../modules/simulation/helpers/allCombinations'
import { rotateAroundVector } from '../../../modules/vector2/helpers/rotate'
import { BehaviorSubject } from 'rxjs'
import { vector2 } from '../types/vector2'
export class Transform {
public positionSubject = new BehaviorSubject<vector2>([0, 0])
public constructor(
public position: vector2 = [0, 0],
public _position: vector2 = [0, 0],
public scale: vector2 = [1, 1],
public rotation = 0
) {}
) {
this.updatePositionSubject()
}
public getBoundingBox() {
const result = [...this.position, ...this.scale] as vector4
@ -37,6 +42,29 @@ export class Transform {
return edges as [vector2, vector2][]
}
/**
* Pushes the current position trough the position subject
*/
private updatePositionSubject() {
this.positionSubject.next(this.position)
}
/**
* getter for the position
*/
get position() {
return this._position
}
/**
* setter for the position
*/
set position(value: vector2) {
this._position = value
this.updatePositionSubject()
}
/** Short forms for random stuff */
get x() {
@ -77,10 +105,14 @@ export class Transform {
set x(value: number) {
this.position = [value, this.y]
this.updatePositionSubject()
}
set y(value: number) {
this.position = [this.x, value]
this.updatePositionSubject()
}
set width(value: number) {
@ -92,7 +124,6 @@ export class Transform {
}
}
export type vector2 = [number, number]
export type vector3 = [number, number, number]
export type vector4 = [number, number, number, number]
export type vector8 = [

View file

@ -9,6 +9,7 @@ export interface Context {
get: (index: number) => boolean
set: (index: number, state: boolean) => void
color: (color: string) => void
innerText: (value: string) => void
update: () => void
enviroment: SimulationEnv
colors: Record<string, string>

View file

@ -1,9 +1,9 @@
import React, { RefObject, forwardRef, MouseEvent, WheelEvent } from 'react'
import { useObservable } from 'rxjs-hooks'
import { Subject } from 'rxjs'
import { mouseButton } from '../types/mouseButton'
import { MouseEventInfo } from './MouseEventInfo'
import { MouseEventInfo } from '../types/MouseEventInfo'
import { width, height } from '../../screen/helpers/Screen'
import { getEventInfo } from '../helpers/getEventInfo'
export interface FluidCanvasProps {
mouseDownOuput: Subject<MouseEventInfo>
@ -11,15 +11,6 @@ export interface FluidCanvasProps {
mouseMoveOutput: Subject<MouseEventInfo>
}
export const getEventInfo = (
e: MouseEvent<HTMLCanvasElement>
): MouseEventInfo => {
return {
button: e.button as mouseButton,
position: [e.clientX, e.clientY]
}
}
export const mouseEventHandler = (output: Subject<MouseEventInfo>) => (
e: MouseEvent<HTMLCanvasElement>
) => {

View file

@ -0,0 +1,17 @@
import { MouseEvent } from 'react'
import { MouseEventInfo } from '../types/MouseEventInfo'
import { mouseButton } from '../types/mouseButton'
/**
* Extracts the bareminimum from a mouseEvent instance
*
* @param e The MouseEvent instance
*/
export const getEventInfo = (
e: MouseEvent<HTMLCanvasElement | HTMLDivElement>
): MouseEventInfo => {
return {
button: e.button as mouseButton,
position: [e.clientX, e.clientY]
}
}

View file

@ -1,2 +1,6 @@
@import './mui-overrides.scss';
@import './toasts.scss';
* {
user-select: none;
}

View file

@ -1,5 +1,5 @@
import { vector2 } from '../../../common/math/types/vector2'
import { mouseButton } from '../types/mouseButton'
import { mouseButton } from './mouseButton'
/**
* The info about the mouse passed to mouse subjects

View file

@ -1,4 +1,4 @@
import { Subject } from 'rxjs'
import { MouseEventInfo } from '../components/MouseEventInfo'
import { MouseEventInfo } from './MouseEventInfo'
export type MouseSubject = Subject<MouseEventInfo>

View file

@ -1,8 +1,7 @@
import './GateProperties.scss'
import React, { ChangeEvent } from 'react'
import React, { ChangeEvent, MouseEvent } from 'react'
import { getRendererSafely } from '../helpers/getRendererSafely'
import { Property } from '../../simulation/types/GateTemplate'
import { BehaviorSubject } from 'rxjs'
import { useObservable } from 'rxjs-hooks'
import Divider from '@material-ui/core/Divider'
import TextField from '@material-ui/core/TextField'
@ -12,8 +11,6 @@ import { Gate } from '../../simulation/classes/Gate'
export interface GatePropertyProps {
raw: Property
name: string
output: BehaviorSubject<string | number | boolean>
gate: Gate
}
@ -22,8 +19,10 @@ export interface GatePropertyProps {
*
* @param param0 The props passed to the component
*/
export const GatePropery = ({ name, raw, output, gate }: GatePropertyProps) => {
const outputSnapshot = useObservable(() => output, '')
export const GatePropery = ({ raw, gate }: GatePropertyProps) => {
const { name } = raw
const prop = gate.props[name]
const outputSnapshot = useObservable(() => prop, '')
const displayableName = `${name[0].toUpperCase()}${name.slice(1)}:`
@ -37,10 +36,8 @@ export const GatePropery = ({ name, raw, output, gate }: GatePropertyProps) => {
value = Number(target.value)
}
gate.props[name] = value
if (raw.type !== 'boolean') {
output.next(target.value)
prop.next(value)
}
if (raw.needsUpdate === true) {
@ -49,13 +46,16 @@ export const GatePropery = ({ name, raw, output, gate }: GatePropertyProps) => {
}
let input = <></>
if (raw.type === 'number' || raw.type === 'text') {
if (raw.type === 'number' || raw.type === 'text' || raw.type === 'string') {
input = (
<TextField
onChange={handleChange}
label={displayableName}
value={outputSnapshot}
type={raw.type}
multiline={raw.type === 'string'}
rowsMax={7}
/>
)
} else if (raw.type === 'boolean') {
@ -64,7 +64,7 @@ export const GatePropery = ({ name, raw, output, gate }: GatePropertyProps) => {
<span className="checkbox-label">{displayableName}</span>
<CheckBox
onClick={() => {
output.next(!outputSnapshot)
prop.next(!outputSnapshot)
}}
onChange={handleChange}
checked={!!outputSnapshot}
@ -93,7 +93,6 @@ const GateProperties = () => {
}
const gate = node.data
const gateProps = gate.props
return (
<div
@ -113,20 +112,11 @@ const GateProperties = () => {
<Divider id="gate-props-divider" />
{gate.template.properties.data.map((raw, index) => {
const { name, base } = raw
const prop = gateProps[name]
const output = new BehaviorSubject(
gateProps.hasOwnProperty(name) ? prop : base
)
return (
<GatePropery
output={output}
raw={raw}
name={name}
gate={gate}
key={`${index}-${id.value}-${output.value}`}
key={`${index}-${id.value}`}
/>
)
})}

View file

@ -1,4 +1,4 @@
import { BehaviorSubject, Subject } from 'rxjs'
export const open = new Subject<boolean>()
export const open = new BehaviorSubject<boolean>(false)
export const id = new BehaviorSubject<number>(0)

View file

@ -1,6 +1,14 @@
import { InputStore } from '../../input/stores/InputStore'
import { CreateSimulationStore } from '../../create-simulation/stores/CreateSimulationStore'
import { open as propModalIsOpen } from '../../logic-gates/subjects/LogicGatePropsSubjects'
/**
* Returns true if any modal is open
*/
export const modalIsOpen = () => {
return InputStore.data.open.value || CreateSimulationStore.data.open.value
return (
InputStore.data.open.value ||
CreateSimulationStore.data.open.value ||
propModalIsOpen.value
)
}

View file

@ -12,6 +12,8 @@ import rgbLightTemplate from './templates/rgb'
import sequentialDelayerTemplate from './templates/sequentialDelayer'
import xnorTemplate from './templates/xnor'
import xorTemplate from './templates/xor'
import halfAdderTemplate from './templates/halfAdder'
import fullAdderTemplate from './templates/fullAdder'
export const defaultSimulationName = 'default'
export const baseTemplates: DeepPartial<GateTemplate>[] = [
@ -26,7 +28,10 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
rgbLightTemplate,
sequentialDelayerTemplate,
xnorTemplate,
xorTemplate
xorTemplate,
halfAdderTemplate,
fullAdderTemplate
// commentTemplate
]
export const baseSave: RendererState = {

View file

@ -62,7 +62,7 @@ export const getGateState = (gate: Gate): GateState => {
id: gate.id,
template: gate.template.metadata.name,
transform: getTransformState(gate.transform),
props: gate.props
props: gate.getProps()
}
}

View file

@ -0,0 +1,47 @@
import { PartialTemplate } from '../types/PartialTemplate'
/**
* The template of the comment gate
*/
const commentTemplate: PartialTemplate = {
metadata: {
name: 'comment'
},
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'
}
}
export default commentTemplate

View file

@ -0,0 +1,32 @@
import { PartialTemplate } from '../types/PartialTemplate'
/**
* The template of the fullAdder gate
*/
const fullAdderTemplate: PartialTemplate = {
metadata: {
name: 'full adder'
},
material: {
type: 'image',
fill: require('../../../assets/full-adder')
},
code: {
activation: `
const result = context.get(0) + context.get(1) + context.get(2)
context.set(0, result & 1)
context.set(1, result >> 1)
`
},
pins: {
inputs: {
count: 3
},
outputs: {
count: 2
}
}
}
export default fullAdderTemplate

View file

@ -0,0 +1,32 @@
import { PartialTemplate } from '../types/PartialTemplate'
/**
* The template of the halfAdder gate
*/
const halfAdderTemplate: PartialTemplate = {
metadata: {
name: 'half adder'
},
material: {
type: 'image',
fill: require('../../../assets/half-adder')
},
code: {
activation: `
const result = context.get(0) + context.get(1)
context.set(0, result & 1)
context.set(1, result >> 1)
`
},
pins: {
inputs: {
count: 2
},
outputs: {
count: 2
}
}
}
export default halfAdderTemplate

View file

@ -1,4 +1,4 @@
import { vector2 } from '../../../common/math/classes/Transform'
import { vector2 } from '../../../common/math/types/vector2'
export type simulationMode = 'ic' | 'project'

View file

@ -4,7 +4,7 @@ import { GateTemplate, PinCount } from '../types/GateTemplate'
import { idStore } from '../stores/idStore'
import { Context, InitialisationContext } from '../../activation/types/Context'
import { toFunction } from '../../activation/helpers/toFunction'
import { Subscription } from 'rxjs'
import { Subscription, BehaviorSubject } from 'rxjs'
import { SimulationError } from '../../errors/classes/SimulationError'
import { getGateTimePipes } from '../helpers/getGateTimePipes'
import { ImageStore } from '../../simulationRenderer/stores/imageStore'
@ -15,6 +15,7 @@ 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 } from 'rxjs/operators'
/**
* The interface for the pins of a gate
@ -115,10 +116,20 @@ export class Gate {
*/
public env: SimulationEnv = 'global'
/**
* Holds all the gate-related text
*/
public text = {
inner: new BehaviorSubject('text goes here')
}
/**
* The props used by the activation function (the same as memory but presists)
*/
public props: Record<string, string | number | boolean> = {}
public props: Record<
string,
BehaviorSubject<string | number | boolean>
> = {}
/**
* The main logic gate class
@ -129,7 +140,7 @@ export class Gate {
public constructor(
template: DeepPartial<GateTemplate> = {},
id?: number,
props: Record<string, string> = {}
props: Record<string, string | number | boolean> = {}
) {
this.template = completeTemplate(template)
@ -259,15 +270,15 @@ export class Gate {
/**
* Assign the props passed to the gate and mere them with the base ones
*/
private assignProps(props: Record<string, string>) {
private assignProps(props: Record<string, string | boolean | number>) {
let shouldUpdate = false
if (this.template.properties.enabled) {
for (const { base, name, needsUpdate } of this.template.properties
.data) {
this.props[name] = props.hasOwnProperty(name)
? props[name]
: base
this.props[name] = new BehaviorSubject(
props.hasOwnProperty(name) ? props[name] : base
)
if (!shouldUpdate && needsUpdate) {
shouldUpdate = true
@ -301,6 +312,19 @@ export class Gate {
}
}
/**
* Used to get the props as an object
*/
public getProps() {
const props: Record<string, string | boolean | number> = {}
for (const key in this.props) {
props[key] = this.props[key].value
}
return props
}
/**
* Clears subscriptions to prevent memory leaks
*/
@ -349,10 +373,13 @@ export class Gate {
}
},
getProperty: (name: string) => {
return this.props[name]
return this.props[name].value
},
setProperty: (name: string, value: string | number | boolean) => {
this.props[name] = value
this.props[name].next(value)
},
innerText: (value: string) => {
this.text.inner.next(value)
},
update: () => {
this.update()
@ -369,7 +396,8 @@ export class Gate {
* Generates pin wrappers from an array of pins
*
* @param pins The pins to wwap
*/ private wrapPins(pins: Pin[]) {
*/
private wrapPins(pins: Pin[]) {
const result: PinWrapper[] = []
const length = pins.length

View file

@ -3,6 +3,7 @@ import { GateStorage } from './GateStorage'
import { LruCacheNode } from '@eix-js/utils'
import { Wire } from './Wire'
import { simulationMode } from '../../saving/types/SimulationSave'
import { BehaviorSubject } from 'rxjs'
/**
* The env a simulation can run in

View file

@ -53,5 +53,9 @@ export const DefaultGateTemplate: GateTemplate = {
properties: {
enabled: false,
data: []
},
innerText: {
enabled: false,
color: 'white'
}
}

View file

@ -4,7 +4,7 @@ import { Gate } from '../classes/Gate'
import { add, relativeTo, multiply } from '../../vector2/helpers/basic'
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
import { DefaultGateTemplate } from '../constants'
import { vector2 } from '../../../common/math/classes/Transform'
import { vector2 } from '../../../common/math/types/vector2'
import { Screen } from '../../screen/helpers/Screen'
import { toast } from 'react-toastify'
import { createToastArguments } from '../../toasts/helpers/createToastArguments'

View file

@ -1,4 +1,4 @@
import { vector2 } from '../../../common/math/classes/Transform'
import { vector2 } from '../../../common/math/types/vector2'
export interface PinCount {
variable: boolean
@ -75,4 +75,8 @@ export interface GateTemplate {
enabled: boolean
data: Property[]
}
innerText: {
color: string
enabled: boolean
}
}

View file

@ -1,14 +1,42 @@
import { Transform } from '../../../common/math/classes/Transform'
import { vector2 } from '../../../common/math/types/vector2'
import {
substract,
multiplyVectors,
add,
inverse
} from '../../vector2/helpers/basic'
/**
* worldPosition(x) = (x - p) / s
* s * wp = x - p
* screenPosition(x) = s * x + p
*/
export class Camera {
public transform = new Transform([0, 0])
/**
* Gets the world position of a vector
*
* @param position The position to transform
*/
public toWordPostition(position: vector2) {
return [
(position[0] - this.transform.position[0]) /
this.transform.scale[0],
(position[1] - this.transform.position[1]) / this.transform.scale[1]
] as vector2
return multiplyVectors(
substract(position, this.transform.position),
inverse(this.transform.scale)
)
}
/**
* Gets the screen position of a vector
*
* @param position The position to transform
*/
public toScreenPosition(position: vector2) {
return add(
multiplyVectors(position, this.transform.scale),
this.transform.position
)
}
}

View file

@ -1,20 +1,11 @@
import { Camera } from './Camera'
import { Simulation } from '../../simulation/classes/Simulation'
import { Subject } from 'rxjs'
import { MouseEventInfo } from '../../core/components/MouseEventInfo'
import { pointInSquare } from '../../../common/math/helpers/pointInSquare'
import { MouseEventInfo } from '../../core/types/MouseEventInfo'
import { vector2 } from '../../../common/math/types/vector2'
import { relativeTo, add, invert } from '../../vector2/helpers/basic'
import { SimulationRendererOptions } from '../types/SimulationRendererOptions'
import {
defaultSimulationRendererOptions,
mouseButtons,
shiftInput
} from '../constants'
import { getPinPosition } from '../helpers/pinPosition'
import { pointInCircle } from '../../../common/math/helpers/pointInCircle'
import { defaultSimulationRendererOptions } from '../constants'
import { SelectedPins } from '../types/SelectedPins'
import { Wire } from '../../simulation/classes/Wire'
import { currentStore } from '../../saving/stores/currentStore'
import { saveStore } from '../../saving/stores/saveStore'
import {
@ -27,18 +18,14 @@ import { RefObject } from 'react'
import { dumpSimulation } from '../../saving/helpers/dumpSimulation'
import { modalIsOpen } from '../../modals/helpers/modalIsOpen'
import { SimulationError } from '../../errors/classes/SimulationError'
import { deleteWire } from '../../simulation/helpers/deleteWire'
import { RendererState, WireState } from '../../saving/types/SimulationSave'
import { setToArray } from '../../../common/lang/arrays/helpers/setToArray'
import { Transform } from '../../../common/math/classes/Transform'
import { gatesInSelection } from '../helpers/gatesInSelection'
import { selectionType } from '../types/selectionType'
import { addIdToSelection, idIsSelected } from '../helpers/idIsSelected'
import { GateInitter } from '../types/GateInitter'
import {
open as propsAreOpen,
id as propsModalId
} from '../../logic-gates/subjects/LogicGatePropsSubjects'
import { handleMouseDown } from '../helpers/handleMouseDown'
import { handleMouseUp } from '../helpers/handleMouseUp'
import { handleMouseMove } from '../helpers/handleMouseMove'
export class SimulationRenderer {
public mouseDownOutput = new Subject<MouseEventInfo>()
@ -83,223 +70,9 @@ export class SimulationRenderer {
}
public init() {
this.mouseDownOutput.subscribe(event => {
const worldPosition = this.camera.toWordPostition(event.position)
const gates = Array.from(this.simulation.gates)
this.lastMousePosition = worldPosition
// We need to iterate from the last to the first
// because if we have 2 overlapping gates,
// we want to select the one on top
for (let index = gates.length - 1; index >= 0; index--) {
const { transform, id, pins, template } = gates[index]
if (pointInSquare(worldPosition, transform)) {
if (event.button === mouseButtons.drag) {
gates[index].onClick()
this.mouseState |= 1
if (!idIsSelected(this, id)) {
this.clearSelection()
addIdToSelection(this, 'temporary', id)
}
const gateNode = this.simulation.gates.get(id)
if (gateNode) {
return this.simulation.gates.moveOnTop(gateNode)
} else {
throw new SimulationError(
`Cannot find gate with id ${id}`
)
}
} else if (
event.button === mouseButtons.properties &&
template.properties.enabled
) {
propsModalId.next(id)
return propsAreOpen.next(true)
}
}
for (const pin of pins) {
const position = getPinPosition(this, transform, pin)
if (
pointInCircle(
worldPosition,
position,
this.options.gates.pinRadius
)
) {
if (pin.value.pairs.size) {
if (pin.value.type & 1) {
const wire = this.simulation.wires.find(
wire => wire.end.value === pin.value
)
if (wire) {
deleteWire(this.simulation, wire)
} else {
throw new SimulationError(
`Cannot find wire to remove`
)
}
return
}
}
if (
this.selectedPins.start &&
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 & 2) >> 1) {
this.selectedPins.start = {
wrapper: pin,
transform
}
} else if (pin.value.type & 1) {
this.selectedPins.end = {
wrapper: pin,
transform
}
}
if (this.selectedPins.start && this.selectedPins.end) {
this.simulation.wires.push(
new Wire(
this.selectedPins.start.wrapper,
this.selectedPins.end.wrapper
)
)
this.selectedPins.start = null
this.selectedPins.end = null
}
return
}
}
}
if (!shiftInput.value && event.button === mouseButtons.unselect) {
this.clearSelection()
}
if (event.button === mouseButtons.pan) {
// the second bit = pannning
this.mouseState |= 0b10
} else if (event.button === mouseButtons.select) {
this.selectedArea.position = this.lastMousePosition
this.selectedArea.scale = [0, 0]
// the third bit = selecting
this.mouseState |= 0b100
}
})
this.mouseUpOutput.subscribe(event => {
if (event.button === mouseButtons.drag && this.mouseState & 1) {
const selected = this.getSelected()
for (const gate of selected) {
gate.transform.rotation = 0
}
this.selectedGates.temporary.clear()
// turn first bit to 0
this.mouseState &= 6
// for debugging
if ((this.mouseState >> 1) & 1 || this.mouseState & 1) {
throw new SimulationError(
'First 2 bits of mouseState need to be set to 0'
)
}
}
if (
event.button === mouseButtons.pan &&
(this.mouseState >> 1) & 1
) {
// turn second bit to 0
this.mouseState &= 5
}
if (event.button === mouseButtons.select && this.mouseState >> 2) {
// turn the third bit to 0
this.mouseState &= 3
const selectedGates = gatesInSelection(
this.selectedArea,
Array.from(this.simulation.gates)
)
for (const { id } of selectedGates) {
addIdToSelection(this, 'permanent', id)
const node = this.simulation.gates.get(id)
if (node) {
this.simulation.gates.moveOnTop(node)
} else {
throw new SimulationError(
`Cannot find node in gate storage with id ${id}`
)
}
}
}
})
this.mouseMoveOutput.subscribe(event => {
const worldPosition = this.camera.toWordPostition(event.position)
const offset = invert(
relativeTo(this.lastMousePosition, worldPosition)
)
const scaledOffset = offset.map(
(value, index) => value * this.camera.transform.scale[index]
) as vector2
if (this.mouseState & 1) {
for (const gate of this.getSelected()) {
const { transform } = gate
transform.x -= offset[0]
transform.y -= offset[1]
}
}
if ((this.mouseState >> 1) & 1) {
this.camera.transform.position = add(
this.camera.transform.position,
invert(scaledOffset)
)
this.spawnCount = 0
}
if ((this.mouseState >> 2) & 1) {
this.selectedArea.scale = relativeTo(
this.selectedArea.position,
this.camera.toWordPostition(event.position)
)
}
this.lastMousePosition = this.camera.toWordPostition(event.position)
})
this.mouseDownOutput.subscribe(handleMouseDown(this))
this.mouseUpOutput.subscribe(handleMouseUp(this))
this.mouseMoveOutput.subscribe(handleMouseMove(this))
this.reloadSave()
}
@ -316,6 +89,11 @@ export class SimulationRenderer {
}
}
/**
* Loads a simulation state
*
* @param save LThe state to load
*/
public loadSave(save: RendererState) {
this.simulation = fromSimulationState(save.simulation)
this.camera = fromCameraState(save.camera)

View file

@ -1,5 +1,5 @@
import { SimulationRendererOptions } from './types/SimulationRendererOptions'
import { vector2 } from '../../common/math/classes/Transform'
import { vector2 } from '../../common/math/types/vector2'
import { mouseButton } from '../core/types/mouseButton'
import { KeyboardInput } from '../keybindings/classes/KeyboardInput'

View file

@ -0,0 +1,15 @@
import { currentContext } from '../subjects'
import { SimulationError } from '../../errors/classes/SimulationError'
/**
* Gets the current context and throw an error if it doesnt exist
*
* @throws SimulationError if no context can be found
*/
export const getCurrentContextSafely = () => {
if (currentContext.value) {
return currentContext.value
} else {
throw new SimulationError(`Cannot find a rendering context`)
}
}

View file

@ -0,0 +1,135 @@
import { MouseEventInfo } from '../../core/types/MouseEventInfo'
import { SimulationRenderer } from '../classes/SimulationRenderer'
import { pointInSquare } from '../../../common/math/helpers/pointInSquare'
import { idIsSelected, addIdToSelection } from './idIsSelected'
import { mouseButtons, shiftInput } from '../constants'
import { SimulationError } from '../../errors/classes/SimulationError'
import { openGateProps } from './openGateProps'
import { getPinPosition } from './pinPosition'
import { pointInCircle } from '../../../common/math/helpers/pointInCircle'
import { deleteWire } from '../../simulation/helpers/deleteWire'
import { Wire } from '../../simulation/classes/Wire'
export const handleMouseDown = (renderer: SimulationRenderer) => (
event: MouseEventInfo
) => {
const worldPosition = renderer.camera.toWordPostition(event.position)
const gates = Array.from(renderer.simulation.gates)
renderer.lastMousePosition = worldPosition
// We need to iterate from the last to the first
// because if we have 2 overlapping gates,
// we want to select the one on top
for (let index = gates.length - 1; index >= 0; index--) {
const { transform, id, pins, template } = gates[index]
if (pointInSquare(worldPosition, transform)) {
if (event.button === mouseButtons.drag) {
gates[index].onClick()
renderer.mouseState |= 1
if (!idIsSelected(renderer, id)) {
renderer.clearSelection()
addIdToSelection(renderer, 'temporary', id)
}
const gateNode = renderer.simulation.gates.get(id)
if (gateNode) {
return renderer.simulation.gates.moveOnTop(gateNode)
} else {
throw new SimulationError(`Cannot find gate with id ${id}`)
}
} else if (
event.button === mouseButtons.properties &&
template.properties.enabled
) {
return openGateProps(id)
}
}
for (const pin of pins) {
const position = getPinPosition(renderer, transform, pin)
if (
pointInCircle(
worldPosition,
position,
renderer.options.gates.pinRadius
)
) {
if (pin.value.pairs.size) {
if (pin.value.type & 1) {
const wire = renderer.simulation.wires.find(
wire => wire.end.value === pin.value
)
if (wire) {
deleteWire(renderer.simulation, wire)
} else {
throw new SimulationError(
`Cannot find wire to remove`
)
}
return
}
}
if (
renderer.selectedPins.start &&
pin.value === renderer.selectedPins.start.wrapper.value
) {
renderer.selectedPins.start = null
renderer.selectedPins.end = null
} else if (
renderer.selectedPins.end &&
pin.value === renderer.selectedPins.end.wrapper.value
) {
renderer.selectedPins.start = null
renderer.selectedPins.end = null
} else if ((pin.value.type & 2) >> 1) {
renderer.selectedPins.start = {
wrapper: pin,
transform
}
} else if (pin.value.type & 1) {
renderer.selectedPins.end = {
wrapper: pin,
transform
}
}
if (renderer.selectedPins.start && renderer.selectedPins.end) {
renderer.simulation.wires.push(
new Wire(
renderer.selectedPins.start.wrapper,
renderer.selectedPins.end.wrapper
)
)
renderer.selectedPins.start = null
renderer.selectedPins.end = null
}
return
}
}
}
if (!shiftInput.value && event.button === mouseButtons.unselect) {
renderer.clearSelection()
}
if (event.button === mouseButtons.pan) {
// the second bit = pannning
renderer.mouseState |= 0b10
} else if (event.button === mouseButtons.select) {
renderer.selectedArea.position = renderer.lastMousePosition
renderer.selectedArea.scale = [0, 0]
// the third bit = selecting
renderer.mouseState |= 0b100
}
}

View file

@ -0,0 +1,47 @@
import { SimulationRenderer } from '../classes/SimulationRenderer'
import { MouseEventInfo } from '../../core/types/MouseEventInfo'
import {
substract,
multiplyVectors,
relativeTo
} from '../../vector2/helpers/basic'
export const handleMouseMove = (renderer: SimulationRenderer) => (
event: MouseEventInfo
) => {
const worldPosition = renderer.camera.toWordPostition(event.position)
const offset = substract(renderer.lastMousePosition, worldPosition)
const scaledOffset = multiplyVectors(
offset,
renderer.camera.transform.scale
)
if (renderer.mouseState & 1) {
for (const gate of renderer.getSelected()) {
const { transform } = gate
transform.x -= offset[0]
transform.y -= offset[1]
}
}
if ((renderer.mouseState >> 1) & 1) {
renderer.camera.transform.position = substract(
renderer.camera.transform.position,
scaledOffset
)
renderer.spawnCount = 0
}
if ((renderer.mouseState >> 2) & 1) {
renderer.selectedArea.scale = relativeTo(
renderer.selectedArea.position,
renderer.camera.toWordPostition(event.position)
)
}
renderer.lastMousePosition = renderer.camera.toWordPostition(event.position)
}

View file

@ -0,0 +1,59 @@
import { SimulationRenderer } from '../classes/SimulationRenderer'
import { MouseEventInfo } from '../../core/types/MouseEventInfo'
import { mouseButtons } from '../constants'
import { SimulationError } from '../../errors/classes/SimulationError'
import { addIdToSelection } from './idIsSelected'
import { gatesInSelection } from './gatesInSelection'
export const handleMouseUp = (renderer: SimulationRenderer) => (
event: MouseEventInfo
) => {
if (event.button === mouseButtons.drag && renderer.mouseState & 1) {
const selected = renderer.getSelected()
for (const gate of selected) {
gate.transform.rotation = 0
}
renderer.selectedGates.temporary.clear()
// turn first bit to 0
renderer.mouseState &= 6
// for debugging
if ((renderer.mouseState >> 1) & 1 || renderer.mouseState & 1) {
throw new SimulationError(
'First 2 bits of mouseState need to be set to 0'
)
}
}
if (event.button === mouseButtons.pan && (renderer.mouseState >> 1) & 1) {
// turn second bit to 0
renderer.mouseState &= 5
}
if (event.button === mouseButtons.select && renderer.mouseState >> 2) {
// turn the third bit to 0
renderer.mouseState &= 3
const selectedGates = gatesInSelection(
renderer.selectedArea,
Array.from(renderer.simulation.gates)
)
for (const { id } of selectedGates) {
addIdToSelection(renderer, 'permanent', id)
const node = renderer.simulation.gates.get(id)
if (node) {
renderer.simulation.gates.moveOnTop(node)
} else {
throw new SimulationError(
`Cannot find node in gate storage with id ${id}`
)
}
}
}
}

View file

@ -0,0 +1,14 @@
import {
open as propsAreOpen,
id as propsModalId
} from '../../logic-gates/subjects/LogicGatePropsSubjects'
/**
* Opens the properties modal for a gate
*
* @param id The id of the gate
*/
export const openGateProps = (id: number) => {
propsModalId.next(id)
return propsAreOpen.next(true)
}

View file

@ -1,9 +0,0 @@
import { vector3 } from '../../../common/math/types/vector3'
import { vector2 } from '../../../common/math/types/vector2'
export const projectPointOnPlane = (point: vector3, light: vector3) =>
point.slice(0, 2).map((position, index) => {
const delta = light[index] - position
return light[index] - (delta + (point[2] * delta) / light[2])
}) as vector2

View file

@ -5,6 +5,7 @@ import { clearCanvas } from '../../../common/canvas/helpers/clearCanvas'
import { renderClickedPins } from './renderClickedPins'
import { renderWires } from './renderWires'
import { renderSelectedArea } from './renderSelectedArea'
import { currentContext } from '../subjects'
export const renderSimulation = (
ctx: CanvasRenderingContext2D,
@ -12,6 +13,11 @@ export const renderSimulation = (
) => {
clearCanvas(ctx)
// push current context if it changed
if (currentContext.value !== ctx) {
currentContext.next(ctx)
}
const transform = renderer.camera.transform
ctx.translate(...transform.position)

View file

@ -1,6 +1,6 @@
import { clamp } from '../../simulation/helpers/clamp'
import { Camera } from '../classes/Camera'
import { vector2 } from '../../../common/math/classes/Transform'
import { vector2 } from '../../../common/math/types/vector2'
import { multiply, substract } from '../../vector2/helpers/basic'
import { repeat } from '../../vector2/helpers/repeat'

View file

@ -1,5 +1,8 @@
import { SimulationRenderer } from '../classes/SimulationRenderer'
/**
* Gets the stroke width for wires
*/
export const wireRadius = (renderer: SimulationRenderer) => {
return (
2 *

View file

@ -0,0 +1,8 @@
import { BehaviorSubject } from 'rxjs'
/**
* Behavior subject holding the curentCnvasRenderingContext2d
*/
export const currentContext = new BehaviorSubject<null | CanvasRenderingContext2D>(
null
)

View file

@ -1,4 +1,4 @@
import { vector2 } from '../../../common/math/classes/Transform'
import { vector2 } from '../../../common/math/types/vector2'
/**
* Used to init a gate at a certain position