removed rotations and added the base for multi-gate selections
This commit is contained in:
parent
116c0d5e01
commit
a9034559a3
|
@ -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
|
||||
|
|
6
src/common/lang/arrays/helpers/setToArray.ts
Normal file
6
src/common/lang/arrays/helpers/setToArray.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Transforms a set into an array
|
||||
*
|
||||
* @param set The set to convert
|
||||
*/
|
||||
export const setToArray = <T>(set: Set<T>) => Array.from(set.values())
|
|
@ -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() {
|
||||
|
|
|
@ -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<MouseEventInfo>
|
||||
mouseUpOutput: Subject<MouseEventInfo>
|
||||
|
@ -21,7 +17,7 @@ export const getEventInfo = (
|
|||
e: MouseEvent<HTMLCanvasElement>
|
||||
): MouseEventInfo => {
|
||||
return {
|
||||
button: e.button,
|
||||
button: e.button as mouseButton,
|
||||
position: [e.clientX, e.clientY]
|
||||
}
|
||||
}
|
||||
|
|
10
src/modules/core/components/MouseEventInfo.tsx
Normal file
10
src/modules/core/components/MouseEventInfo.tsx
Normal file
|
@ -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
|
||||
}
|
|
@ -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 = () => {
|
|||
<CreateSimulationButton />
|
||||
<OpenSimulation />
|
||||
<LogicGates />
|
||||
<SimulationActions />
|
||||
</List>
|
||||
|
||||
<Language />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Subject } from 'rxjs'
|
||||
import { MouseEventInfo } from '../components/FluidCanvas'
|
||||
import { MouseEventInfo } from '../components/MouseEventInfo'
|
||||
|
||||
export type MouseSubject = Subject<MouseEventInfo>
|
||||
|
|
4
src/modules/core/types/mouseButton.ts
Normal file
4
src/modules/core/types/mouseButton.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* All possibl values for the mouse button
|
||||
*/
|
||||
export type mouseButton = 0 | 1 | 2
|
|
@ -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}'`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export const DutchTranslation: Translation = {
|
|||
createSimulation: 'Maak simulatie',
|
||||
logicGates: 'Logische poorten',
|
||||
openSimulation: 'Open simulatie',
|
||||
simulation: 'Todo',
|
||||
language: 'Taal'
|
||||
},
|
||||
createSimulation: {
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { supportedLanguage } from './supportedLanguages'
|
||||
import { simulationMode } from '../../saving/types/SimulationSave'
|
||||
import { possibleAction } from '../../simulation-actions/types/possibleAction'
|
||||
|
||||
export type SentenceFactory<T extends string[]> = (...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<possibleAction, string>
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import { KeyBindingMap } from './types/KeyBindingMap'
|
||||
import { save } from '../saving/helpers/save'
|
||||
|
||||
export const keyBindings: KeyBindingMap = []
|
|
@ -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<string, KeyboardInput> = {}
|
||||
|
||||
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<string>()
|
||||
|
||||
|
@ -18,6 +31,12 @@ export const initKeyBindings = (bindings: KeyBindingMap = keyBindings) => {
|
|||
}
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (!modalIsOpen()) {
|
||||
const current: {
|
||||
keys: string[]
|
||||
callback: Function
|
||||
}[] = []
|
||||
|
||||
for (const keyBinding of bindings) {
|
||||
let done = false
|
||||
|
||||
|
@ -32,12 +51,34 @@ export const initKeyBindings = (bindings: KeyBindingMap = keyBindings) => {
|
|||
continue
|
||||
}
|
||||
|
||||
current.push({
|
||||
keys: keyBinding.keys,
|
||||
callback: () => {
|
||||
for (const action of keyBinding.actions) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (current.length) {
|
||||
let maxIndex = 0
|
||||
let max = current[0].keys.length
|
||||
|
||||
for (let index = 1; index < current.length; index++) {
|
||||
const element = current[index]
|
||||
|
||||
if (element.keys.length > max) {
|
||||
max = element.keys.length
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
current[maxIndex].callback()
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 | HTMLElement>(null)
|
||||
|
||||
const translation = useTranslation()
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
button
|
||||
onClick={event => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Icon>insert_drive_file</Icon>
|
||||
</ListItemIcon>
|
||||
<ListItemText>{translation.sidebar.simulation}</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Menu
|
||||
keepMounted
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{Object.values(SidebarActions).map(
|
||||
({ icon, name, keybinding, action }, index) => {
|
||||
return (
|
||||
<MenuItem key={index} onClick={action}>
|
||||
<ListItemIcon>
|
||||
<Icon>{icon}</Icon>
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={(keybinding || []).join(' + ')}
|
||||
/>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SimulationActions
|
52
src/modules/simulation-actions/constants.ts
Normal file
52
src/modules/simulation-actions/constants.ts
Normal file
|
@ -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<possibleAction, string> = {
|
||||
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<possibleAction, SidebarAction> = {
|
||||
...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']
|
||||
)
|
||||
}
|
12
src/modules/simulation-actions/helpers/clear.ts
Normal file
12
src/modules/simulation-actions/helpers/clear.ts
Normal file
|
@ -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()
|
||||
}
|
44
src/modules/simulation-actions/helpers/createActionConfig.ts
Normal file
44
src/modules/simulation-actions/helpers/createActionConfig.ts
Normal file
|
@ -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 = <T extends possibleAction>(
|
||||
name: T,
|
||||
callback: ActionConfigCallback,
|
||||
keybinding: string[] = []
|
||||
): Record<T, SidebarAction> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
)
|
||||
)
|
||||
}
|
11
src/modules/simulation-actions/helpers/refresh.ts
Normal file
11
src/modules/simulation-actions/helpers/refresh.ts
Normal file
|
@ -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))
|
||||
}
|
10
src/modules/simulation-actions/helpers/undo.ts
Normal file
10
src/modules/simulation-actions/helpers/undo.ts
Normal file
|
@ -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()
|
||||
}
|
9
src/modules/simulation-actions/types/SidebarAction.ts
Normal file
9
src/modules/simulation-actions/types/SidebarAction.ts
Normal file
|
@ -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
|
||||
}
|
4
src/modules/simulation-actions/types/possibleAction.ts
Normal file
4
src/modules/simulation-actions/types/possibleAction.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Type repressenting al lpossible actions
|
||||
*/
|
||||
export type possibleAction = 'save' | 'clear' | 'clean' | 'refresh' | 'undo'
|
|
@ -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<Gate>
|
||||
|
||||
/**
|
||||
* 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<number, GateNode>()
|
||||
|
||||
/**
|
||||
* The head of the dequeue
|
||||
*/
|
||||
private head = new LruCacheNode<Gate>(0, null)
|
||||
|
||||
/**
|
||||
* The tail of the dequue
|
||||
*/
|
||||
private tail = new LruCacheNode<Gate>(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<Gate> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<MouseEventInfo>()
|
||||
|
@ -37,17 +38,16 @@ export class SimulationRenderer {
|
|||
public mouseMoveOutput = new Subject<MouseEventInfo>()
|
||||
public wheelOutput = new Subject<unknown>()
|
||||
|
||||
public selectedGate: number | null = null
|
||||
public selectedGates = new Set<Selection>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (!gate || !gate.data) return
|
||||
|
||||
const transform = gate.data.transform
|
||||
if (this.mouseState & 1 && this.selectedGates.size) {
|
||||
for (const gate of this.getSelected()) {
|
||||
const { transform } = gate
|
||||
|
||||
transform.x = worldPosition[0] - this.gateSelectionOffset[0]
|
||||
transform.y = worldPosition[1] - this.gateSelectionOffset[1]
|
||||
|
||||
if (!this.movedSelection) {
|
||||
this.movedSelection = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,28 +277,17 @@ 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
|
||||
}
|
||||
|
||||
const node = this.simulation.gates.get(selected.id)
|
||||
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
if (!node) continue
|
||||
|
||||
for (const wire of this.simulation.wires) {
|
||||
if (wireConnectedToGate(selected, wire)) {
|
||||
if (wireConnectedToGate(gate, wire)) {
|
||||
wire.dispose()
|
||||
}
|
||||
}
|
||||
|
@ -296,9 +296,10 @@ export class SimulationRenderer {
|
|||
wire => wire.active
|
||||
)
|
||||
|
||||
selected.dispose()
|
||||
gate.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 || !gate.data) return null
|
||||
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}`
|
||||
)
|
||||
}
|
||||
|
||||
return gate.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
4
src/modules/simulationRenderer/types/Selection.ts
Normal file
4
src/modules/simulationRenderer/types/Selection.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface Selection {
|
||||
id: number
|
||||
permanent: boolean
|
||||
}
|
Loading…
Reference in a new issue