diff --git a/src/assets/full-adder.svg b/src/assets/full-adder.svg new file mode 100644 index 0000000..8e89b8f --- /dev/null +++ b/src/assets/full-adder.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/half-adder.svg b/src/assets/half-adder.svg new file mode 100644 index 0000000..2f1e82e --- /dev/null +++ b/src/assets/half-adder.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/common/math/classes/Transform.ts b/src/common/math/classes/Transform.ts index c501983..33b7714 100644 --- a/src/common/math/classes/Transform.ts +++ b/src/common/math/classes/Transform.ts @@ -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([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 = [ diff --git a/src/modules/activation/types/Context.ts b/src/modules/activation/types/Context.ts index 1283da2..850651d 100644 --- a/src/modules/activation/types/Context.ts +++ b/src/modules/activation/types/Context.ts @@ -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 diff --git a/src/modules/core/components/FluidCanvas.tsx b/src/modules/core/components/FluidCanvas.tsx index 7a142a5..23fec82 100644 --- a/src/modules/core/components/FluidCanvas.tsx +++ b/src/modules/core/components/FluidCanvas.tsx @@ -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 @@ -11,15 +11,6 @@ export interface FluidCanvasProps { mouseMoveOutput: Subject } -export const getEventInfo = ( - e: MouseEvent -): MouseEventInfo => { - return { - button: e.button as mouseButton, - position: [e.clientX, e.clientY] - } -} - export const mouseEventHandler = (output: Subject) => ( e: MouseEvent ) => { diff --git a/src/modules/core/helpers/getEventInfo.ts b/src/modules/core/helpers/getEventInfo.ts new file mode 100644 index 0000000..72a29e4 --- /dev/null +++ b/src/modules/core/helpers/getEventInfo.ts @@ -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 +): MouseEventInfo => { + return { + button: e.button as mouseButton, + position: [e.clientX, e.clientY] + } +} diff --git a/src/modules/core/styles/global-styles/global-styles.scss b/src/modules/core/styles/global-styles/global-styles.scss index 578c293..0231e7f 100644 --- a/src/modules/core/styles/global-styles/global-styles.scss +++ b/src/modules/core/styles/global-styles/global-styles.scss @@ -1,2 +1,6 @@ @import './mui-overrides.scss'; @import './toasts.scss'; + +* { + user-select: none; +} diff --git a/src/modules/core/components/MouseEventInfo.tsx b/src/modules/core/types/MouseEventInfo.ts similarity index 80% rename from src/modules/core/components/MouseEventInfo.tsx rename to src/modules/core/types/MouseEventInfo.ts index 07fbcb8..6296342 100644 --- a/src/modules/core/components/MouseEventInfo.tsx +++ b/src/modules/core/types/MouseEventInfo.ts @@ -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 diff --git a/src/modules/core/types/MouseSubject.ts b/src/modules/core/types/MouseSubject.ts index 07b79ee..0191703 100644 --- a/src/modules/core/types/MouseSubject.ts +++ b/src/modules/core/types/MouseSubject.ts @@ -1,4 +1,4 @@ import { Subject } from 'rxjs' -import { MouseEventInfo } from '../components/MouseEventInfo' +import { MouseEventInfo } from './MouseEventInfo' export type MouseSubject = Subject diff --git a/src/modules/logic-gates/components/GatePropertiesModal.tsx b/src/modules/logic-gates/components/GatePropertiesModal.tsx index cc250a4..1a167d0 100644 --- a/src/modules/logic-gates/components/GatePropertiesModal.tsx +++ b/src/modules/logic-gates/components/GatePropertiesModal.tsx @@ -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 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 = ( ) } else if (raw.type === 'boolean') { @@ -64,7 +64,7 @@ export const GatePropery = ({ name, raw, output, gate }: GatePropertyProps) => { {displayableName} { - 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 (
{ {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 ( ) })} diff --git a/src/modules/logic-gates/subjects/LogicGatePropsSubjects.ts b/src/modules/logic-gates/subjects/LogicGatePropsSubjects.ts index a75af4a..b822ea3 100644 --- a/src/modules/logic-gates/subjects/LogicGatePropsSubjects.ts +++ b/src/modules/logic-gates/subjects/LogicGatePropsSubjects.ts @@ -1,4 +1,4 @@ import { BehaviorSubject, Subject } from 'rxjs' -export const open = new Subject() +export const open = new BehaviorSubject(false) export const id = new BehaviorSubject(0) diff --git a/src/modules/modals/helpers/modalIsOpen.ts b/src/modules/modals/helpers/modalIsOpen.ts index 10a6d23..0c17e97 100644 --- a/src/modules/modals/helpers/modalIsOpen.ts +++ b/src/modules/modals/helpers/modalIsOpen.ts @@ -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 + ) } diff --git a/src/modules/saving/constants.ts b/src/modules/saving/constants.ts index 92c452f..3758345 100644 --- a/src/modules/saving/constants.ts +++ b/src/modules/saving/constants.ts @@ -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[] = [ @@ -26,7 +28,10 @@ export const baseTemplates: DeepPartial[] = [ rgbLightTemplate, sequentialDelayerTemplate, xnorTemplate, - xorTemplate + xorTemplate, + halfAdderTemplate, + fullAdderTemplate + // commentTemplate ] export const baseSave: RendererState = { diff --git a/src/modules/saving/helpers/getState.ts b/src/modules/saving/helpers/getState.ts index 22af919..30b02d1 100644 --- a/src/modules/saving/helpers/getState.ts +++ b/src/modules/saving/helpers/getState.ts @@ -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() } } diff --git a/src/modules/saving/templates/comment.ts b/src/modules/saving/templates/comment.ts new file mode 100644 index 0000000..48decbd --- /dev/null +++ b/src/modules/saving/templates/comment.ts @@ -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 diff --git a/src/modules/saving/templates/fullAdder.ts b/src/modules/saving/templates/fullAdder.ts new file mode 100644 index 0000000..2c1099e --- /dev/null +++ b/src/modules/saving/templates/fullAdder.ts @@ -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 diff --git a/src/modules/saving/templates/halfAdder.ts b/src/modules/saving/templates/halfAdder.ts new file mode 100644 index 0000000..6a3ccd8 --- /dev/null +++ b/src/modules/saving/templates/halfAdder.ts @@ -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 diff --git a/src/modules/saving/types/SimulationSave.ts b/src/modules/saving/types/SimulationSave.ts index 1d58e7a..d607667 100644 --- a/src/modules/saving/types/SimulationSave.ts +++ b/src/modules/saving/types/SimulationSave.ts @@ -1,4 +1,4 @@ -import { vector2 } from '../../../common/math/classes/Transform' +import { vector2 } from '../../../common/math/types/vector2' export type simulationMode = 'ic' | 'project' diff --git a/src/modules/simulation/classes/Gate.ts b/src/modules/simulation/classes/Gate.ts index fe2b284..3c9455c 100644 --- a/src/modules/simulation/classes/Gate.ts +++ b/src/modules/simulation/classes/Gate.ts @@ -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 = {} + public props: Record< + string, + BehaviorSubject + > = {} /** * The main logic gate class @@ -129,7 +140,7 @@ export class Gate { public constructor( template: DeepPartial = {}, id?: number, - props: Record = {} + props: Record = {} ) { 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) { + private assignProps(props: Record) { 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 = {} + + 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 diff --git a/src/modules/simulation/classes/Simulation.ts b/src/modules/simulation/classes/Simulation.ts index 90a9550..bd022d1 100644 --- a/src/modules/simulation/classes/Simulation.ts +++ b/src/modules/simulation/classes/Simulation.ts @@ -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 diff --git a/src/modules/simulation/constants.ts b/src/modules/simulation/constants.ts index 77b66be..fd73458 100644 --- a/src/modules/simulation/constants.ts +++ b/src/modules/simulation/constants.ts @@ -53,5 +53,9 @@ export const DefaultGateTemplate: GateTemplate = { properties: { enabled: false, data: [] + }, + innerText: { + enabled: false, + color: 'white' } } diff --git a/src/modules/simulation/helpers/addGate.ts b/src/modules/simulation/helpers/addGate.ts index 775a041..610fbff 100644 --- a/src/modules/simulation/helpers/addGate.ts +++ b/src/modules/simulation/helpers/addGate.ts @@ -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' diff --git a/src/modules/simulation/types/GateTemplate.ts b/src/modules/simulation/types/GateTemplate.ts index ab089da..dfe8fa6 100644 --- a/src/modules/simulation/types/GateTemplate.ts +++ b/src/modules/simulation/types/GateTemplate.ts @@ -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 + } } diff --git a/src/modules/simulationRenderer/classes/Camera.ts b/src/modules/simulationRenderer/classes/Camera.ts index 05c07bd..d4c028a 100644 --- a/src/modules/simulationRenderer/classes/Camera.ts +++ b/src/modules/simulationRenderer/classes/Camera.ts @@ -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 + ) } } diff --git a/src/modules/simulationRenderer/classes/SimulationRenderer.ts b/src/modules/simulationRenderer/classes/SimulationRenderer.ts index 332b95c..ef88e6d 100644 --- a/src/modules/simulationRenderer/classes/SimulationRenderer.ts +++ b/src/modules/simulationRenderer/classes/SimulationRenderer.ts @@ -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() @@ -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) diff --git a/src/modules/simulationRenderer/constants.ts b/src/modules/simulationRenderer/constants.ts index 342c716..d283aed 100644 --- a/src/modules/simulationRenderer/constants.ts +++ b/src/modules/simulationRenderer/constants.ts @@ -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' diff --git a/src/modules/simulationRenderer/helpers/getCurrentContextSafely.ts b/src/modules/simulationRenderer/helpers/getCurrentContextSafely.ts new file mode 100644 index 0000000..29ed5cd --- /dev/null +++ b/src/modules/simulationRenderer/helpers/getCurrentContextSafely.ts @@ -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`) + } +} diff --git a/src/modules/simulationRenderer/helpers/handleMouseDown.ts b/src/modules/simulationRenderer/helpers/handleMouseDown.ts new file mode 100644 index 0000000..eb5281a --- /dev/null +++ b/src/modules/simulationRenderer/helpers/handleMouseDown.ts @@ -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 + } +} diff --git a/src/modules/simulationRenderer/helpers/handleMouseMove.ts b/src/modules/simulationRenderer/helpers/handleMouseMove.ts new file mode 100644 index 0000000..c40f888 --- /dev/null +++ b/src/modules/simulationRenderer/helpers/handleMouseMove.ts @@ -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) +} diff --git a/src/modules/simulationRenderer/helpers/handleMouseUp.ts b/src/modules/simulationRenderer/helpers/handleMouseUp.ts new file mode 100644 index 0000000..4b9c75b --- /dev/null +++ b/src/modules/simulationRenderer/helpers/handleMouseUp.ts @@ -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}` + ) + } + } + } +} diff --git a/src/modules/simulationRenderer/helpers/openGateProps.ts b/src/modules/simulationRenderer/helpers/openGateProps.ts new file mode 100644 index 0000000..56a663f --- /dev/null +++ b/src/modules/simulationRenderer/helpers/openGateProps.ts @@ -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) +} diff --git a/src/modules/simulationRenderer/helpers/projectPoint.ts b/src/modules/simulationRenderer/helpers/projectPoint.ts deleted file mode 100644 index cbe1f70..0000000 --- a/src/modules/simulationRenderer/helpers/projectPoint.ts +++ /dev/null @@ -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 diff --git a/src/modules/simulationRenderer/helpers/renderSimulation.ts b/src/modules/simulationRenderer/helpers/renderSimulation.ts index e55e60e..379e570 100644 --- a/src/modules/simulationRenderer/helpers/renderSimulation.ts +++ b/src/modules/simulationRenderer/helpers/renderSimulation.ts @@ -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) diff --git a/src/modules/simulationRenderer/helpers/scaleCanvas.ts b/src/modules/simulationRenderer/helpers/scaleCanvas.ts index 902e21b..663e709 100644 --- a/src/modules/simulationRenderer/helpers/scaleCanvas.ts +++ b/src/modules/simulationRenderer/helpers/scaleCanvas.ts @@ -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' diff --git a/src/modules/simulationRenderer/helpers/wireRadius.ts b/src/modules/simulationRenderer/helpers/wireRadius.ts index cbaa565..3a1a9be 100644 --- a/src/modules/simulationRenderer/helpers/wireRadius.ts +++ b/src/modules/simulationRenderer/helpers/wireRadius.ts @@ -1,5 +1,8 @@ import { SimulationRenderer } from '../classes/SimulationRenderer' +/** + * Gets the stroke width for wires + */ export const wireRadius = (renderer: SimulationRenderer) => { return ( 2 * diff --git a/src/modules/simulationRenderer/subjects.ts b/src/modules/simulationRenderer/subjects.ts new file mode 100644 index 0000000..2ca67bc --- /dev/null +++ b/src/modules/simulationRenderer/subjects.ts @@ -0,0 +1,8 @@ +import { BehaviorSubject } from 'rxjs' + +/** + * Behavior subject holding the curentCnvasRenderingContext2d + */ +export const currentContext = new BehaviorSubject( + null +) diff --git a/src/modules/simulationRenderer/types/GateInitter.ts b/src/modules/simulationRenderer/types/GateInitter.ts index c58e888..34e262e 100644 --- a/src/modules/simulationRenderer/types/GateInitter.ts +++ b/src/modules/simulationRenderer/types/GateInitter.ts @@ -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