diff --git a/deploy.ts b/deploy.ts index 2dd1290..fefa040 100644 --- a/deploy.ts +++ b/deploy.ts @@ -1,8 +1,6 @@ const { publish } = require('gh-pages') const { exec } = require('child_process') -// const { publish } = require("gh-pages") - const args = process.argv.splice(2) const mFlag = (args.indexOf('--message') + 1 || args.indexOf('-m') + 1) - 1 diff --git a/src/common/lang/arrays/helpers/setToArray.ts b/src/common/lang/arrays/helpers/setToArray.ts new file mode 100644 index 0000000..53b0cd4 --- /dev/null +++ b/src/common/lang/arrays/helpers/setToArray.ts @@ -0,0 +1,6 @@ +/** + * Transforms a set into an array + * + * @param set The set to convert + */ +export const setToArray = (set: Set) => Array.from(set.values()) diff --git a/src/modules/core/components/Canvas.tsx b/src/modules/core/components/Canvas.tsx index a94ddfa..90ec616 100644 --- a/src/modules/core/components/Canvas.tsx +++ b/src/modules/core/components/Canvas.tsx @@ -3,7 +3,6 @@ import FluidCanvas from './FluidCanvas' import loop from 'mainloop.js' import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' import { renderSimulation } from '../../simulationRenderer/helpers/renderSimulation' -import { updateSimulation } from '../../simulationRenderer/helpers/updateSimulation' import { rendererSubject } from '../subjects/rendererSubject' import { loadSubject } from '../subjects/loadedSubject' @@ -21,7 +20,7 @@ class Canvas extends Component { if (this.renderingContext) { renderSimulation(this.renderingContext, this.renderer) } - }).setUpdate(delta => updateSimulation(this.renderer, delta)) + }) } public componentDidMount() { diff --git a/src/modules/core/components/FluidCanvas.tsx b/src/modules/core/components/FluidCanvas.tsx index ad18cee..589c75f 100644 --- a/src/modules/core/components/FluidCanvas.tsx +++ b/src/modules/core/components/FluidCanvas.tsx @@ -2,15 +2,11 @@ import React, { RefObject, forwardRef, MouseEvent, WheelEvent } from 'react' import { useObservable } from 'rxjs-hooks' import { Screen } from '../classes/Screen' import { Subject } from 'rxjs' -import { vector2 } from '../../../common/math/types/vector2' +import { mouseButton } from '../types/mouseButton' +import { MouseEventInfo } from './MouseEventInfo' const screen = new Screen() -export interface MouseEventInfo { - position: vector2 - button: number -} - export interface FluidCanvasProps { mouseDownOuput: Subject mouseUpOutput: Subject @@ -21,7 +17,7 @@ export const getEventInfo = ( e: MouseEvent ): MouseEventInfo => { return { - button: e.button, + button: e.button as mouseButton, position: [e.clientX, e.clientY] } } diff --git a/src/modules/core/components/MouseEventInfo.tsx b/src/modules/core/components/MouseEventInfo.tsx new file mode 100644 index 0000000..07fbcb8 --- /dev/null +++ b/src/modules/core/components/MouseEventInfo.tsx @@ -0,0 +1,10 @@ +import { vector2 } from '../../../common/math/types/vector2' +import { mouseButton } from '../types/mouseButton' + +/** + * The info about the mouse passed to mouse subjects + */ +export interface MouseEventInfo { + position: vector2 + button: mouseButton +} diff --git a/src/modules/core/components/Sidebar.tsx b/src/modules/core/components/Sidebar.tsx index 3fee73e..a917fcc 100644 --- a/src/modules/core/components/Sidebar.tsx +++ b/src/modules/core/components/Sidebar.tsx @@ -6,6 +6,7 @@ import CreateSimulationButton from './CreateSimulationButton' import LogicGates from './LogicGates' import { makeStyles, createStyles } from '@material-ui/core/styles' import Language from './Language' +import SimulationActions from '../../simulation-actions/components/SimulationActions' /** * The width of the sidebar */ @@ -70,6 +71,7 @@ const Sidebar = () => { + diff --git a/src/modules/core/types/MouseSubject.ts b/src/modules/core/types/MouseSubject.ts index 51f49c8..07b79ee 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/FluidCanvas' +import { MouseEventInfo } from '../components/MouseEventInfo' export type MouseSubject = Subject diff --git a/src/modules/core/types/mouseButton.ts b/src/modules/core/types/mouseButton.ts new file mode 100644 index 0000000..15a37b1 --- /dev/null +++ b/src/modules/core/types/mouseButton.ts @@ -0,0 +1,4 @@ +/** + * All possibl values for the mouse button + */ +export type mouseButton = 0 | 1 | 2 diff --git a/src/modules/internalisation/translations/english.ts b/src/modules/internalisation/translations/english.ts index c1e332d..a8574a0 100644 --- a/src/modules/internalisation/translations/english.ts +++ b/src/modules/internalisation/translations/english.ts @@ -9,6 +9,7 @@ export const EnglishTranslation: Translation = { createSimulation: 'Create simulation', logicGates: 'Logic gates', openSimulation: 'Open simulations', + simulation: 'Simulation', language: 'Language' }, createSimulation: { @@ -23,11 +24,22 @@ export const EnglishTranslation: Translation = { question: 'What do you want your simulation to be called?' } }, + actions: { + save: 'Save', + clean: 'Clean', + clear: 'Clear', + refresh: 'Refresh', + undo: 'Undo' + }, messages: { createdSimulation: name => `Succesfully created simulation '${name}'`, switchedToSimulation: name => `Succesfully switched to simulation '${name}'`, savedSimulation: name => `Succesfully saved simulation '${name}'`, - compiledIc: name => `Succesfully compiled circuit '${name}'` + compiledIc: name => `Succesfully compiled circuit '${name}'`, + cleaned: name => `Succesfully cleaned simulation '${name}'`, + cleared: name => `Succesfully cleared simulation '${name}'`, + refreshed: name => `Succesfully refreshed simulation '${name}'`, + undone: name => `Succesfully undone simulation '${name}'` } } diff --git a/src/modules/internalisation/translations/nederlands.ts b/src/modules/internalisation/translations/nederlands.ts index 4d2bd77..9981bfa 100644 --- a/src/modules/internalisation/translations/nederlands.ts +++ b/src/modules/internalisation/translations/nederlands.ts @@ -9,6 +9,7 @@ export const DutchTranslation: Translation = { createSimulation: 'Maak simulatie', logicGates: 'Logische poorten', openSimulation: 'Open simulatie', + simulation: 'Todo', language: 'Taal' }, createSimulation: { diff --git a/src/modules/internalisation/translations/romanian.ts b/src/modules/internalisation/translations/romanian.ts index c2b4449..5d33cc0 100644 --- a/src/modules/internalisation/translations/romanian.ts +++ b/src/modules/internalisation/translations/romanian.ts @@ -9,6 +9,7 @@ export const RomanianTranslation: Translation = { createSimulation: 'Creează o simulație', openSimulation: 'Deschide o simulație', logicGates: 'Porți logice', + simulation: 'Simulație', language: 'Limba' }, createSimulation: { @@ -23,6 +24,9 @@ export const RomanianTranslation: Translation = { question: 'Cum vrei să numești simulația?' } }, + actions: { + save: 'Salvează' + }, messages: { createdSimulation: name => `Simulația '${name}' a fost creeată cu succes`, diff --git a/src/modules/internalisation/types/TranslationInterface.ts b/src/modules/internalisation/types/TranslationInterface.ts index fcba812..9fb95d8 100644 --- a/src/modules/internalisation/types/TranslationInterface.ts +++ b/src/modules/internalisation/types/TranslationInterface.ts @@ -1,5 +1,6 @@ import { supportedLanguage } from './supportedLanguages' import { simulationMode } from '../../saving/types/SimulationSave' +import { possibleAction } from '../../simulation-actions/types/possibleAction' export type SentenceFactory = (...names: T) => string export type NameSentence = SentenceFactory<[string]> @@ -13,6 +14,7 @@ export interface Translation { createSimulation: string openSimulation: string logicGates: string + simulation: string language: string } createSimulation: { @@ -29,5 +31,10 @@ export interface Translation { switchedToSimulation: NameSentence savedSimulation: NameSentence compiledIc: NameSentence + refreshed: NameSentence + cleared: NameSentence + cleaned: NameSentence + undone: NameSentence } + actions: Record } diff --git a/src/modules/keybindings/constants.ts b/src/modules/keybindings/constants.ts deleted file mode 100644 index 500ed41..0000000 --- a/src/modules/keybindings/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { KeyBindingMap } from './types/KeyBindingMap' -import { save } from '../saving/helpers/save' - -export const keyBindings: KeyBindingMap = [] diff --git a/src/modules/keybindings/helpers/initialiseKeyBindings.ts b/src/modules/keybindings/helpers/initialiseKeyBindings.ts index 85c2e18..f2673e3 100644 --- a/src/modules/keybindings/helpers/initialiseKeyBindings.ts +++ b/src/modules/keybindings/helpers/initialiseKeyBindings.ts @@ -1,9 +1,22 @@ -import { keyBindings } from '../constants' import { KeyboardInput } from '../classes/KeyboardInput' -import { KeyBindingMap } from '../types/KeyBindingMap' +import { KeyBindingMap, KeyBinding } from '../types/KeyBindingMap' +import { SidebarActions } from '../../simulation-actions/constants' +import { SidebarAction } from '../../simulation-actions/types/SidebarAction' +import { modalIsOpen } from '../../modals/helpers/modalIsOpen' export const listeners: Record = {} +const keyBindings = Object.values(SidebarActions) + .filter(action => action.keybinding) + .map( + (action): KeyBinding => { + return { + actions: [action.action], + keys: action.keybinding || [] + } + } + ) + export const initKeyBindings = (bindings: KeyBindingMap = keyBindings) => { const allKeys = new Set() @@ -18,26 +31,54 @@ export const initKeyBindings = (bindings: KeyBindingMap = keyBindings) => { } window.addEventListener('keydown', e => { - for (const keyBinding of bindings) { - let done = false + if (!modalIsOpen()) { + const current: { + keys: string[] + callback: Function + }[] = [] - for (const key of keyBinding.keys) { - if (!(done || listeners[key].value)) { - done = true - break + for (const keyBinding of bindings) { + let done = false + + for (const key of keyBinding.keys) { + if (!(done || listeners[key].value)) { + done = true + break + } } + + if (done) { + continue + } + + current.push({ + keys: keyBinding.keys, + callback: () => { + for (const action of keyBinding.actions) { + action() + } + } + }) } - if (done) { - continue - } + if (current.length) { + let maxIndex = 0 + let max = current[0].keys.length - for (const action of keyBinding.actions) { - action() - } + for (let index = 1; index < current.length; index++) { + const element = current[index] - e.preventDefault() - e.stopPropagation() + if (element.keys.length > max) { + max = element.keys.length + maxIndex = index + } + } + + current[maxIndex].callback() + + e.preventDefault() + e.stopPropagation() + } } }) } diff --git a/src/modules/saving/helpers/dumpSimulation.ts b/src/modules/saving/helpers/dumpSimulation.ts index 4100086..7156681 100644 --- a/src/modules/saving/helpers/dumpSimulation.ts +++ b/src/modules/saving/helpers/dumpSimulation.ts @@ -8,7 +8,7 @@ import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationR export const dumpSimulation = (renderer: SimulationRenderer) => { renderer.simulation.dispose() renderer.lastMousePosition = [0, 0] - renderer.selectedGate = null + renderer.selectedGates = new Set() renderer.selectedPins = { end: null, start: null diff --git a/src/modules/simulation-actions/components/SimulationActions.tsx b/src/modules/simulation-actions/components/SimulationActions.tsx new file mode 100644 index 0000000..8366915 --- /dev/null +++ b/src/modules/simulation-actions/components/SimulationActions.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react' +import ListItem from '@material-ui/core/ListItem' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import ListItemText from '@material-ui/core/ListItemText' +import Menu from '@material-ui/core/Menu' +import MenuItem from '@material-ui/core/MenuItem' +import Icon from '@material-ui/core/Icon' +import { useTranslation } from '../../internalisation/helpers/useLanguage' +import { SidebarActions } from '../constants' + +/** + * Component wich contains the sidebar 'Simulation' button + */ +const SimulationActions = () => { + const [anchorEl, setAnchorEl] = useState(null) + + const translation = useTranslation() + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + <> + { + setAnchorEl(event.currentTarget) + }} + > + + insert_drive_file + + {translation.sidebar.simulation} + + + + {Object.values(SidebarActions).map( + ({ icon, name, keybinding, action }, index) => { + return ( + + + {icon} + + + + + ) + } + )} + + + ) +} + +export default SimulationActions diff --git a/src/modules/simulation-actions/constants.ts b/src/modules/simulation-actions/constants.ts new file mode 100644 index 0000000..cf0d5d9 --- /dev/null +++ b/src/modules/simulation-actions/constants.ts @@ -0,0 +1,52 @@ +import { SidebarAction } from './types/SidebarAction' +import { possibleAction } from './types/possibleAction' +import { save } from '../saving/helpers/save' +import { refresh } from './helpers/refresh' +import { undo } from './helpers/undo' +import { createActionConfig } from './helpers/createActionConfig' +import { clear } from './helpers/clear' + +export const actionIcons: Record = { + clean: 'layers_clear', + clear: 'clear', + refresh: 'refresh', + save: 'save', + undo: 'undo' +} + +/** + * Array with all the actions for the SimulationAction component to render + */ +export const SidebarActions: Record = { + ...createActionConfig('save', save, ['ctrl', 's']), + ...createActionConfig( + 'refresh', + { + run: refresh + }, + ['ctrl', 'r'] + ), + ...createActionConfig( + 'undo', + { + run: undo + }, + ['ctrl', 'z'] + ), + ...createActionConfig( + 'clear', + { + run: clear + }, + ['ctrl', 'delete'] + ), + ...createActionConfig( + 'clean', + { + run: () => { + console.log('Cleaning') + } + }, + ['ctrl', 'shift', 'delete'] + ) +} diff --git a/src/modules/simulation-actions/helpers/clear.ts b/src/modules/simulation-actions/helpers/clear.ts new file mode 100644 index 0000000..a50e199 --- /dev/null +++ b/src/modules/simulation-actions/helpers/clear.ts @@ -0,0 +1,12 @@ +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' + +/** + * Clears the simuolation of a renderer + * + * @param renderer The renderer to clear the simulation of + */ +export const clear = (renderer: SimulationRenderer) => { + renderer.simulation.dispose() + renderer.simulation.wires = [] + renderer.simulation.gates.clear() +} diff --git a/src/modules/simulation-actions/helpers/createActionConfig.ts b/src/modules/simulation-actions/helpers/createActionConfig.ts new file mode 100644 index 0000000..91d6bd6 --- /dev/null +++ b/src/modules/simulation-actions/helpers/createActionConfig.ts @@ -0,0 +1,44 @@ +import { possibleAction } from '../types/possibleAction' +import { SidebarAction } from '../types/SidebarAction' +import { actionIcons } from '../constants' +import { createRendererAction } from './createRendererActions' +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' +import { getRendererSafely } from '../../logic-gates/helpers/getRendererSafely' + +export type ActionConfigFunction = (renderer?: SimulationRenderer) => void + +export type ActionConfigCallback = + | { + renderer?: boolean + run: ActionConfigFunction + } + | ActionConfigFunction + +export const createActionConfig = ( + name: T, + callback: ActionConfigCallback, + keybinding: string[] = [] +): Record => { + let action: ActionConfigFunction + + if (callback instanceof Function) { + action = callback + } else { + if (callback.renderer !== false) { + action = createRendererAction(name, callback.run) + } else { + action = callback.run + } + } + + return { + [name]: { + name, + action: () => { + action(getRendererSafely()) + }, + icon: actionIcons[name], + keybinding + } + } +} diff --git a/src/modules/simulation-actions/helpers/createRendererActions.ts b/src/modules/simulation-actions/helpers/createRendererActions.ts new file mode 100644 index 0000000..4e95340 --- /dev/null +++ b/src/modules/simulation-actions/helpers/createRendererActions.ts @@ -0,0 +1,41 @@ +import { possibleAction } from '../types/possibleAction' +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' +import { CurrentLanguage } from '../../internalisation/stores/currentLanguage' +import { getRendererSafely } from '../../logic-gates/helpers/getRendererSafely' +import { createToastArguments } from '../../toasts/helpers/createToastArguments' +import { actionIcons } from '../constants' +import { toast } from 'react-toastify' +import { Translation } from '../../internalisation/types/TranslationInterface' + +/** + * Map used to get the correct message from any action name + */ +export const actionToMessageMap: Record< + possibleAction, + keyof Translation['messages'] +> = { + clean: 'cleaned', + clear: 'cleared', + refresh: 'refreshed', + undo: 'undone', + save: 'savedSimulation' +} + +export const createRendererAction = ( + action: possibleAction, + callback: (renderer: SimulationRenderer) => void +) => () => { + const translation = CurrentLanguage.getTranslation() + const renderer = getRendererSafely() + + callback(renderer) + + toast( + ...createToastArguments( + translation.messages[actionToMessageMap[action]]( + renderer.simulation.name + ), + actionIcons[action] + ) + ) +} diff --git a/src/modules/simulation-actions/helpers/refresh.ts b/src/modules/simulation-actions/helpers/refresh.ts new file mode 100644 index 0000000..0fcf20d --- /dev/null +++ b/src/modules/simulation-actions/helpers/refresh.ts @@ -0,0 +1,11 @@ +import { getRendererState } from '../../saving/helpers/getState' +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' + +/** + * Refreshes a simulations + * + * @param renderer - the renderer to refresh + */ +export const refresh = (renderer: SimulationRenderer) => { + renderer.loadSave(getRendererState(renderer)) +} diff --git a/src/modules/simulation-actions/helpers/undo.ts b/src/modules/simulation-actions/helpers/undo.ts new file mode 100644 index 0000000..8602b6d --- /dev/null +++ b/src/modules/simulation-actions/helpers/undo.ts @@ -0,0 +1,10 @@ +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' + +/** + * Undoes a simulation + * + * @param renderer - the renderer to undo the simulation of + */ +export const undo = (renderer: SimulationRenderer) => { + renderer.reloadSave() +} diff --git a/src/modules/simulation-actions/types/SidebarAction.ts b/src/modules/simulation-actions/types/SidebarAction.ts new file mode 100644 index 0000000..236cb41 --- /dev/null +++ b/src/modules/simulation-actions/types/SidebarAction.ts @@ -0,0 +1,9 @@ +/** + * The interface for all the actions wvound to the Simulation button on the sidebar + */ +export interface SidebarAction { + name: string + icon: string + keybinding?: string[] + action: () => void +} diff --git a/src/modules/simulation-actions/types/possibleAction.ts b/src/modules/simulation-actions/types/possibleAction.ts new file mode 100644 index 0000000..7efe140 --- /dev/null +++ b/src/modules/simulation-actions/types/possibleAction.ts @@ -0,0 +1,4 @@ +/** + * Type repressenting al lpossible actions + */ +export type possibleAction = 'save' | 'clear' | 'clean' | 'refresh' | 'undo' diff --git a/src/modules/simulation/classes/GateStorage.ts b/src/modules/simulation/classes/GateStorage.ts index 795d543..da64687 100644 --- a/src/modules/simulation/classes/GateStorage.ts +++ b/src/modules/simulation/classes/GateStorage.ts @@ -1,18 +1,48 @@ import { LruCacheNode } from '@eix-js/utils' import { Gate } from './Gate' +/** + * The nodes used inside the gat storage + */ export type GateNode = LruCacheNode +/** + * dequeue implementation with iteration support. + * Used to store the logic gates + */ export class GateStorage { + /** + * The map containing all the hash - node pairs + */ private hashMap = new Map() + + /** + * The head of the dequeue + */ private head = new LruCacheNode(0, null) + + /** + * The tail of the dequue + */ private tail = new LruCacheNode(0, null) public constructor() { + this.init() + } + + /** + * Links the head to the tail + */ + private init() { this.head.next = this.tail this.tail.previous = this.head } + /** + * Deletes a gate + * + * @param node The node to delete + */ public delete(node: GateNode) { node.previous.next = node.next node.next.previous = node.previous @@ -20,11 +50,22 @@ export class GateStorage { return this } + /** + * Adds a new hash - gate pair to the dequeue + * + * @param key The ket of the gate + * @param node The gate itself + */ public set(key: number, node: GateNode) { this.hashMap.set(key, node) this.addToHead(node) } + /** + * Adds a node directly after head + * + * @param node The node to add + */ public addToHead(node: GateNode) { node.next = this.head.next node.next.previous = node @@ -34,24 +75,40 @@ export class GateStorage { return this } + /** + * Moves a node directly after the head + * + * @param node The node to move + */ public moveOnTop(node: GateNode) { this.delete(node).addToHead(node) return this } + /** + * Gets a gate by its key + * + * @param key The key of the gate + */ public get(key: number) { const node = this.hashMap.get(key) return node } + /** + * Gets the first gate in the dequeue + */ public first() { const first = this.head.next - return + return first } + /** + * Used for iterating over the dequeue + */ public *[Symbol.iterator](): Iterator { let last = this.tail @@ -67,4 +124,13 @@ export class GateStorage { } } } + + /** + * Deletes every gate in the dequeue + */ + public clear() { + this.hashMap = new Map() + + this.init() + } } diff --git a/src/modules/simulationRenderer/classes/MouseVelocityManager.ts b/src/modules/simulationRenderer/classes/MouseVelocityManager.ts deleted file mode 100644 index 145719c..0000000 --- a/src/modules/simulationRenderer/classes/MouseVelocityManager.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Singleton } from '@eix-js/utils' -import { MouseSubject } from '../../core/types/MouseSubject' -import { clamp } from '../../simulation/helpers/clamp' - -@Singleton -export class MouseVelocityManager { - private history: number[] = [] - private total = 0 - private limit = 10 - private lastPosition = 0 - private lastDirection = 0 - private minimumDifference = 10 - - private lastMove = performance.now() - private resetLimit = 500 - - // mouseMoveInput is optional because we want to be able to get - // the instance even if we don't have a subject - // (as long as we passed one the first time) - public constructor(public mouseMoveInput?: MouseSubject) { - if (this.mouseMoveInput) { - this.mouseMoveInput.subscribe(event => { - this.lastMove = performance.now() - - const position = event.position[0] - const dx = position - this.lastPosition - - if (Math.abs(dx) < this.minimumDifference) { - this.lastPosition = position - return - } - - if (dx === 0) { - this.lastDirection = 0 - } else { - this.lastDirection = Math.abs(dx) / dx - } - - this.lastPosition = event.position[0] - }) - } else { - throw new Error( - 'You need to pass a MouseMoveInput the first time you instantiate this class!' - ) - } - } - - public getDirection() { - if (this.history.length) { - return clamp(-1, 1, this.total / this.history.length) - } - return 0 - } - - public update(maybeAgain = true) { - if (this.lastDirection === 0) { - return 0 - } - - if ( - this.lastDirection !== 0 && - performance.now() - this.lastMove > this.resetLimit - ) { - this.lastDirection = 0 - } - - this.history.push(this.lastDirection) - - this.total += this.lastDirection - - if (this.history.length > this.limit) { - const removed = this.history.shift() - - if (removed !== undefined) { - this.total -= removed - } - } - } - - public clear(value?: number) { - if (value) { - this.lastPosition = value - } - this.history = [] - this.total = 0 - this.lastMove = performance.now() - this.lastDirection = 0 - } -} diff --git a/src/modules/simulationRenderer/classes/SimulationRenderer.ts b/src/modules/simulationRenderer/classes/SimulationRenderer.ts index b7cfc67..268e0ef 100644 --- a/src/modules/simulationRenderer/classes/SimulationRenderer.ts +++ b/src/modules/simulationRenderer/classes/SimulationRenderer.ts @@ -1,20 +1,18 @@ import { Camera } from './Camera' import { Simulation } from '../../simulation/classes/Simulation' import { Subject } from 'rxjs' -import { MouseEventInfo } from '../../core/components/FluidCanvas' +import { MouseEventInfo } from '../../core/components/MouseEventInfo' import { pointInSquare } from '../../../common/math/helpers/pointInSquare' import { vector2 } from '../../../common/math/types/vector2' -import { MouseVelocityManager } from './MouseVelocityManager' import { Screen } from '../../core/classes/Screen' import { relativeTo, add, invert } from '../../vector2/helpers/basic' import { SimulationRendererOptions } from '../types/SimulationRendererOptions' -import { defaultSimulationRendererOptions } from '../constants' +import { defaultSimulationRendererOptions, mouseButtons } from '../constants' import { getPinPosition } from '../helpers/pinPosition' import { pointInCircle } from '../../../common/math/helpers/pointInCircle' import { SelectedPins } from '../types/SelectedPins' import { Wire } from '../../simulation/classes/Wire' import { KeyBindingMap } from '../../keybindings/types/KeyBindingMap' -import { save } from '../../saving/helpers/save' import { initKeyBindings } from '../../keybindings/helpers/initialiseKeyBindings' import { currentStore } from '../../saving/stores/currentStore' import { saveStore } from '../../saving/stores/saveStore' @@ -30,6 +28,9 @@ 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 } from '../../saving/types/SimulationSave' +import { setToArray } from '../../../common/lang/arrays/helpers/setToArray' +import { Selection } from '../types/Selection' export class SimulationRenderer { public mouseDownOutput = new Subject() @@ -37,17 +38,16 @@ export class SimulationRenderer { public mouseMoveOutput = new Subject() public wheelOutput = new Subject() - public selectedGate: number | null = null + public selectedGates = new Set() public lastMousePosition: vector2 = [0, 0] - public movedSelection = false public options: SimulationRendererOptions - public mouseManager = new MouseVelocityManager(this.mouseMoveOutput) public screen = new Screen() public camera = new Camera() // first bit = dragging - // second bit = moving around - private mouseState = 0b00 + // second bit = panning around + // third bit = selecting + private mouseState = 0b000 private gateSelectionOffset: vector2 = [0, 0] // this is used for spawning gates @@ -81,16 +81,18 @@ export class SimulationRenderer { for (let index = gates.length - 1; index >= 0; index--) { const { transform, id, pins } = gates[index] - if (pointInSquare(worldPosition, transform)) { - // run function + if ( + event.button === mouseButtons.drag && + pointInSquare(worldPosition, transform) + ) { gates[index].onClick() - this.mouseManager.clear(worldPosition[0]) - this.mouseState |= 1 - this.movedSelection = false - this.selectedGate = id + this.selectedGates.add({ + id, + permanent: false + }) this.gateSelectionOffset = worldPosition.map( (position, index) => position - transform.position[index] @@ -100,9 +102,11 @@ export class SimulationRenderer { if (gateNode) { return this.simulation.gates.moveOnTop(gateNode) + } else { + throw new SimulationError( + `Cannot find gate with id ${id}` + ) } - - return } for (const pin of pins) { @@ -145,7 +149,7 @@ export class SimulationRenderer { ) { this.selectedPins.start = null this.selectedPins.end = null - } else if ((pin.value.type & 0b10) >> 1) { + } else if ((pin.value.type & 2) >> 1) { this.selectedPins.start = { wrapper: pin, transform @@ -167,22 +171,31 @@ export class SimulationRenderer { this.selectedPins.start = null this.selectedPins.end = null } + + return } } } - this.mouseState |= 0b10 + if (event.button === mouseButtons.pan) { + this.mouseState |= 0b10 + } }) - this.mouseUpOutput.subscribe(event => { - if (this.selectedGate !== null) { + this.mouseUpOutput.subscribe(() => { + if (this.selectedGates.size) { const selected = this.getSelected() - if (selected) { - selected.transform.rotation = 0 + for (const gate of selected) { + gate.transform.rotation = 0 + } + + for (const selection of this.selectedGates.values()) { + if (!selection.permanent) { + this.selectedGates.delete(selection) + } } - this.selectedGate = null this.mouseState &= 0 } @@ -194,18 +207,12 @@ export class SimulationRenderer { const worldPosition = this.camera.toWordPostition(event.position) - if (this.mouseState & 1 && this.selectedGate !== null) { - const gate = this.getGateById(this.selectedGate) + if (this.mouseState & 1 && this.selectedGates.size) { + for (const gate of this.getSelected()) { + const { transform } = gate - if (!gate || !gate.data) return - - const transform = gate.data.transform - - transform.x = worldPosition[0] - this.gateSelectionOffset[0] - transform.y = worldPosition[1] - this.gateSelectionOffset[1] - - if (!this.movedSelection) { - this.movedSelection = true + transform.x = worldPosition[0] - this.gateSelectionOffset[0] + transform.y = worldPosition[1] - this.gateSelectionOffset[1] } } @@ -245,6 +252,11 @@ export class SimulationRenderer { } } + public loadSave(save: RendererState) { + this.simulation = fromSimulationState(save.simulation) + this.camera = fromCameraState(save.camera) + } + public reloadSave() { try { const current = currentStore.get() @@ -253,8 +265,7 @@ export class SimulationRenderer { if (!save) return if (!(save.simulation || save.camera)) return - this.simulation = fromSimulationState(save.simulation) - this.camera = fromCameraState(save.camera) + this.loadSave(save) } catch (e) { throw new Error( `An error occured while loading the save: ${ @@ -266,38 +277,28 @@ export class SimulationRenderer { private initKeyBindings() { const bindings: KeyBindingMap = [ - { - keys: ['ctrl', 's'], - actions: [() => save(this)] - }, { keys: ['delete'], actions: [ () => { - const selected = this.getSelected() + for (const gate of this.getSelected()) { + const node = this.simulation.gates.get(gate.id) - if (!selected) { - return - } + if (!node) continue - const node = this.simulation.gates.get(selected.id) - - if (!node) { - return - } - - for (const wire of this.simulation.wires) { - if (wireConnectedToGate(selected, wire)) { - wire.dispose() + for (const wire of this.simulation.wires) { + if (wireConnectedToGate(gate, wire)) { + wire.dispose() + } } + + this.simulation.wires = this.simulation.wires.filter( + wire => wire.active + ) + + gate.dispose() + this.simulation.gates.delete(node) } - - this.simulation.wires = this.simulation.wires.filter( - wire => wire.active - ) - - selected.dispose() - this.simulation.gates.delete(node) } ] } @@ -310,13 +311,25 @@ export class SimulationRenderer { return this.simulation.gates.get(id) } + /** + * Gets all selected gates in the simulation + * + * @throws SimulationError if an id isnt valid + * @throws SimulationError if the id doesnt have a data prop + */ public getSelected() { - if (this.selectedGate === null) return null + return setToArray(this.selectedGates).map(({ id }) => { + const gate = this.simulation.gates.get(id) - const gate = this.getGateById(this.selectedGate) + if (!gate) { + throw new SimulationError(`Cannot find gate with id ${id}`) + } else if (!gate.data) { + throw new SimulationError( + `Cannot find data of gate with id ${id}` + ) + } - if (!gate || !gate.data) return null - - return gate.data + return gate.data + }) } } diff --git a/src/modules/simulationRenderer/constants.ts b/src/modules/simulationRenderer/constants.ts index 783d2e0..08bb3ac 100644 --- a/src/modules/simulationRenderer/constants.ts +++ b/src/modules/simulationRenderer/constants.ts @@ -1,5 +1,6 @@ import { SimulationRendererOptions } from './types/SimulationRendererOptions' import { vector2 } from '../../common/math/classes/Transform' +import { mouseButton } from '../core/types/mouseButton' export const defaultSimulationRendererOptions: SimulationRendererOptions = { dnd: { @@ -30,3 +31,13 @@ export const defaultSimulationRendererOptions: SimulationRendererOptions = { } export const imageQuality: vector2 = [100, 100] + +export const mouseButtons: Record< + 'zoom' | 'pan' | 'drag' | 'select', + mouseButton +> = { + zoom: 1, + drag: 0, + pan: 0, + select: 2 +} diff --git a/src/modules/simulationRenderer/helpers/renderGate.ts b/src/modules/simulationRenderer/helpers/renderGate.ts index 0369246..aad12c9 100644 --- a/src/modules/simulationRenderer/helpers/renderGate.ts +++ b/src/modules/simulationRenderer/helpers/renderGate.ts @@ -13,7 +13,7 @@ export const renderGate = ( ) => { renderPins(ctx, renderer, gate) - if (renderer.selectedGate === gate.id) { + if (renderer.selectedGates.has(gate.id)) { ctx.strokeStyle = renderer.options.gates.gateStroke.active } else { ctx.strokeStyle = renderer.options.gates.gateStroke.normal diff --git a/src/modules/simulationRenderer/helpers/scaleCanvas.ts b/src/modules/simulationRenderer/helpers/scaleCanvas.ts index f6028bd..5c1e08e 100644 --- a/src/modules/simulationRenderer/helpers/scaleCanvas.ts +++ b/src/modules/simulationRenderer/helpers/scaleCanvas.ts @@ -2,7 +2,7 @@ import { Screen } from '../../core/classes/Screen' import { clamp } from '../../simulation/helpers/clamp' import { Camera } from '../classes/Camera' import { vector2 } from '../../../common/math/classes/Transform' -import { MouseEventInfo } from '../../core/components/FluidCanvas' +import { MouseEventInfo } from '../../core/components/MouseEventInfo' // import { WheelEvent } from 'react' const screen = new Screen() diff --git a/src/modules/simulationRenderer/helpers/updateSimulation.ts b/src/modules/simulationRenderer/helpers/updateSimulation.ts deleted file mode 100644 index ef5f902..0000000 --- a/src/modules/simulationRenderer/helpers/updateSimulation.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SimulationRenderer } from '../classes/SimulationRenderer' - -export const updateSimulation = ( - renderer: SimulationRenderer, - delta: number -) => { - const selected = renderer.getSelected() - - if (selected && renderer.movedSelection) { - renderer.mouseManager.update() - selected.transform.rotation = - renderer.mouseManager.getDirection() * renderer.options.dnd.rotation - } else { - if (selected) { - selected.transform.rotation = 0 - } - renderer.mouseManager.update() - } -} diff --git a/src/modules/simulationRenderer/types/Selection.ts b/src/modules/simulationRenderer/types/Selection.ts new file mode 100644 index 0000000..19e1dbc --- /dev/null +++ b/src/modules/simulationRenderer/types/Selection.ts @@ -0,0 +1,4 @@ +export interface Selection { + id: number + permanent: boolean +}