diff --git a/src/common/canvas/helpers/clearCanvas.ts b/src/common/canvas/helpers/clearCanvas.ts index fca0c01..7a84e00 100644 --- a/src/common/canvas/helpers/clearCanvas.ts +++ b/src/common/canvas/helpers/clearCanvas.ts @@ -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) } diff --git a/src/common/math/classes/Transform.ts b/src/common/math/classes/Transform.ts index cf2dcba..c501983 100644 --- a/src/common/math/classes/Transform.ts +++ b/src/common/math/classes/Transform.ts @@ -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() { diff --git a/src/common/math/helpers/pointInSquare.ts b/src/common/math/helpers/pointInSquare.ts index 516bfe8..5eb3f76 100644 --- a/src/common/math/helpers/pointInSquare.ts +++ b/src/common/math/helpers/pointInSquare.ts @@ -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 + ) +} diff --git a/src/modules/core/classes/Screen.ts b/src/modules/core/classes/Screen.ts deleted file mode 100644 index d3e86dc..0000000 --- a/src/modules/core/classes/Screen.ts +++ /dev/null @@ -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(this.getWidth()) - public height = new BehaviorSubject(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) - } -} diff --git a/src/modules/core/components/FluidCanvas.tsx b/src/modules/core/components/FluidCanvas.tsx index 589c75f..b04f90a 100644 --- a/src/modules/core/components/FluidCanvas.tsx +++ b/src/modules/core/components/FluidCanvas.tsx @@ -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 @@ -30,14 +28,14 @@ export const mouseEventHandler = (output: Subject) => ( const FluidCanvas = forwardRef( (props: FluidCanvasProps, ref: RefObject) => { - const width = useObservable(() => screen.width, 0) - const height = useObservable(() => screen.height, 0) + const currentWidth = useObservable(() => width, 0) + const currentHeight = useObservable(() => height, 0) return ( `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}'` } diff --git a/src/modules/internalisation/translations/nederlands.ts b/src/modules/internalisation/translations/nederlands.ts index 9981bfa..e3d9454 100644 --- a/src/modules/internalisation/translations/nederlands.ts +++ b/src/modules/internalisation/translations/nederlands.ts @@ -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}` } } diff --git a/src/modules/internalisation/translations/romanian.ts b/src/modules/internalisation/translations/romanian.ts index 5d33cc0..1a61d63 100644 --- a/src/modules/internalisation/translations/romanian.ts +++ b/src/modules/internalisation/translations/romanian.ts @@ -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ă` } } diff --git a/src/modules/internalisation/types/TranslationInterface.ts b/src/modules/internalisation/types/TranslationInterface.ts index 9fb95d8..69e8381 100644 --- a/src/modules/internalisation/types/TranslationInterface.ts +++ b/src/modules/internalisation/types/TranslationInterface.ts @@ -32,7 +32,6 @@ export interface Translation { savedSimulation: NameSentence compiledIc: NameSentence refreshed: NameSentence - cleared: NameSentence cleaned: NameSentence undone: NameSentence } diff --git a/src/modules/saving/constants.ts b/src/modules/saving/constants.ts index d748cb1..af1ecc4 100644 --- a/src/modules/saving/constants.ts +++ b/src/modules/saving/constants.ts @@ -26,7 +26,8 @@ export const baseTemplates: DeepPartial[] = [ 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[] = [ }, 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' diff --git a/src/modules/saving/helpers/dumpSimulation.ts b/src/modules/saving/helpers/dumpSimulation.ts index 7156681..40ea32f 100644 --- a/src/modules/saving/helpers/dumpSimulation.ts +++ b/src/modules/saving/helpers/dumpSimulation.ts @@ -8,7 +8,7 @@ import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationR export const dumpSimulation = (renderer: SimulationRenderer) => { renderer.simulation.dispose() renderer.lastMousePosition = [0, 0] - renderer.selectedGates = new Set() + renderer.clearSelection() renderer.selectedPins = { end: null, start: null diff --git a/src/modules/screen/helpers/Screen.ts b/src/modules/screen/helpers/Screen.ts new file mode 100644 index 0000000..66c8711 --- /dev/null +++ b/src/modules/screen/helpers/Screen.ts @@ -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 } diff --git a/src/modules/screen/helpers/getWidth.ts b/src/modules/screen/helpers/getWidth.ts new file mode 100644 index 0000000..7019456 --- /dev/null +++ b/src/modules/screen/helpers/getWidth.ts @@ -0,0 +1,6 @@ +import { sidebarWidth } from '../../core/components/Sidebar' + +/** + * Helper to get the width of the canvas + */ +export const getWidth = () => window.innerWidth - sidebarWidth diff --git a/src/modules/simulation-actions/components/SimulationActions.tsx b/src/modules/simulation-actions/components/SimulationActions.tsx index 8366915..b72be83 100644 --- a/src/modules/simulation-actions/components/SimulationActions.tsx +++ b/src/modules/simulation-actions/components/SimulationActions.tsx @@ -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 = () => { diff --git a/src/modules/simulation-actions/constants.ts b/src/modules/simulation-actions/constants.ts index cf0d5d9..57f7268 100644 --- a/src/modules/simulation-actions/constants.ts +++ b/src/modules/simulation-actions/constants.ts @@ -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 = { - 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 = { }, ['ctrl', 'z'] ), - ...createActionConfig( - 'clear', - { - run: clear - }, - ['ctrl', 'delete'] - ), ...createActionConfig( 'clean', { @@ -47,6 +42,8 @@ export const SidebarActions: Record = { console.log('Cleaning') } }, - ['ctrl', 'shift', 'delete'] - ) + ['ctrl', 'delete'] + ), + ...createActionConfig('select all', selectAll, ['ctrl', 'a']), + ...createActionConfig('delete selection', deleteSelection, ['delete']) } diff --git a/src/modules/simulation-actions/helpers/createActionConfig.ts b/src/modules/simulation-actions/helpers/createActionConfig.ts index 91d6bd6..dafac9b 100644 --- a/src/modules/simulation-actions/helpers/createActionConfig.ts +++ b/src/modules/simulation-actions/helpers/createActionConfig.ts @@ -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 = | { diff --git a/src/modules/simulation-actions/helpers/createRendererActions.ts b/src/modules/simulation-actions/helpers/createRendererActions.ts index 4e95340..1aaaa47 100644 --- a/src/modules/simulation-actions/helpers/createRendererActions.ts +++ b/src/modules/simulation-actions/helpers/createRendererActions.ts @@ -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 > = { clean: 'cleaned', - clear: 'cleared', refresh: 'refreshed', undo: 'undone', save: 'savedSimulation' @@ -30,12 +28,14 @@ export const createRendererAction = ( callback(renderer) - toast( - ...createToastArguments( - translation.messages[actionToMessageMap[action]]( - renderer.simulation.name - ), - actionIcons[action] + const messageName = actionToMessageMap[action] + + if (messageName) { + toast( + ...createToastArguments( + translation.messages[messageName](renderer.simulation.name), + actionIcons[action] + ) ) - ) + } } diff --git a/src/modules/simulation-actions/helpers/deleteSelection.ts b/src/modules/simulation-actions/helpers/deleteSelection.ts new file mode 100644 index 0000000..1721eda --- /dev/null +++ b/src/modules/simulation-actions/helpers/deleteSelection.ts @@ -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) + } +} diff --git a/src/modules/simulation-actions/helpers/selectAll.ts b/src/modules/simulation-actions/helpers/selectAll.ts new file mode 100644 index 0000000..560bfa5 --- /dev/null +++ b/src/modules/simulation-actions/helpers/selectAll.ts @@ -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) + } +} diff --git a/src/modules/simulation-actions/types/possibleAction.ts b/src/modules/simulation-actions/types/possibleAction.ts index 7efe140..447e0b8 100644 --- a/src/modules/simulation-actions/types/possibleAction.ts +++ b/src/modules/simulation-actions/types/possibleAction.ts @@ -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' diff --git a/src/modules/simulation/helpers/addGate.ts b/src/modules/simulation/helpers/addGate.ts index 9ccafc0..90833a4 100644 --- a/src/modules/simulation/helpers/addGate.ts +++ b/src/modules/simulation/helpers/addGate.ts @@ -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 diff --git a/src/modules/simulationRenderer/classes/Camera.ts b/src/modules/simulationRenderer/classes/Camera.ts index 5e0c8d7..05c07bd 100644 --- a/src/modules/simulationRenderer/classes/Camera.ts +++ b/src/modules/simulationRenderer/classes/Camera.ts @@ -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]) / diff --git a/src/modules/simulationRenderer/classes/SimulationRenderer.ts b/src/modules/simulationRenderer/classes/SimulationRenderer.ts index 268e0ef..3c65033 100644 --- a/src/modules/simulationRenderer/classes/SimulationRenderer.ts +++ b/src/modules/simulationRenderer/classes/SimulationRenderer.ts @@ -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() @@ -38,17 +40,22 @@ export class SimulationRenderer { public mouseMoveOutput = new Subject() public wheelOutput = new Subject() - public selectedGates = new Set() - public lastMousePosition: vector2 = [0, 0] + public selectedGates: Record> = { + temporary: new Set(), + permanent: new Set() + } + 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,9 +132,9 @@ export class SimulationRenderer { `Cannot find wire to remove` ) } - } - return + return + } } if ( @@ -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() - this.mouseState &= 0 + // 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 &= 0b00 + if ( + event.button === mouseButtons.select && + (this.mouseState >> 2) & 1 + ) { + // turn the third bit to 0 + this.mouseState &= (1 << 2) - 1 + + 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) { + 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 = worldPosition[0] - this.gateSelectionOffset[0] - transform.y = worldPosition[1] - this.gateSelectionOffset[1] + transform.x -= offset[0] + transform.y -= offset[1] } } if ((this.mouseState >> 1) & 1) { - const offset = invert( - relativeTo(this.lastMousePosition, worldPosition) - ).map( - (value, index) => value * this.camera.transform.scale[index] - ) as vector2 - 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() + } } diff --git a/src/modules/simulationRenderer/constants.ts b/src/modules/simulationRenderer/constants.ts index 08bb3ac..03b9857 100644 --- a/src/modules/simulationRenderer/constants.ts +++ b/src/modules/simulationRenderer/constants.ts @@ -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 } diff --git a/src/modules/simulationRenderer/helpers/aabb.ts b/src/modules/simulationRenderer/helpers/aabb.ts new file mode 100644 index 0000000..b23fc78 --- /dev/null +++ b/src/modules/simulationRenderer/helpers/aabb.ts @@ -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 + ) +} diff --git a/src/modules/simulationRenderer/helpers/deleteGate.ts b/src/modules/simulationRenderer/helpers/deleteGate.ts new file mode 100644 index 0000000..d42097c --- /dev/null +++ b/src/modules/simulationRenderer/helpers/deleteGate.ts @@ -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) +} diff --git a/src/modules/simulationRenderer/helpers/gatesInSelection.ts b/src/modules/simulationRenderer/helpers/gatesInSelection.ts new file mode 100644 index 0000000..c82d8fa --- /dev/null +++ b/src/modules/simulationRenderer/helpers/gatesInSelection.ts @@ -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 + }) +} diff --git a/src/modules/simulationRenderer/helpers/idIsSelected.ts b/src/modules/simulationRenderer/helpers/idIsSelected.ts new file mode 100644 index 0000000..1f02b3f --- /dev/null +++ b/src/modules/simulationRenderer/helpers/idIsSelected.ts @@ -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) + } +} diff --git a/src/modules/simulationRenderer/helpers/renderGate.ts b/src/modules/simulationRenderer/helpers/renderGate.ts index aad12c9..5f535b2 100644 --- a/src/modules/simulationRenderer/helpers/renderGate.ts +++ b/src/modules/simulationRenderer/helpers/renderGate.ts @@ -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 diff --git a/src/modules/simulationRenderer/helpers/renderSelectedArea.ts b/src/modules/simulationRenderer/helpers/renderSelectedArea.ts new file mode 100644 index 0000000..947f9d4 --- /dev/null +++ b/src/modules/simulationRenderer/helpers/renderSelectedArea.ts @@ -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() + } +} diff --git a/src/modules/simulationRenderer/helpers/renderSimulation.ts b/src/modules/simulationRenderer/helpers/renderSimulation.ts index b77ae65..e55e60e 100644 --- a/src/modules/simulationRenderer/helpers/renderSimulation.ts +++ b/src/modules/simulationRenderer/helpers/renderSimulation.ts @@ -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)) diff --git a/src/modules/simulationRenderer/helpers/scaleCanvas.ts b/src/modules/simulationRenderer/helpers/scaleCanvas.ts index 5c1e08e..b1976bd 100644 --- a/src/modules/simulationRenderer/helpers/scaleCanvas.ts +++ b/src/modules/simulationRenderer/helpers/scaleCanvas.ts @@ -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 diff --git a/src/modules/simulationRenderer/types/Selection.ts b/src/modules/simulationRenderer/types/Selection.ts deleted file mode 100644 index 19e1dbc..0000000 --- a/src/modules/simulationRenderer/types/Selection.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Selection { - id: number - permanent: boolean -} diff --git a/src/modules/simulationRenderer/types/SimulationRendererOptions.ts b/src/modules/simulationRenderer/types/SimulationRendererOptions.ts index 222d204..98c18fe 100644 --- a/src/modules/simulationRenderer/types/SimulationRendererOptions.ts +++ b/src/modules/simulationRenderer/types/SimulationRendererOptions.ts @@ -24,4 +24,8 @@ export interface SimulationRendererOptions { spawning: { spawnOffset: number } + selecting: { + stroke: string + fill: string + } } diff --git a/src/modules/simulationRenderer/types/selectionType.ts b/src/modules/simulationRenderer/types/selectionType.ts new file mode 100644 index 0000000..d105b72 --- /dev/null +++ b/src/modules/simulationRenderer/types/selectionType.ts @@ -0,0 +1 @@ +export type selectionType = 'permanent' | 'temporary'