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' ? (
-
+
) : (
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)