removed rotations and added the base for multi-gate selections

This commit is contained in:
Matei Adriel 2019-07-24 17:56:58 +03:00
parent 116c0d5e01
commit a9034559a3
32 changed files with 520 additions and 211 deletions

View file

@ -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

View 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())

View file

@ -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() {

View file

@ -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]
}
}

View 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
}

View file

@ -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 />

View file

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

View file

@ -0,0 +1,4 @@
/**
* All possibl values for the mouse button
*/
export type mouseButton = 0 | 1 | 2

View file

@ -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}'`
}
}

View file

@ -9,6 +9,7 @@ export const DutchTranslation: Translation = {
createSimulation: 'Maak simulatie',
logicGates: 'Logische poorten',
openSimulation: 'Open simulatie',
simulation: 'Todo',
language: 'Taal'
},
createSimulation: {

View file

@ -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`,

View file

@ -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>
}

View file

@ -1,4 +0,0 @@
import { KeyBindingMap } from './types/KeyBindingMap'
import { save } from '../saving/helpers/save'
export const keyBindings: KeyBindingMap = []

View file

@ -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,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()
}
}
})
}

View file

@ -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

View file

@ -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

View 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']
)
}

View 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()
}

View 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
}
}
}

View file

@ -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]
)
)
}

View 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))
}

View 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()
}

View 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
}

View file

@ -0,0 +1,4 @@
/**
* Type repressenting al lpossible actions
*/
export type possibleAction = 'save' | 'clear' | 'clean' | 'refresh' | 'undo'

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}
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
})
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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()

View file

@ -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()
}
}

View file

@ -0,0 +1,4 @@
export interface Selection {
id: number
permanent: boolean
}