clipboard manipuation
This commit is contained in:
parent
9f1463f410
commit
ce325167c2
|
@ -12,5 +12,7 @@ body {
|
|||
|
||||
.page {
|
||||
@include page-width();
|
||||
|
||||
background-color: $bg;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -26,3 +26,8 @@ export const icons: IconInterface = {
|
|||
ic: 'memory'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The width of the sidebar
|
||||
*/
|
||||
export const sidebarWidth = 240
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 =>
|
||||
|
|
0
src/modules/logic-gates/components/GatePreview.tsx
Normal file
0
src/modules/logic-gates/components/GatePreview.tsx
Normal file
|
@ -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;
|
||||
|
|
|
@ -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' ? (
|
||||
<img src={template.material.fill} alt={template.metadata.name} />
|
||||
<img src={fill} alt={template.metadata.name} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: template.material.fill
|
||||
backgroundColor: fill,
|
||||
backgroundImage: `linear-gradient(-60deg,${[
|
||||
...Object.values(template.material.colors)
|
||||
.map(color => repeat(color, gradientSmoothness))
|
||||
.flat(),
|
||||
...repeat(fill, gradientSmoothness)
|
||||
].join(',')})`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
@ -27,7 +39,12 @@ const LogicGate = ({ template }: LogicGateProps) => {
|
|||
return (
|
||||
<div className="gate">
|
||||
<section>
|
||||
<div className="gate-preview">{gatePreview}</div>
|
||||
<div
|
||||
className="gate-preview"
|
||||
onClick={() => addGateFromTemplate(template)}
|
||||
>
|
||||
{gatePreview}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="gate-name">{name}</div>
|
||||
|
|
|
@ -201,6 +201,48 @@ export const baseTemplates: DeepPartial<GateTemplate>[] = [
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { sidebarWidth } from '../../core/components/Sidebar'
|
||||
import { sidebarWidth } from '../../core/constants'
|
||||
|
||||
/**
|
||||
* Helper to get the width of the canvas
|
||||
|
|
|
@ -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<possibleAction, string> = {
|
||||
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<possibleAction, SidebarAction> = {
|
|||
},
|
||||
['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'])
|
||||
}
|
||||
|
|
57
src/modules/simulation-actions/helpers/copy.ts
Normal file
57
src/modules/simulation-actions/helpers/copy.ts
Normal file
|
@ -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()
|
||||
}
|
13
src/modules/simulation-actions/helpers/duplicate.ts
Normal file
13
src/modules/simulation-actions/helpers/duplicate.ts
Normal file
|
@ -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
|
||||
}
|
46
src/modules/simulation-actions/helpers/paste.ts
Normal file
46
src/modules/simulation-actions/helpers/paste.ts
Normal file
|
@ -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)
|
||||
}
|
|
@ -9,3 +9,7 @@ export type possibleAction =
|
|||
| 'select all'
|
||||
| 'delete selection'
|
||||
| 'delete simulation'
|
||||
| 'copy'
|
||||
| 'paste'
|
||||
| 'duplicate'
|
||||
| 'cut'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<MouseEventInfo>()
|
||||
|
@ -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<HTMLCanvasElement>) {
|
||||
if (ref.current) {
|
||||
ref.current.addEventListener('wheel', event => {
|
||||
if (!modalIsOpen()) {
|
||||
if (!modalIsOpen() && location.pathname === '/') {
|
||||
event.preventDefault()
|
||||
|
||||
handleScroll(event, this.camera)
|
||||
|
|
9
src/modules/simulationRenderer/types/GateInitter.ts
Normal file
9
src/modules/simulationRenderer/types/GateInitter.ts
Normal file
|
@ -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
|
||||
}
|
8
src/modules/vector2/helpers/repeat.ts
Normal file
8
src/modules/vector2/helpers/repeat.ts
Normal file
|
@ -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 = <T>(element: T, count = 1): T[] =>
|
||||
[...Array(count)].fill(element)
|
Loading…
Reference in a new issue