more advanced selection
This commit is contained in:
parent
a9034559a3
commit
48b967ead8
|
@ -1,9 +1,4 @@
|
|||
import { Screen } from '../../../modules/core/classes/Screen'
|
||||
|
||||
/**
|
||||
* A screen instance used for the canvas clearing
|
||||
*/
|
||||
const screen = new Screen()
|
||||
import { Screen } from '../../../modules/screen/helpers/Screen'
|
||||
|
||||
/**
|
||||
* Clears the used portion of the canvas
|
||||
|
@ -11,5 +6,5 @@ const screen = new Screen()
|
|||
* @param ctx the context to clear
|
||||
*/
|
||||
export const clearCanvas = (ctx: CanvasRenderingContext2D) => {
|
||||
ctx.clearRect(0, 0, screen.x, screen.y)
|
||||
ctx.clearRect(0, 0, Screen.width, Screen.height)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@ export class Transform {
|
|||
) {}
|
||||
|
||||
public getBoundingBox() {
|
||||
return [...this.position, ...this.scale] as vector4
|
||||
const result = [...this.position, ...this.scale] as vector4
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public getPoints() {
|
||||
|
@ -21,18 +23,7 @@ export class Transform {
|
|||
this.y + this.width * combination[1]
|
||||
])
|
||||
|
||||
const pointsInTheRightOrder = [
|
||||
points[0],
|
||||
points[1],
|
||||
points[3],
|
||||
points[2]
|
||||
] as vector2[]
|
||||
|
||||
const result = pointsInTheRightOrder.map(point =>
|
||||
rotateAroundVector(point, this.center, this.rotation)
|
||||
) as vector2[]
|
||||
|
||||
return result
|
||||
return points as vector2[]
|
||||
}
|
||||
|
||||
public getEdges() {
|
||||
|
@ -64,12 +55,20 @@ export class Transform {
|
|||
return this.scale[1]
|
||||
}
|
||||
|
||||
get minX() {
|
||||
return Math.min(this.x, this.x + this.width)
|
||||
}
|
||||
|
||||
get maxX() {
|
||||
return this.x + this.width
|
||||
return Math.max(this.x, this.x + this.width)
|
||||
}
|
||||
|
||||
get minY() {
|
||||
return Math.min(this.y, this.y + this.height)
|
||||
}
|
||||
|
||||
get maxY() {
|
||||
return this.y + this.height
|
||||
return Math.max(this.y, this.y + this.height)
|
||||
}
|
||||
|
||||
get center() {
|
||||
|
|
|
@ -3,9 +3,21 @@ import { vector2 } from '../types/vector2'
|
|||
|
||||
export const pointInSquare = (point: vector2, square: Transform) => {
|
||||
return (
|
||||
point[0] >= square.x &&
|
||||
point[0] >= square.minX &&
|
||||
point[0] <= square.maxX &&
|
||||
point[1] >= square.y &&
|
||||
point[1] >= square.minY &&
|
||||
point[1] <= square.maxY
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The old version of pontInSquare
|
||||
*/
|
||||
export const oldPointInSquare = (point: vector2, square: Transform) => {
|
||||
return (
|
||||
point[0] >= square.x &&
|
||||
point[0] <= square.x + square.width &&
|
||||
point[1] >= square.y &&
|
||||
point[1] <= square.y + square.height
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { Singleton } from '@eix-js/utils'
|
||||
import { Observable, fromEvent, BehaviorSubject } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { multiply } from '../../vector2/helpers/basic'
|
||||
import { sidebarWidth } from '../components/Sidebar'
|
||||
|
||||
@Singleton
|
||||
export class Screen {
|
||||
private getWidth() {
|
||||
return window.innerWidth - sidebarWidth
|
||||
}
|
||||
|
||||
public width = new BehaviorSubject<number>(this.getWidth())
|
||||
public height = new BehaviorSubject<number>(window.innerHeight)
|
||||
|
||||
public constructor() {
|
||||
const resize = fromEvent(window, 'resize')
|
||||
|
||||
resize
|
||||
.pipe(map(() => this.getWidth()))
|
||||
.subscribe(val => this.width.next(val))
|
||||
resize
|
||||
.pipe(map(() => window.innerHeight))
|
||||
.subscribe(val => this.height.next(val))
|
||||
}
|
||||
|
||||
public get x() {
|
||||
return this.width.value
|
||||
}
|
||||
|
||||
public get y() {
|
||||
return this.height.value
|
||||
}
|
||||
|
||||
public get center() {
|
||||
return multiply([this.x, this.y], 0.5)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
import React, { RefObject, forwardRef, MouseEvent, WheelEvent } from 'react'
|
||||
import { useObservable } from 'rxjs-hooks'
|
||||
import { Screen } from '../classes/Screen'
|
||||
import { Subject } from 'rxjs'
|
||||
import { mouseButton } from '../types/mouseButton'
|
||||
import { MouseEventInfo } from './MouseEventInfo'
|
||||
|
||||
const screen = new Screen()
|
||||
import { width, height } from '../../screen/helpers/Screen'
|
||||
|
||||
export interface FluidCanvasProps {
|
||||
mouseDownOuput: Subject<MouseEventInfo>
|
||||
|
@ -30,14 +28,14 @@ export const mouseEventHandler = (output: Subject<MouseEventInfo>) => (
|
|||
|
||||
const FluidCanvas = forwardRef(
|
||||
(props: FluidCanvasProps, ref: RefObject<HTMLCanvasElement>) => {
|
||||
const width = useObservable(() => screen.width, 0)
|
||||
const height = useObservable(() => screen.height, 0)
|
||||
const currentWidth = useObservable(() => width, 0)
|
||||
const currentHeight = useObservable(() => height, 0)
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={ref}
|
||||
width={width}
|
||||
height={height}
|
||||
width={currentWidth}
|
||||
height={currentHeight}
|
||||
onMouseDown={mouseEventHandler(props.mouseDownOuput)}
|
||||
onMouseUp={mouseEventHandler(props.mouseUpOutput)}
|
||||
onMouseMove={mouseEventHandler(props.mouseMoveOutput)}
|
||||
|
|
|
@ -27,9 +27,10 @@ export const EnglishTranslation: Translation = {
|
|||
actions: {
|
||||
save: 'Save',
|
||||
clean: 'Clean',
|
||||
clear: 'Clear',
|
||||
refresh: 'Refresh',
|
||||
undo: 'Undo'
|
||||
undo: 'Undo',
|
||||
'select all': 'Select all',
|
||||
'delete selection': 'Delete selection'
|
||||
},
|
||||
messages: {
|
||||
createdSimulation: name => `Succesfully created simulation '${name}'`,
|
||||
|
@ -38,7 +39,6 @@ export const EnglishTranslation: Translation = {
|
|||
savedSimulation: name => `Succesfully saved simulation '${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}'`
|
||||
}
|
||||
|
|
|
@ -12,6 +12,14 @@ export const DutchTranslation: Translation = {
|
|||
simulation: 'Todo',
|
||||
language: 'Taal'
|
||||
},
|
||||
actions: {
|
||||
'delete selection': 'Todo',
|
||||
'select all': 'Todo',
|
||||
clean: 'Todo',
|
||||
refresh: 'Todo',
|
||||
save: 'Todo',
|
||||
undo: 'Todo'
|
||||
},
|
||||
createSimulation: {
|
||||
mode: {
|
||||
question: 'Wat voor simulatie wil je maken?',
|
||||
|
@ -30,6 +38,9 @@ export const DutchTranslation: Translation = {
|
|||
switchedToSimulation: name =>
|
||||
`Succesvol veranderd naar simulatie '${name}'`,
|
||||
savedSimulation: name => `Simulatie succesvol opgeslagen '${name}'`,
|
||||
compiledIc: name => `Todo: ${name}`
|
||||
compiledIc: name => `Todo: ${name}`,
|
||||
cleaned: name => `Todo ${name}`,
|
||||
refreshed: name => `Todo ${name}`,
|
||||
undone: name => `Todo ${name}`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,12 @@ export const RomanianTranslation: Translation = {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
save: 'Salvează'
|
||||
save: 'Salvează',
|
||||
'delete selection': 'Șterge selecția',
|
||||
'select all': 'Selectează totul',
|
||||
clean: 'Curăță',
|
||||
refresh: 'Reîncarcă',
|
||||
undo: 'Întoarce'
|
||||
},
|
||||
messages: {
|
||||
createdSimulation: name =>
|
||||
|
@ -33,6 +38,9 @@ export const RomanianTranslation: Translation = {
|
|||
switchedToSimulation: name =>
|
||||
`Simulația '${name}' a fost deschisă cu succes`,
|
||||
savedSimulation: name => `Simulația '${name}' a fost salvată cu succes`,
|
||||
compiledIc: name => `Simulația '${name}' a fost compilată cu succes`
|
||||
compiledIc: name => `Simulația '${name}' a fost compilată cu succes`,
|
||||
cleaned: name => `Simulația '${name}' a fost curățată cu succes`,
|
||||
refreshed: name => `Simulația '${name}' a fost reîncărcată cu succes`,
|
||||
undone: name => `Acțiunea a fost întoarsă`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ export interface Translation {
|
|||
savedSimulation: NameSentence
|
||||
compiledIc: NameSentence
|
||||
refreshed: NameSentence
|
||||
cleared: NameSentence
|
||||
cleaned: NameSentence
|
||||
undone: NameSentence
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
|
|||
name: 'or'
|
||||
},
|
||||
material: {
|
||||
value: 'yellow'
|
||||
type: 'image',
|
||||
value: require('../../assets/or_gate.png')
|
||||
},
|
||||
code: {
|
||||
activation: `context.set(0, context.get(0) || context.get(1))`
|
||||
|
@ -38,6 +39,24 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
|
|||
},
|
||||
info: ['https://en.wikipedia.org/wiki/OR_gate']
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
name: 'nor'
|
||||
},
|
||||
material: {
|
||||
type: 'image',
|
||||
value: require('../../assets/nor_gate.png')
|
||||
},
|
||||
code: {
|
||||
activation: `context.set(0, !(context.get(0) || context.get(1)))`
|
||||
},
|
||||
pins: {
|
||||
inputs: {
|
||||
count: 2
|
||||
}
|
||||
},
|
||||
info: ['https://en.wikipedia.org/wiki/NOR_gate']
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
name: 'xor'
|
||||
|
|
|
@ -8,7 +8,7 @@ import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationR
|
|||
export const dumpSimulation = (renderer: SimulationRenderer) => {
|
||||
renderer.simulation.dispose()
|
||||
renderer.lastMousePosition = [0, 0]
|
||||
renderer.selectedGates = new Set()
|
||||
renderer.clearSelection()
|
||||
renderer.selectedPins = {
|
||||
end: null,
|
||||
start: null
|
||||
|
|
27
src/modules/screen/helpers/Screen.ts
Normal file
27
src/modules/screen/helpers/Screen.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Transform } from '../../../common/math/classes/Transform'
|
||||
import { BehaviorSubject, fromEvent } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { getWidth } from '../helpers/getWidth'
|
||||
|
||||
const width = new BehaviorSubject(getWidth())
|
||||
const height = new BehaviorSubject(window.innerHeight)
|
||||
|
||||
const resize = fromEvent(window, 'resize')
|
||||
|
||||
resize.pipe(map(getWidth)).subscribe(val => width.next(val))
|
||||
resize.pipe(map(() => window.innerHeight)).subscribe(val => height.next(val))
|
||||
|
||||
/**
|
||||
* The main screen transform
|
||||
*/
|
||||
const Screen = new Transform()
|
||||
|
||||
width.subscribe(currentWidth => {
|
||||
Screen.width = currentWidth
|
||||
})
|
||||
|
||||
height.subscribe(currentHeight => {
|
||||
Screen.height = currentHeight
|
||||
})
|
||||
|
||||
export { Screen, height, width }
|
6
src/modules/screen/helpers/getWidth.ts
Normal file
6
src/modules/screen/helpers/getWidth.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { sidebarWidth } from '../../core/components/Sidebar'
|
||||
|
||||
/**
|
||||
* Helper to get the width of the canvas
|
||||
*/
|
||||
export const getWidth = () => window.innerWidth - sidebarWidth
|
|
@ -7,6 +7,7 @@ import MenuItem from '@material-ui/core/MenuItem'
|
|||
import Icon from '@material-ui/core/Icon'
|
||||
import { useTranslation } from '../../internalisation/helpers/useLanguage'
|
||||
import { SidebarActions } from '../constants'
|
||||
import { possibleAction } from '../types/possibleAction'
|
||||
|
||||
/**
|
||||
* Component wich contains the sidebar 'Simulation' button
|
||||
|
@ -49,7 +50,11 @@ const SimulationActions = () => {
|
|||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={name}
|
||||
primary={
|
||||
translation.actions[
|
||||
name as possibleAction
|
||||
]
|
||||
}
|
||||
secondary={(keybinding || []).join(' + ')}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
|
|
@ -4,14 +4,16 @@ 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'
|
||||
import { selectAll } from './helpers/selectAll'
|
||||
import { deleteSelection } from './helpers/deleteSelection'
|
||||
|
||||
export const actionIcons: Record<possibleAction, string> = {
|
||||
clean: 'layers_clear',
|
||||
clear: 'clear',
|
||||
clean: 'clear',
|
||||
refresh: 'refresh',
|
||||
save: 'save',
|
||||
undo: 'undo'
|
||||
undo: 'undo',
|
||||
'select all': 'select_all',
|
||||
'delete selection': 'delete'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,13 +35,6 @@ export const SidebarActions: Record<possibleAction, SidebarAction> = {
|
|||
},
|
||||
['ctrl', 'z']
|
||||
),
|
||||
...createActionConfig(
|
||||
'clear',
|
||||
{
|
||||
run: clear
|
||||
},
|
||||
['ctrl', 'delete']
|
||||
),
|
||||
...createActionConfig(
|
||||
'clean',
|
||||
{
|
||||
|
@ -47,6 +42,8 @@ export const SidebarActions: Record<possibleAction, SidebarAction> = {
|
|||
console.log('Cleaning')
|
||||
}
|
||||
},
|
||||
['ctrl', 'shift', 'delete']
|
||||
)
|
||||
['ctrl', 'delete']
|
||||
),
|
||||
...createActionConfig('select all', selectAll, ['ctrl', 'a']),
|
||||
...createActionConfig('delete selection', deleteSelection, ['delete'])
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ 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 ActionConfigFunction = (renderer: SimulationRenderer) => void
|
||||
|
||||
export type ActionConfigCallback =
|
||||
| {
|
||||
|
|
|
@ -10,12 +10,10 @@ 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']
|
||||
export const actionToMessageMap: Partial<
|
||||
Record<possibleAction, keyof Translation['messages']>
|
||||
> = {
|
||||
clean: 'cleaned',
|
||||
clear: 'cleared',
|
||||
refresh: 'refreshed',
|
||||
undo: 'undone',
|
||||
save: 'savedSimulation'
|
||||
|
@ -30,12 +28,14 @@ export const createRendererAction = (
|
|||
|
||||
callback(renderer)
|
||||
|
||||
const messageName = actionToMessageMap[action]
|
||||
|
||||
if (messageName) {
|
||||
toast(
|
||||
...createToastArguments(
|
||||
translation.messages[actionToMessageMap[action]](
|
||||
renderer.simulation.name
|
||||
),
|
||||
translation.messages[messageName](renderer.simulation.name),
|
||||
actionIcons[action]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
|
||||
import { deleteGate } from '../../simulationRenderer/helpers/deleteGate'
|
||||
|
||||
export const deleteSelection = (renderer: SimulationRenderer) => {
|
||||
for (const gate of renderer.getSelected()) {
|
||||
deleteGate(renderer.simulation, gate)
|
||||
}
|
||||
}
|
13
src/modules/simulation-actions/helpers/selectAll.ts
Normal file
13
src/modules/simulation-actions/helpers/selectAll.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
|
||||
import { addIdToSelection } from '../../simulationRenderer/helpers/idIsSelected'
|
||||
|
||||
/**
|
||||
* Selects all the gates of an renderer
|
||||
*
|
||||
* @param renderer The renderer to selet all the gates of
|
||||
*/
|
||||
export const selectAll = (renderer: SimulationRenderer) => {
|
||||
for (const { id } of renderer.simulation.gates) {
|
||||
addIdToSelection(renderer, 'permanent', id)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
/**
|
||||
* Type repressenting al lpossible actions
|
||||
*/
|
||||
export type possibleAction = 'save' | 'clear' | 'clean' | 'refresh' | 'undo'
|
||||
export type possibleAction =
|
||||
| 'save'
|
||||
| 'clean'
|
||||
| 'refresh'
|
||||
| 'undo'
|
||||
| 'select all'
|
||||
| 'delete selection'
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { templateStore } from '../../saving/stores/templateStore'
|
||||
import { SimulationError } from '../../errors/classes/SimulationError'
|
||||
import { Simulation } from '../classes/Simulation'
|
||||
import { Gate } from '../classes/Gate'
|
||||
import { add, relativeTo, multiply } from '../../vector2/helpers/basic'
|
||||
import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer'
|
||||
import { DefaultGateTemplate } from '../constants'
|
||||
import { vector2 } from '../../../common/math/classes/Transform'
|
||||
import { Screen } from '../../screen/helpers/Screen'
|
||||
|
||||
export const addGate = (renderer: SimulationRenderer, templateName: string) => {
|
||||
const template = templateStore.get(templateName)
|
||||
|
@ -22,7 +22,7 @@ export const addGate = (renderer: SimulationRenderer, templateName: string) => {
|
|||
|
||||
const origin = relativeTo(
|
||||
multiply(gateScale, 0.5),
|
||||
relativeTo(renderer.camera.transform.position, renderer.screen.center)
|
||||
relativeTo(renderer.camera.transform.position, Screen.center)
|
||||
)
|
||||
|
||||
const scalarOffset = renderer.options.spawning.spawnOffset
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
import { Transform } from '../../../common/math/classes/Transform'
|
||||
import { vector2 } from '../../../common/math/types/vector2'
|
||||
import { Screen } from '../../core/classes/Screen'
|
||||
import { relativeTo } from '../../vector2/helpers/basic'
|
||||
|
||||
export class Camera {
|
||||
public transform = new Transform([0, 0])
|
||||
|
||||
public constructor() {
|
||||
// this.screen.height.subscribe(value => {
|
||||
// this.transform.height = value
|
||||
// })
|
||||
// this.screen.width.subscribe(value => {
|
||||
// this.transform.width = value
|
||||
// })
|
||||
}
|
||||
|
||||
public toWordPostition(position: vector2) {
|
||||
return [
|
||||
(position[0] - this.transform.position[0]) /
|
||||
|
|
|
@ -2,9 +2,11 @@ import { Camera } from './Camera'
|
|||
import { Simulation } from '../../simulation/classes/Simulation'
|
||||
import { Subject } from 'rxjs'
|
||||
import { MouseEventInfo } from '../../core/components/MouseEventInfo'
|
||||
import { pointInSquare } from '../../../common/math/helpers/pointInSquare'
|
||||
import {
|
||||
oldPointInSquare,
|
||||
pointInSquare
|
||||
} from '../../../common/math/helpers/pointInSquare'
|
||||
import { vector2 } from '../../../common/math/types/vector2'
|
||||
import { Screen } from '../../core/classes/Screen'
|
||||
import { relativeTo, add, invert } from '../../vector2/helpers/basic'
|
||||
import { SimulationRendererOptions } from '../types/SimulationRendererOptions'
|
||||
import { defaultSimulationRendererOptions, mouseButtons } from '../constants'
|
||||
|
@ -12,8 +14,6 @@ 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 { initKeyBindings } from '../../keybindings/helpers/initialiseKeyBindings'
|
||||
import { currentStore } from '../../saving/stores/currentStore'
|
||||
import { saveStore } from '../../saving/stores/saveStore'
|
||||
import {
|
||||
|
@ -21,7 +21,6 @@ import {
|
|||
fromCameraState
|
||||
} from '../../saving/helpers/fromState'
|
||||
import merge from 'deepmerge'
|
||||
import { wireConnectedToGate } from '../helpers/wireConnectedToGate'
|
||||
import { updateMouse, handleScroll } from '../helpers/scaleCanvas'
|
||||
import { RefObject } from 'react'
|
||||
import { dumpSimulation } from '../../saving/helpers/dumpSimulation'
|
||||
|
@ -30,7 +29,10 @@ 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'
|
||||
import { Transform } from '../../../common/math/classes/Transform'
|
||||
import { gatesInSelection } from '../helpers/gatesInSelection'
|
||||
import { selectionType } from '../types/selectionType'
|
||||
import { addIdToSelection } from '../helpers/idIsSelected'
|
||||
|
||||
export class SimulationRenderer {
|
||||
public mouseDownOutput = new Subject<MouseEventInfo>()
|
||||
|
@ -38,17 +40,22 @@ export class SimulationRenderer {
|
|||
public mouseMoveOutput = new Subject<MouseEventInfo>()
|
||||
public wheelOutput = new Subject<unknown>()
|
||||
|
||||
public selectedGates = new Set<Selection>()
|
||||
public lastMousePosition: vector2 = [0, 0]
|
||||
public selectedGates: Record<selectionType, Set<number>> = {
|
||||
temporary: new Set<number>(),
|
||||
permanent: new Set<number>()
|
||||
}
|
||||
|
||||
public options: SimulationRendererOptions
|
||||
public screen = new Screen()
|
||||
public camera = new Camera()
|
||||
|
||||
public selectedArea = new Transform()
|
||||
|
||||
// first bit = dragging
|
||||
// second bit = panning around
|
||||
// third bit = selecting
|
||||
private mouseState = 0b000
|
||||
private gateSelectionOffset: vector2 = [0, 0]
|
||||
public mouseState = 0b000
|
||||
|
||||
public lastMousePosition: vector2 = [0, 0]
|
||||
|
||||
// this is used for spawning gates
|
||||
public spawnCount = 0
|
||||
|
@ -89,14 +96,7 @@ export class SimulationRenderer {
|
|||
|
||||
this.mouseState |= 1
|
||||
|
||||
this.selectedGates.add({
|
||||
id,
|
||||
permanent: false
|
||||
})
|
||||
this.gateSelectionOffset = worldPosition.map(
|
||||
(position, index) =>
|
||||
position - transform.position[index]
|
||||
) as vector2
|
||||
addIdToSelection(this, 'temporary', id)
|
||||
|
||||
const gateNode = this.simulation.gates.get(id)
|
||||
|
||||
|
@ -132,10 +132,10 @@ export class SimulationRenderer {
|
|||
`Cannot find wire to remove`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.selectedPins.start &&
|
||||
|
@ -177,29 +177,59 @@ export class SimulationRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
if (event.button === mouseButtons.unselect) {
|
||||
this.clearSelection()
|
||||
}
|
||||
|
||||
if (event.button === mouseButtons.pan) {
|
||||
// the second bit = pannning
|
||||
this.mouseState |= 0b10
|
||||
} else if (event.button === mouseButtons.select) {
|
||||
this.selectedArea.position = this.lastMousePosition
|
||||
this.selectedArea.scale = [0, 0]
|
||||
|
||||
// the third bit = selecting
|
||||
this.mouseState |= 0b100
|
||||
}
|
||||
})
|
||||
|
||||
this.mouseUpOutput.subscribe(() => {
|
||||
if (this.selectedGates.size) {
|
||||
this.mouseUpOutput.subscribe(event => {
|
||||
if (event.button === mouseButtons.drag) {
|
||||
const selected = this.getSelected()
|
||||
|
||||
for (const gate of selected) {
|
||||
gate.transform.rotation = 0
|
||||
}
|
||||
|
||||
for (const selection of this.selectedGates.values()) {
|
||||
if (!selection.permanent) {
|
||||
this.selectedGates.delete(selection)
|
||||
this.selectedGates.temporary.clear()
|
||||
|
||||
// turn first 2 bits to 0
|
||||
this.mouseState &= 1 << 2
|
||||
|
||||
// for debugging
|
||||
if ((this.mouseState >> 1) & 1 || this.mouseState & 1) {
|
||||
throw new SimulationError(
|
||||
'First 2 bits of mouseState need to be set to 0'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.mouseState &= 0
|
||||
}
|
||||
if (
|
||||
event.button === mouseButtons.select &&
|
||||
(this.mouseState >> 2) & 1
|
||||
) {
|
||||
// turn the third bit to 0
|
||||
this.mouseState &= (1 << 2) - 1
|
||||
|
||||
this.mouseState &= 0b00
|
||||
const selectedGates = gatesInSelection(
|
||||
this.selectedArea,
|
||||
Array.from(this.simulation.gates)
|
||||
)
|
||||
|
||||
for (const { id } of selectedGates) {
|
||||
addIdToSelection(this, 'permanent', id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.mouseMoveOutput.subscribe(event => {
|
||||
|
@ -207,22 +237,22 @@ export class SimulationRenderer {
|
|||
|
||||
const worldPosition = this.camera.toWordPostition(event.position)
|
||||
|
||||
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.mouseState >> 1) & 1) {
|
||||
const offset = invert(
|
||||
relativeTo(this.lastMousePosition, worldPosition)
|
||||
).map(
|
||||
(value, index) => value * this.camera.transform.scale[index]
|
||||
) as vector2
|
||||
|
||||
if (this.mouseState & 1) {
|
||||
for (const gate of this.getSelected()) {
|
||||
const { transform } = gate
|
||||
|
||||
transform.x -= offset[0]
|
||||
transform.y -= offset[1]
|
||||
}
|
||||
}
|
||||
|
||||
if ((this.mouseState >> 1) & 1) {
|
||||
this.camera.transform.position = add(
|
||||
this.camera.transform.position,
|
||||
invert(offset)
|
||||
|
@ -231,13 +261,17 @@ export class SimulationRenderer {
|
|||
this.spawnCount = 0
|
||||
}
|
||||
|
||||
if ((this.mouseState >> 2) & 1) {
|
||||
this.selectedArea.scale = relativeTo(
|
||||
this.selectedArea.position,
|
||||
this.camera.toWordPostition(event.position)
|
||||
)
|
||||
}
|
||||
|
||||
this.lastMousePosition = this.camera.toWordPostition(event.position)
|
||||
})
|
||||
|
||||
dumpSimulation(this)
|
||||
|
||||
this.reloadSave()
|
||||
this.initKeyBindings()
|
||||
}
|
||||
|
||||
public updateWheelListener() {
|
||||
|
@ -259,6 +293,8 @@ export class SimulationRenderer {
|
|||
|
||||
public reloadSave() {
|
||||
try {
|
||||
dumpSimulation(this)
|
||||
|
||||
const current = currentStore.get()
|
||||
const save = saveStore.get(current)
|
||||
|
||||
|
@ -275,42 +311,6 @@ export class SimulationRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
private initKeyBindings() {
|
||||
const bindings: KeyBindingMap = [
|
||||
{
|
||||
keys: ['delete'],
|
||||
actions: [
|
||||
() => {
|
||||
for (const gate of this.getSelected()) {
|
||||
const node = this.simulation.gates.get(gate.id)
|
||||
|
||||
if (!node) continue
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
initKeyBindings(bindings)
|
||||
}
|
||||
|
||||
public getGateById(id: number) {
|
||||
return this.simulation.gates.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all selected gates in the simulation
|
||||
*
|
||||
|
@ -318,7 +318,7 @@ export class SimulationRenderer {
|
|||
* @throws SimulationError if the id doesnt have a data prop
|
||||
*/
|
||||
public getSelected() {
|
||||
return setToArray(this.selectedGates).map(({ id }) => {
|
||||
return setToArray(this.allSelectedIds()).map(id => {
|
||||
const gate = this.simulation.gates.get(id)
|
||||
|
||||
if (!gate) {
|
||||
|
@ -332,4 +332,22 @@ export class SimulationRenderer {
|
|||
return gate.data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* helper to merge the temporary and permanent selection
|
||||
*/
|
||||
public allSelectedIds() {
|
||||
return new Set([
|
||||
...setToArray(this.selectedGates.permanent),
|
||||
...setToArray(this.selectedGates.temporary)
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to clear all selected sets
|
||||
*/
|
||||
public clearSelection() {
|
||||
this.selectedGates.permanent.clear()
|
||||
this.selectedGates.temporary.clear()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,17 +27,22 @@ export const defaultSimulationRendererOptions: SimulationRendererOptions = {
|
|||
},
|
||||
spawning: {
|
||||
spawnOffset: 30
|
||||
},
|
||||
selecting: {
|
||||
fill: 'rgba(128,128,128,0.3)',
|
||||
stroke: `rgba(128,128,128,0.7)`
|
||||
}
|
||||
}
|
||||
|
||||
export const imageQuality: vector2 = [100, 100]
|
||||
|
||||
export const mouseButtons: Record<
|
||||
'zoom' | 'pan' | 'drag' | 'select',
|
||||
'zoom' | 'pan' | 'drag' | 'select' | 'unselect',
|
||||
mouseButton
|
||||
> = {
|
||||
zoom: 1,
|
||||
drag: 0,
|
||||
pan: 0,
|
||||
select: 2
|
||||
drag: 2,
|
||||
pan: 2,
|
||||
select: 0,
|
||||
unselect: 0
|
||||
}
|
||||
|
|
10
src/modules/simulationRenderer/helpers/aabb.ts
Normal file
10
src/modules/simulationRenderer/helpers/aabb.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Transform } from '../../../common/math/classes/Transform'
|
||||
|
||||
export const aabbCollisionDetection = (rect1: Transform, rect2: Transform) => {
|
||||
return !(
|
||||
rect1.maxX < rect2.minX ||
|
||||
rect1.maxY < rect2.minY ||
|
||||
rect1.minX > rect2.maxX ||
|
||||
rect1.minY > rect2.maxY
|
||||
)
|
||||
}
|
29
src/modules/simulationRenderer/helpers/deleteGate.ts
Normal file
29
src/modules/simulationRenderer/helpers/deleteGate.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { SimulationRenderer } from '../classes/SimulationRenderer'
|
||||
import { Gate } from '../../simulation/classes/Gate'
|
||||
import { wireConnectedToGate } from './wireConnectedToGate'
|
||||
import { Simulation } from '../../simulation/classes/Simulation'
|
||||
|
||||
/**
|
||||
* Helper to delete a gate from a simulation
|
||||
*
|
||||
* @param simulation The simulation to remove the gate from
|
||||
* @param gate The gate to remove
|
||||
*/
|
||||
export const deleteGate = (simulation: Simulation, gate: Gate) => {
|
||||
const node = simulation.gates.get(gate.id)
|
||||
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const wire of simulation.wires) {
|
||||
if (wireConnectedToGate(gate, wire)) {
|
||||
wire.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
simulation.wires = simulation.wires.filter(wire => wire.active)
|
||||
|
||||
gate.dispose()
|
||||
simulation.gates.delete(node)
|
||||
}
|
24
src/modules/simulationRenderer/helpers/gatesInSelection.ts
Normal file
24
src/modules/simulationRenderer/helpers/gatesInSelection.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Gate } from '../../simulation/classes/Gate'
|
||||
import { aabbCollisionDetection } from './aabb'
|
||||
import { Transform } from '../../../common/math/classes/Transform'
|
||||
import { pointInSquare } from '../../../common/math/helpers/pointInSquare'
|
||||
|
||||
/**
|
||||
* Finds all selections in the selected area
|
||||
*
|
||||
* @param renderer The renderer to find the selected gates of
|
||||
*/
|
||||
export const gatesInSelection = (
|
||||
selectedArea: Transform,
|
||||
gates: Gate[] = []
|
||||
) => {
|
||||
return gates.filter(({ transform }) => {
|
||||
for (const point of transform.getPoints()) {
|
||||
if (pointInSquare(point, selectedArea)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
36
src/modules/simulationRenderer/helpers/idIsSelected.ts
Normal file
36
src/modules/simulationRenderer/helpers/idIsSelected.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { SimulationRenderer } from '../classes/SimulationRenderer'
|
||||
import { selectionType } from '../types/selectionType'
|
||||
|
||||
/**
|
||||
* Checks if an id is selected inside a renderer
|
||||
*
|
||||
* @param renderer The renderer to check for the id
|
||||
* @param gateId The id of the gate
|
||||
*/
|
||||
export const idIsSelected = (renderer: SimulationRenderer, gateId: number) => {
|
||||
return renderer.allSelectedIds().has(gateId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an id to a selection set
|
||||
*
|
||||
* @param renderer The renderer to add the id to the selection set of
|
||||
* @param type The selection type
|
||||
* @param id The id to select
|
||||
*/
|
||||
export const addIdToSelection = (
|
||||
renderer: SimulationRenderer,
|
||||
type: selectionType = 'temporary',
|
||||
id: number
|
||||
) => {
|
||||
if (idIsSelected(renderer, id)) {
|
||||
if (renderer.selectedGates.permanent.has(id)) {
|
||||
return
|
||||
} else if (type === 'temporary') {
|
||||
renderer.selectedGates.temporary.delete(id)
|
||||
renderer.selectedGates.permanent.add(id)
|
||||
}
|
||||
} else {
|
||||
renderer.selectedGates[type].add(id)
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import { useTransform } from '../../../common/canvas/helpers/useTransform'
|
|||
import { roundRect } from '../../../common/canvas/helpers/drawRoundedSquare'
|
||||
import { roundImage } from '../../../common/canvas/helpers/drawRoundedImage'
|
||||
import { ImageStore } from '../stores/imageStore'
|
||||
import { gatesInSelection } from './gatesInSelection'
|
||||
import { idIsSelected } from './idIsSelected'
|
||||
|
||||
export const renderGate = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
|
@ -13,7 +15,11 @@ export const renderGate = (
|
|||
) => {
|
||||
renderPins(ctx, renderer, gate)
|
||||
|
||||
if (renderer.selectedGates.has(gate.id)) {
|
||||
if (
|
||||
((renderer.mouseState >> 2) & 1 &&
|
||||
gatesInSelection(renderer.selectedArea, [gate]).length) ||
|
||||
idIsSelected(renderer, gate.id)
|
||||
) {
|
||||
ctx.strokeStyle = renderer.options.gates.gateStroke.active
|
||||
} else {
|
||||
ctx.strokeStyle = renderer.options.gates.gateStroke.normal
|
||||
|
|
22
src/modules/simulationRenderer/helpers/renderSelectedArea.ts
Normal file
22
src/modules/simulationRenderer/helpers/renderSelectedArea.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { SimulationRenderer } from '../classes/SimulationRenderer'
|
||||
|
||||
/**
|
||||
* Renders the selected area of a renderer
|
||||
*
|
||||
* @param ctx The context to draw on
|
||||
* @param renderer The renderer to draw the selected area of
|
||||
*/
|
||||
export const renderSelectedArea = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
renderer: SimulationRenderer
|
||||
) => {
|
||||
if (renderer.mouseState >> 2) {
|
||||
ctx.fillStyle = renderer.options.selecting.fill
|
||||
ctx.strokeStyle = renderer.options.selecting.stroke
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.rect(...renderer.selectedArea.getBoundingBox())
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import { renderGate } from './renderGate'
|
|||
import { clearCanvas } from '../../../common/canvas/helpers/clearCanvas'
|
||||
import { renderClickedPins } from './renderClickedPins'
|
||||
import { renderWires } from './renderWires'
|
||||
import { vector2 } from '../../../common/math/classes/Transform'
|
||||
import { renderSelectedArea } from './renderSelectedArea'
|
||||
|
||||
export const renderSimulation = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
|
@ -26,6 +26,7 @@ export const renderSimulation = (
|
|||
}
|
||||
|
||||
renderClickedPins(ctx, renderer)
|
||||
renderSelectedArea(ctx, renderer)
|
||||
|
||||
ctx.scale(...inverse(transform.scale))
|
||||
ctx.translate(...invert(transform.position))
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
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/MouseEventInfo'
|
||||
// import { WheelEvent } from 'react'
|
||||
|
||||
const screen = new Screen()
|
||||
import { Screen } from '../../screen/helpers/Screen'
|
||||
|
||||
const scrollStep = 1.3
|
||||
const zoomLimits = [0.1, 10]
|
||||
|
||||
let absoluteMousePosition = [screen.x / 2, screen.y / 2]
|
||||
let absoluteMousePosition = [Screen.width / 2, Screen.height]
|
||||
|
||||
export const updateMouse = (e: MouseEventInfo) => {
|
||||
absoluteMousePosition = e.position
|
||||
|
@ -20,8 +17,7 @@ export const handleScroll = (e: WheelEvent, camera: Camera) => {
|
|||
const sign = e.deltaY / Math.abs(e.deltaY)
|
||||
const zoom = scrollStep ** sign
|
||||
|
||||
const size = [screen.width.value, screen.height.value]
|
||||
const mouseFraction = size.map(
|
||||
const mouseFraction = Screen.scale.map(
|
||||
(value, index) => absoluteMousePosition[index] / value
|
||||
)
|
||||
const newScale = camera.transform.scale.map(value =>
|
||||
|
@ -29,7 +25,9 @@ export const handleScroll = (e: WheelEvent, camera: Camera) => {
|
|||
)
|
||||
const delta = camera.transform.scale.map(
|
||||
(value, index) =>
|
||||
size[index] * (newScale[index] - value) * mouseFraction[index]
|
||||
Screen.scale[index] *
|
||||
(newScale[index] - value) *
|
||||
mouseFraction[index]
|
||||
)
|
||||
|
||||
camera.transform.scale = newScale as vector2
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export interface Selection {
|
||||
id: number
|
||||
permanent: boolean
|
||||
}
|
|
@ -24,4 +24,8 @@ export interface SimulationRendererOptions {
|
|||
spawning: {
|
||||
spawnOffset: number
|
||||
}
|
||||
selecting: {
|
||||
stroke: string
|
||||
fill: string
|
||||
}
|
||||
}
|
||||
|
|
1
src/modules/simulationRenderer/types/selectionType.ts
Normal file
1
src/modules/simulationRenderer/types/selectionType.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type selectionType = 'permanent' | 'temporary'
|
Loading…
Reference in a new issue