clipboard manipuation

This commit is contained in:
Matei Adriel 2019-07-28 18:16:52 +03:00
parent 9f1463f410
commit ce325167c2
21 changed files with 291 additions and 26 deletions

View file

@ -12,5 +12,7 @@ body {
.page {
@include page-width();
background-color: $bg;
overflow-y: auto;
}

View file

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

View file

@ -26,3 +26,8 @@ export const icons: IconInterface = {
ic: 'memory'
}
}
/**
* The width of the sidebar
*/
export const sidebarWidth = 240

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { sidebarWidth } from '../../core/components/Sidebar'
import { sidebarWidth } from '../../core/constants'
/**
* Helper to get the width of the canvas

View file

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

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

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

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

View file

@ -9,3 +9,7 @@ export type possibleAction =
| 'select all'
| 'delete selection'
| 'delete simulation'
| 'copy'
| 'paste'
| 'duplicate'
| 'cut'

View file

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

View file

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

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

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