diff --git a/src/modules/core/components/App.scss b/src/modules/core/components/App.scss index 835675a..796021b 100644 --- a/src/modules/core/components/App.scss +++ b/src/modules/core/components/App.scss @@ -12,5 +12,7 @@ body { .page { @include page-width(); + background-color: $bg; + overflow-y: auto; } diff --git a/src/modules/core/components/Sidebar.tsx b/src/modules/core/components/Sidebar.tsx index 21d91b2..eb61f39 100644 --- a/src/modules/core/components/Sidebar.tsx +++ b/src/modules/core/components/Sidebar.tsx @@ -9,10 +9,7 @@ import Language from './Language' import SimulationActions from '../../simulation-actions/components/SimulationActions' import { Route, Switch } from 'react-router' import BackToSimulation from './BackToSimulation' -/** - * The width of the sidebar - */ -export const sidebarWidth = 240 +import { sidebarWidth } from '../constants' /** * The z-index of the sidebar. diff --git a/src/modules/core/constants.ts b/src/modules/core/constants.ts index 5174c13..97ebc68 100644 --- a/src/modules/core/constants.ts +++ b/src/modules/core/constants.ts @@ -26,3 +26,8 @@ export const icons: IconInterface = { ic: 'memory' } } + +/** + * The width of the sidebar + */ +export const sidebarWidth = 240 diff --git a/src/modules/internalisation/translations/english.ts b/src/modules/internalisation/translations/english.ts index 72abb80..b4da4b9 100644 --- a/src/modules/internalisation/translations/english.ts +++ b/src/modules/internalisation/translations/english.ts @@ -30,6 +30,10 @@ export const EnglishTranslation: Translation = { clean: 'Clean', refresh: 'Refresh', undo: 'Undo', + paste: 'Paste', + copy: 'Copy', + duplicate: 'Duplicate', + cut: 'Cut', 'select all': 'Select all', 'delete selection': 'Delete selection', 'delete simulation': 'Delete simulation' diff --git a/src/modules/internalisation/translations/nederlands.ts b/src/modules/internalisation/translations/nederlands.ts index 9995ae6..c2ce593 100644 --- a/src/modules/internalisation/translations/nederlands.ts +++ b/src/modules/internalisation/translations/nederlands.ts @@ -20,7 +20,11 @@ export const DutchTranslation: Translation = { refresh: 'Todo', save: 'Todo', undo: 'Todo', - 'delete simulation': `Todo` + 'delete simulation': `Todo`, + copy: 'Todo', + cut: 'Todo', + duplicate: 'Todo', + paste: 'Todo' }, createSimulation: { mode: { @@ -44,6 +48,7 @@ export const DutchTranslation: Translation = { cleaned: name => `${name} gewist`, refreshed: name => `${name} ververst`, undone: name => `${name} ongedaan gemaakt`, - deletedSimulation: name => `Todo` + deletedSimulation: name => `Todo`, + addedGate: name => 'Todo' } } diff --git a/src/modules/internalisation/translations/romanian.ts b/src/modules/internalisation/translations/romanian.ts index b258c9e..1640002 100644 --- a/src/modules/internalisation/translations/romanian.ts +++ b/src/modules/internalisation/translations/romanian.ts @@ -32,7 +32,11 @@ export const RomanianTranslation: Translation = { clean: 'Curăță', refresh: 'Reîncarcă', undo: 'Întoarce', - 'delete simulation': 'Șterge simulația' + 'delete simulation': 'Șterge simulația', + copy: 'Copiază', + paste: 'Lipește', + cut: 'Taie', + duplicate: 'Clonează' }, messages: { createdSimulation: name => diff --git a/src/modules/logic-gates/components/GatePreview.tsx b/src/modules/logic-gates/components/GatePreview.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/logic-gates/components/LogicGate.scss b/src/modules/logic-gates/components/LogicGate.scss index dac8ff6..51d1da9 100644 --- a/src/modules/logic-gates/components/LogicGate.scss +++ b/src/modules/logic-gates/components/LogicGate.scss @@ -2,18 +2,19 @@ $gate-margin: 1em; -.gate > section > .gate-preview > * { - width: 100%; -} - .gate > section > .gate-preview > * { display: block; height: 10em; width: 10em; + border-radius: 1em; margin: $gate-margin; } +.gate:hover { + border: 2px solid white !important; +} + .gate > section > .gate-name { width: 100%; text-align: center; diff --git a/src/modules/logic-gates/components/LogicGate.tsx b/src/modules/logic-gates/components/LogicGate.tsx index 9ae0dd8..a3d79bf 100644 --- a/src/modules/logic-gates/components/LogicGate.tsx +++ b/src/modules/logic-gates/components/LogicGate.tsx @@ -4,19 +4,31 @@ import { GateTemplate } from '../../simulation/types/GateTemplate' import GateInfo from './GateInfo' import GateSettings from './GateSettings' import AddGate from './AddGate' +import { addGateFromTemplate } from '../helpers/addGateFromTemplate' +import { repeat } from '../../vector2/helpers/repeat' export interface LogicGateProps { template: GateTemplate } +const gradientSmoothness = 10 + const LogicGate = ({ template }: LogicGateProps) => { + const { fill } = template.material + const gatePreview = template.material.type === 'image' ? ( - {template.metadata.name} + {template.metadata.name} ) : (
repeat(color, gradientSmoothness)) + .flat(), + ...repeat(fill, gradientSmoothness) + ].join(',')})` }} /> ) @@ -27,7 +39,12 @@ const LogicGate = ({ template }: LogicGateProps) => { return (
-
{gatePreview}
+
addGateFromTemplate(template)} + > + {gatePreview} +
{name}
diff --git a/src/modules/saving/constants.ts b/src/modules/saving/constants.ts index e258fbd..126d674 100644 --- a/src/modules/saving/constants.ts +++ b/src/modules/saving/constants.ts @@ -201,6 +201,48 @@ export const baseTemplates: DeepPartial[] = [ count: 0 } } + }, + { + metadata: { + name: 'rgb light' + }, + material: { + fill: '#1C1C1C', + colors: { + 1: '#00f', + 2: `#0f0`, + 3: `#0ff`, + 4: `#f00`, + 5: `#f0f`, + 6: `#ff0`, + 7: `#fff` + } + }, + code: { + activation: ` + const color = (context.get(0) << 2) + (context.get(1) << 1) + context.get(2) + + if (color === 0){ + context.color(context.colors.main) + } + + else{ + context.color(context.colors[color]) + } + ` + }, + integration: { + output: true + }, + info: ['https://en.wikipedia.org/wiki/Incandescent_light_bulb'], + pins: { + outputs: { + count: 0 + }, + inputs: { + count: 3 + } + } } ] diff --git a/src/modules/screen/helpers/Screen.ts b/src/modules/screen/helpers/Screen.ts index 66c8711..1dd17f4 100644 --- a/src/modules/screen/helpers/Screen.ts +++ b/src/modules/screen/helpers/Screen.ts @@ -2,6 +2,7 @@ import { Transform } from '../../../common/math/classes/Transform' import { BehaviorSubject, fromEvent } from 'rxjs' import { map } from 'rxjs/operators' import { getWidth } from '../helpers/getWidth' +import { sidebarWidth } from '../../core/constants' const width = new BehaviorSubject(getWidth()) const height = new BehaviorSubject(window.innerHeight) diff --git a/src/modules/screen/helpers/getWidth.ts b/src/modules/screen/helpers/getWidth.ts index 7019456..598a715 100644 --- a/src/modules/screen/helpers/getWidth.ts +++ b/src/modules/screen/helpers/getWidth.ts @@ -1,4 +1,4 @@ -import { sidebarWidth } from '../../core/components/Sidebar' +import { sidebarWidth } from '../../core/constants' /** * Helper to get the width of the canvas diff --git a/src/modules/simulation-actions/constants.ts b/src/modules/simulation-actions/constants.ts index 7d0e742..5f12ad9 100644 --- a/src/modules/simulation-actions/constants.ts +++ b/src/modules/simulation-actions/constants.ts @@ -8,12 +8,19 @@ import { selectAll } from './helpers/selectAll' import { deleteSelection } from './helpers/deleteSelection' import { cleanRenderer } from './helpers/clean' import { deleteSimulation } from './helpers/deleteSimulation' +import { copy, cut } from './helpers/copy' +import { paste } from './helpers/paste' +import { duplicate } from './helpers/duplicate' export const actionIcons: Record = { clean: 'clear', refresh: 'refresh', save: 'save', undo: 'undo', + copy: 'file_copy', + cut: 'file_copy', + paste: 'unarchive', + duplicate: 'view_module', 'select all': 'select_all', 'delete selection': 'delete', 'delete simulation': 'delete_forever' @@ -52,6 +59,10 @@ export const SidebarActions: Record = { }, ['ctrl', 'shift', 'delete'] ), + ...createActionConfig('cut', cut, ['ctrl', 'x']), + ...createActionConfig('paste', paste, ['ctrl', 'v']), + ...createActionConfig('duplicate', duplicate, ['ctrl', 'd']), + ...createActionConfig('copy', copy, ['ctrl', 'c']), ...createActionConfig('select all', selectAll, ['ctrl', 'a']), ...createActionConfig('delete selection', deleteSelection, ['delete']) } diff --git a/src/modules/simulation-actions/helpers/copy.ts b/src/modules/simulation-actions/helpers/copy.ts new file mode 100644 index 0000000..3380510 --- /dev/null +++ b/src/modules/simulation-actions/helpers/copy.ts @@ -0,0 +1,57 @@ +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' +import { idStore } from '../../simulation/stores/idStore' +import { deleteGate } from '../../simulationRenderer/helpers/deleteGate' + +/** + * Helper to copy the selection of a renderer + * + * @param renderer The renderer to copy the selection of + */ +export const copy = (renderer: SimulationRenderer) => { + const selected = renderer.getSelected() + + renderer.wireClipboard = [] + renderer.clipboard = selected.map(gate => ({ + name: gate.template.metadata.name, + position: gate.transform.position + })) + + for (const wire of renderer.simulation.wires) { + const start = selected.find(gate => gate === wire.start.value.gate) + const end = selected.find(gate => gate === wire.end.value.gate) + + if (start && end) { + const startIndex = selected.indexOf(start) + const endIndex = selected.indexOf(end) + + renderer.wireClipboard.push({ + id: idStore.generate(), + from: { + id: startIndex, + total: wire.start.total, + index: wire.start.index + }, + to: { + id: endIndex, + total: wire.end.total, + index: wire.end.index + } + }) + } + } +} + +/** + * Same as copy but deletes the selected gates + * + * @param renderer The renderer to cut the selected gates of + */ +export const cut = (renderer: SimulationRenderer) => { + copy(renderer) + + for (const gate of renderer.getSelected()) { + deleteGate(renderer.simulation, gate, renderer) + } + + renderer.clearSelection() +} diff --git a/src/modules/simulation-actions/helpers/duplicate.ts b/src/modules/simulation-actions/helpers/duplicate.ts new file mode 100644 index 0000000..cc70d73 --- /dev/null +++ b/src/modules/simulation-actions/helpers/duplicate.ts @@ -0,0 +1,13 @@ +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' +import { copy } from './copy' +import { paste } from './paste' + +export const duplicate = (renderer: SimulationRenderer) => { + const { clipboard, wireClipboard } = renderer + + copy(renderer) + paste(renderer) + + renderer.clipboard = clipboard + renderer.wireClipboard = wireClipboard +} diff --git a/src/modules/simulation-actions/helpers/paste.ts b/src/modules/simulation-actions/helpers/paste.ts new file mode 100644 index 0000000..e643a63 --- /dev/null +++ b/src/modules/simulation-actions/helpers/paste.ts @@ -0,0 +1,46 @@ +import { SimulationRenderer } from '../../simulationRenderer/classes/SimulationRenderer' +import { instantiateGateInitter } from '../../simulation/helpers/addGate' +import { Wire } from '../../simulation/classes/Wire' + +/** + * Pastes the content of the clipboard + * + * @param renderer The renderer to use + */ +export const paste = (renderer: SimulationRenderer) => { + const { clipboard, wireClipboard } = renderer + + const ids: number[] = [] + + for (const initter of clipboard) { + ids.push(instantiateGateInitter(renderer, initter, false)) + } + + for (const wire of wireClipboard) { + const start = renderer.simulation.gates.get(ids[wire.from.id]) + const end = renderer.simulation.gates.get(ids[wire.to.id]) + + if (start && end && start.data && end.data) { + const startPin = start.data._pins.outputs[wire.from.index] + const endPin = end.data._pins.inputs[wire.to.index] + + renderer.simulation.wires.push( + new Wire( + { + value: startPin, + index: wire.from.index, + total: wire.from.total + }, + { + value: endPin, + index: wire.to.index, + total: wire.to.total + } + ) + ) + } + } + + renderer.clearSelection() + renderer.selectedGates.permanent = new Set(ids) +} diff --git a/src/modules/simulation-actions/types/possibleAction.ts b/src/modules/simulation-actions/types/possibleAction.ts index f2dd0c9..aa23806 100644 --- a/src/modules/simulation-actions/types/possibleAction.ts +++ b/src/modules/simulation-actions/types/possibleAction.ts @@ -9,3 +9,7 @@ export type possibleAction = | 'select all' | 'delete selection' | 'delete simulation' + | 'copy' + | 'paste' + | 'duplicate' + | 'cut' diff --git a/src/modules/simulation/helpers/addGate.ts b/src/modules/simulation/helpers/addGate.ts index 732688a..775a041 100644 --- a/src/modules/simulation/helpers/addGate.ts +++ b/src/modules/simulation/helpers/addGate.ts @@ -9,10 +9,20 @@ import { Screen } from '../../screen/helpers/Screen' import { toast } from 'react-toastify' import { createToastArguments } from '../../toasts/helpers/createToastArguments' import { CurrentLanguage } from '../../internalisation/stores/currentLanguage' +import { GateInitter } from '../../simulationRenderer/types/GateInitter' -export const addGate = (renderer: SimulationRenderer, templateName: string) => { +/** + * Adds a gate to a renderer + * + * @param renderer The renderer to add the gate to + * @param templateName The name of the template to add + */ +export const addGate = ( + renderer: SimulationRenderer, + templateName: string, + log = true +) => { const template = templateStore.get(templateName) - const translation = CurrentLanguage.getTranslation() if (!template) throw new SimulationError(`Cannot find template ${templateName}`) @@ -37,10 +47,38 @@ export const addGate = (renderer: SimulationRenderer, templateName: string) => { renderer.simulation.push(gate) renderer.spawnCount++ - toast( - ...createToastArguments( - translation.messages.addedGate(templateName), - 'add_circle_outline' + if (log) { + const translation = CurrentLanguage.getTranslation() + + toast( + ...createToastArguments( + translation.messages.addedGate(templateName), + 'add_circle_outline' + ) ) - ) + } + + return gate.id +} + +/** + * Adds a gate to a renderer and sets its position + * + * @param renderer The renderer to add the gate to + * @param initter The initter to use + */ +export const instantiateGateInitter = ( + renderer: SimulationRenderer, + initter: GateInitter, + log = true +) => { + const id = addGate(renderer, initter.name, log) + + const gate = renderer.simulation.gates.get(id) + + if (gate && gate.data) { + gate.data.transform.position = initter.position + } + + return id } diff --git a/src/modules/simulationRenderer/classes/SimulationRenderer.ts b/src/modules/simulationRenderer/classes/SimulationRenderer.ts index 03404d1..dd719f6 100644 --- a/src/modules/simulationRenderer/classes/SimulationRenderer.ts +++ b/src/modules/simulationRenderer/classes/SimulationRenderer.ts @@ -28,12 +28,13 @@ import { dumpSimulation } from '../../saving/helpers/dumpSimulation' import { modalIsOpen } from '../../modals/helpers/modalIsOpen' import { SimulationError } from '../../errors/classes/SimulationError' import { deleteWire } from '../../simulation/helpers/deleteWire' -import { RendererState } from '../../saving/types/SimulationSave' +import { 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' export class SimulationRenderer { public mouseDownOutput = new Subject() @@ -50,6 +51,8 @@ export class SimulationRenderer { public camera = new Camera() public selectedArea = new Transform() + public clipboard: GateInitter[] = [] + public wireClipboard: WireState[] = [] // first bit = dragging // second bit = panning around @@ -82,8 +85,6 @@ export class SimulationRenderer { this.lastMousePosition = worldPosition - console.log('click') - // 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 @@ -294,7 +295,7 @@ export class SimulationRenderer { public updateWheelListener(ref: RefObject) { if (ref.current) { ref.current.addEventListener('wheel', event => { - if (!modalIsOpen()) { + if (!modalIsOpen() && location.pathname === '/') { event.preventDefault() handleScroll(event, this.camera) diff --git a/src/modules/simulationRenderer/types/GateInitter.ts b/src/modules/simulationRenderer/types/GateInitter.ts new file mode 100644 index 0000000..c58e888 --- /dev/null +++ b/src/modules/simulationRenderer/types/GateInitter.ts @@ -0,0 +1,9 @@ +import { vector2 } from '../../../common/math/classes/Transform' + +/** + * Used to init a gate at a certain position + */ +export interface GateInitter { + name: string + position: vector2 +} diff --git a/src/modules/vector2/helpers/repeat.ts b/src/modules/vector2/helpers/repeat.ts new file mode 100644 index 0000000..120f544 --- /dev/null +++ b/src/modules/vector2/helpers/repeat.ts @@ -0,0 +1,8 @@ +/** + * Repeats an element a number of times + * + * @param element The element to repeat a number of times + * @param count The number of times to repeat the element + */ +export const repeat = (element: T, count = 1): T[] => + [...Array(count)].fill(element)