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 { vector2 } from '../../../common/math/types/vector2' import { relativeTo, add, invert } from '../../vector2/helpers/basic' import { SimulationRendererOptions } from '../types/SimulationRendererOptions' import { defaultSimulationRendererOptions, mouseButtons, shiftInput } from '../constants' import { getPinPosition } from '../helpers/pinPosition' import { pointInCircle } from '../../../common/math/helpers/pointInCircle' import { SelectedPins } from '../types/SelectedPins' import { Wire } from '../../simulation/classes/Wire' import { currentStore } from '../../saving/stores/currentStore' import { saveStore } from '../../saving/stores/saveStore' import { fromSimulationState, fromCameraState } from '../../saving/helpers/fromState' import merge from 'deepmerge' import { handleScroll } from '../helpers/scaleCanvas' import { RefObject } from 'react' import { dumpSimulation } from '../../saving/helpers/dumpSimulation' import { modalIsOpen } from '../../modals/helpers/modalIsOpen' import { SimulationError } from '../../errors/classes/SimulationError' import { deleteWire } from '../../simulation/helpers/deleteWire' import { RendererState, WireState } from '../../saving/types/SimulationSave' import { setToArray } from '../../../common/lang/arrays/helpers/setToArray' import { Transform } from '../../../common/math/classes/Transform' import { gatesInSelection } from '../helpers/gatesInSelection' import { selectionType } from '../types/selectionType' import { addIdToSelection, idIsSelected } from '../helpers/idIsSelected' import { GateInitter } from '../types/GateInitter' import { open as propsAreOpen, id as propsModalId } from '../../logic-gates/subjects/LogicGatePropsSubjects' export class SimulationRenderer { public mouseDownOutput = new Subject<MouseEventInfo>() public mouseUpOutput = new Subject<MouseEventInfo>() public mouseMoveOutput = new Subject<MouseEventInfo>() public wheelOutput = new Subject<unknown>() public selectedGates: Record<selectionType, Set<number>> = { temporary: new Set<number>(), permanent: new Set<number>() } public options: SimulationRendererOptions public camera = new Camera() public selectedArea = new Transform() public clipboard: GateInitter[] = [] public wireClipboard: WireState[] = [] // first bit = dragging // second bit = panning // third bit = selecting public mouseState = 0b000 public lastMousePosition: vector2 = [0, 0] // this is used for spawning gates public spawnCount = 0 public selectedPins: SelectedPins = { start: null, end: null } public constructor( options: Partial<SimulationRendererOptions> = {}, public simulation = new Simulation('project', 'default') ) { this.options = merge(defaultSimulationRendererOptions, options) this.init() } public init() { this.mouseDownOutput.subscribe(event => { const worldPosition = this.camera.toWordPostition(event.position) const gates = Array.from(this.simulation.gates) this.lastMousePosition = worldPosition // We need to iterate from the last to the first // because if we have 2 overlapping gates, // we want to select the one on top for (let index = gates.length - 1; index >= 0; index--) { const { transform, id, pins, template } = gates[index] if (pointInSquare(worldPosition, transform)) { if (event.button === mouseButtons.drag) { gates[index].onClick() this.mouseState |= 1 if (!idIsSelected(this, id)) { this.clearSelection() addIdToSelection(this, 'temporary', id) } const gateNode = this.simulation.gates.get(id) if (gateNode) { return this.simulation.gates.moveOnTop(gateNode) } else { throw new SimulationError( `Cannot find gate with id ${id}` ) } } else if ( event.button === mouseButtons.properties && template.properties.enabled ) { propsModalId.next(id) return propsAreOpen.next(true) } } for (const pin of pins) { const position = getPinPosition(this, transform, pin) if ( pointInCircle( worldPosition, position, this.options.gates.pinRadius ) ) { if (pin.value.pairs.size) { if (pin.value.type & 1) { const wire = this.simulation.wires.find( wire => wire.end.value === pin.value ) if (wire) { deleteWire(this.simulation, wire) } else { throw new SimulationError( `Cannot find wire to remove` ) } return } } if ( this.selectedPins.start && pin.value === this.selectedPins.start.wrapper.value ) { this.selectedPins.start = null this.selectedPins.end = null } else if ( this.selectedPins.end && pin.value === this.selectedPins.end.wrapper.value ) { this.selectedPins.start = null this.selectedPins.end = null } else if ((pin.value.type & 2) >> 1) { this.selectedPins.start = { wrapper: pin, transform } } else if (pin.value.type & 1) { this.selectedPins.end = { wrapper: pin, transform } } if (this.selectedPins.start && this.selectedPins.end) { this.simulation.wires.push( new Wire( this.selectedPins.start.wrapper, this.selectedPins.end.wrapper ) ) this.selectedPins.start = null this.selectedPins.end = null } return } } } if (!shiftInput.value && 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(event => { if (event.button === mouseButtons.drag && this.mouseState & 1) { const selected = this.getSelected() for (const gate of selected) { gate.transform.rotation = 0 } this.selectedGates.temporary.clear() // turn first bit to 0 this.mouseState &= 6 // for debugging if ((this.mouseState >> 1) & 1 || this.mouseState & 1) { throw new SimulationError( 'First 2 bits of mouseState need to be set to 0' ) } } if ( event.button === mouseButtons.pan && (this.mouseState >> 1) & 1 ) { // turn second bit to 0 this.mouseState &= 5 } if (event.button === mouseButtons.select && this.mouseState >> 2) { // turn the third bit to 0 this.mouseState &= 3 const selectedGates = gatesInSelection( this.selectedArea, Array.from(this.simulation.gates) ) for (const { id } of selectedGates) { addIdToSelection(this, 'permanent', id) const node = this.simulation.gates.get(id) if (node) { this.simulation.gates.moveOnTop(node) } else { throw new SimulationError( `Cannot find node in gate storage with id ${id}` ) } } } }) this.mouseMoveOutput.subscribe(event => { const worldPosition = this.camera.toWordPostition(event.position) const offset = invert( relativeTo(this.lastMousePosition, worldPosition) ) const scaledOffset = offset.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(scaledOffset) ) 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) }) this.reloadSave() } public updateWheelListener(ref: RefObject<HTMLCanvasElement>) { if (ref.current) { ref.current.addEventListener('wheel', event => { if (!modalIsOpen() && location.pathname === '/') { event.preventDefault() handleScroll(event, this.camera) } }) } } public loadSave(save: RendererState) { this.simulation = fromSimulationState(save.simulation) this.camera = fromCameraState(save.camera) } public reloadSave() { dumpSimulation(this) const current = currentStore.get() const save = saveStore.get(current) if (!save) return if (!(save.simulation || save.camera)) return this.loadSave(save) } /** * Gets all selected gates in the simulation * * @throws SimulationError if an id isnt valid * @throws SimulationError if the id doesnt have a data prop */ public getSelected() { return setToArray(this.allSelectedIds()).map(id => { const gate = this.simulation.gates.get(id) if (!gate) { throw new SimulationError(`Cannot find gate with id ${id}`) } else if (!gate.data) { throw new SimulationError( `Cannot find data of gate with id ${id}` ) } return gate.data }) } /** * 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() } /** * Clears the selected pins of the renderer */ public clearPinSelection() { this.selectedPins.end = null this.selectedPins.start = null } }