more advanced selection

This commit is contained in:
Matei Adriel 2019-07-25 00:01:50 +03:00
parent a9034559a3
commit 48b967ead8
35 changed files with 419 additions and 215 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,6 @@ export interface Translation {
savedSimulation: NameSentence
compiledIc: NameSentence
refreshed: NameSentence
cleared: NameSentence
cleaned: NameSentence
undone: NameSentence
}

View file

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

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.selectedGates = new Set()
renderer.clearSelection()
renderer.selectedPins = {
end: null,
start: null

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -24,4 +24,8 @@ export interface SimulationRendererOptions {
spawning: {
spawnOffset: number
}
selecting: {
stroke: string
fill: string
}
}

View file

@ -0,0 +1 @@
export type selectionType = 'permanent' | 'temporary'