Adds copy and paste
This commit is contained in:
parent
5baf89a513
commit
eccf5f6307
6 changed files with 181 additions and 18 deletions
|
@ -2,7 +2,12 @@ import { getShapeUtils } from 'lib/shape-utils'
|
|||
import state, { useSelector } from 'state'
|
||||
import { Bounds, GroupShape, PageState } from 'types'
|
||||
import { boundsCollide, boundsContain } from 'utils/bounds'
|
||||
import { deepCompareArrays, getPage, screenToWorld } from 'utils/utils'
|
||||
import {
|
||||
deepCompareArrays,
|
||||
getPage,
|
||||
getViewport,
|
||||
screenToWorld,
|
||||
} from 'utils/utils'
|
||||
import Shape from './shape'
|
||||
|
||||
/*
|
||||
|
@ -21,26 +26,13 @@ export default function Page() {
|
|||
const pageState = s.data.pageStates[page.id]
|
||||
|
||||
if (!viewportCache.has(pageState)) {
|
||||
const [minX, minY] = screenToWorld([0, 0], s.data)
|
||||
const [maxX, maxY] = screenToWorld(
|
||||
[window.innerWidth, window.innerHeight],
|
||||
s.data
|
||||
)
|
||||
|
||||
viewportCache.set(pageState, {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
height: maxX - minX,
|
||||
width: maxY - minY,
|
||||
})
|
||||
const viewport = getViewport(s.data)
|
||||
viewportCache.set(pageState, viewport)
|
||||
}
|
||||
|
||||
const viewport = viewportCache.get(pageState)
|
||||
|
||||
return Object.values(page.shapes)
|
||||
.filter((shape) => shape.parentId === page.id)
|
||||
return s.values.currentShapes
|
||||
.filter((shape) => {
|
||||
const shapeBounds = getShapeUtils(shape).getBounds(shape)
|
||||
return (
|
||||
|
@ -48,7 +40,6 @@ export default function Page() {
|
|||
boundsCollide(viewport, shapeBounds)
|
||||
)
|
||||
})
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
}, deepCompareArrays)
|
||||
|
||||
|
|
48
state/clipboard.ts
Normal file
48
state/clipboard.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Data, Shape } from 'types'
|
||||
import state from './state'
|
||||
|
||||
class Clipboard {
|
||||
current: string
|
||||
fallback = false
|
||||
|
||||
copy = (shapes: Shape[], onComplete?: () => void) => {
|
||||
this.current = JSON.stringify({ id: 'tldr', shapes })
|
||||
|
||||
navigator.permissions.query({ name: 'clipboard-write' }).then((result) => {
|
||||
if (result.state == 'granted' || result.state == 'prompt') {
|
||||
navigator.clipboard.writeText(this.current).then(onComplete, () => {
|
||||
console.warn('Error, could not copy to clipboard. Fallback?')
|
||||
this.fallback = true
|
||||
})
|
||||
} else {
|
||||
this.fallback = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
paste = () => {
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then(this.sendPastedTextToState, this.sendPastedTextToState)
|
||||
}
|
||||
|
||||
sendPastedTextToState(text = this.current) {
|
||||
if (text === undefined) return
|
||||
|
||||
try {
|
||||
const clipboardData = JSON.parse(text)
|
||||
state.send('PASTED_SHAPES_FROM_CLIPBOARD', {
|
||||
shapes: clipboardData.shapes,
|
||||
})
|
||||
} catch (e) {
|
||||
// The text wasn't valid JSON, or it wasn't ours, so paste it as a text object
|
||||
state.send('PASTED_TEXT_FROM_CLIPBOARD', { text })
|
||||
}
|
||||
}
|
||||
|
||||
clear = () => {
|
||||
this.current = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export default new Clipboard()
|
|
@ -15,6 +15,7 @@ import move from './move'
|
|||
import moveToPage from './move-to-page'
|
||||
import nudge from './nudge'
|
||||
import rotate from './rotate'
|
||||
import paste from './paste'
|
||||
import rotateCcw from './rotate-ccw'
|
||||
import stretch from './stretch'
|
||||
import style from './style'
|
||||
|
@ -44,6 +45,7 @@ const commands = {
|
|||
move,
|
||||
moveToPage,
|
||||
nudge,
|
||||
paste,
|
||||
resetBounds,
|
||||
rotate,
|
||||
rotateCcw,
|
||||
|
|
82
state/commands/paste.ts
Normal file
82
state/commands/paste.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data, Shape } from 'types'
|
||||
import {
|
||||
getChildIndexAbove,
|
||||
getCommonBounds,
|
||||
getCurrentCamera,
|
||||
getPage,
|
||||
getSelectedIds,
|
||||
getSelectedShapes,
|
||||
getViewport,
|
||||
screenToWorld,
|
||||
setSelectedIds,
|
||||
setToArray,
|
||||
} from 'utils/utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { current } from 'immer'
|
||||
import vec from 'utils/vec'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import state from 'state/state'
|
||||
|
||||
export default function pasteCommand(data: Data, initialShapes: Shape[]) {
|
||||
const { currentPageId } = data
|
||||
|
||||
const center = screenToWorld(
|
||||
[window.innerWidth / 2, window.innerHeight / 2],
|
||||
data
|
||||
)
|
||||
|
||||
const bounds = getCommonBounds(
|
||||
...initialShapes.map((shape) =>
|
||||
getShapeUtils(shape).getRotatedBounds(shape)
|
||||
)
|
||||
)
|
||||
|
||||
const topLeft = vec.sub(center, [bounds.width / 2, bounds.height / 2])
|
||||
|
||||
const newIdMap = Object.fromEntries(
|
||||
initialShapes.map((shape) => [shape.id, uniqueId()])
|
||||
)
|
||||
|
||||
const oldSelectedIds = setToArray(getSelectedIds(data))
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: 'pasting_new_shapes',
|
||||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
|
||||
let childIndex =
|
||||
(state.values.currentShapes[state.values.currentShapes.length - 1]
|
||||
?.childIndex || 0) + 1
|
||||
|
||||
for (const shape of initialShapes) {
|
||||
const topLeftOffset = vec.sub(shape.point, [bounds.minX, bounds.minY])
|
||||
|
||||
const newId = newIdMap[shape.id]
|
||||
|
||||
shapes[newId] = {
|
||||
...shape,
|
||||
id: newId,
|
||||
parentId: oldSelectedIds[shape.parentId] || data.currentPageId,
|
||||
childIndex: childIndex++,
|
||||
point: vec.add(topLeft, topLeftOffset),
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedIds(data, Object.values(newIdMap))
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
|
||||
Object.values(newIdMap).forEach((id) => delete shapes[id])
|
||||
|
||||
setSelectedIds(data, oldSelectedIds)
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
|
@ -5,6 +5,7 @@ import vec from 'utils/vec'
|
|||
import inputs from './inputs'
|
||||
import history from './history'
|
||||
import storage from './storage'
|
||||
import clipboard from './clipboard'
|
||||
import * as Sessions from './sessions'
|
||||
import commands from './commands'
|
||||
import {
|
||||
|
@ -133,6 +134,9 @@ const state = createState({
|
|||
else: ['zoomCameraToFit', 'zoomCameraToActual'],
|
||||
},
|
||||
on: {
|
||||
COPIED: { if: 'hasSelection', do: 'copyToClipboard' },
|
||||
PASTED: { do: 'pasteFromClipboard' },
|
||||
PASTED_SHAPES_FROM_CLIPBOARD: 'pasteShapesFromClipboard',
|
||||
LOADED_FONTS: 'resetShapes',
|
||||
TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
|
||||
TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
|
||||
|
@ -1657,6 +1661,18 @@ const state = createState({
|
|||
|
||||
/* ---------------------- Data ---------------------- */
|
||||
|
||||
copyToClipboard(data) {
|
||||
clipboard.copy(getSelectedShapes(data))
|
||||
},
|
||||
|
||||
pasteFromClipboard(data) {
|
||||
clipboard.paste()
|
||||
},
|
||||
|
||||
pasteShapesFromClipboard(data, payload: { shapes: Shape[] }) {
|
||||
commands.paste(data, payload.shapes)
|
||||
},
|
||||
|
||||
restoreSavedData(data) {
|
||||
storage.firstLoad(data)
|
||||
},
|
||||
|
@ -1705,6 +1721,13 @@ const state = createState({
|
|||
selectedBounds(data) {
|
||||
return getSelectionBounds(data)
|
||||
},
|
||||
currentShapes(data) {
|
||||
const page = getPage(data)
|
||||
|
||||
return Object.values(page.shapes)
|
||||
.filter((shape) => shape.parentId === page.id)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
},
|
||||
selectedStyle(data) {
|
||||
const selectedIds = Array.from(getSelectedIds(data).values())
|
||||
const { currentStyle } = data
|
||||
|
|
|
@ -15,6 +15,23 @@ export function screenToWorld(point: number[], data: Data) {
|
|||
return vec.sub(vec.div(point, camera.zoom), camera.point)
|
||||
}
|
||||
|
||||
export function getViewport(data: Data): Bounds {
|
||||
const [minX, minY] = screenToWorld([0, 0], data)
|
||||
const [maxX, maxY] = screenToWorld(
|
||||
[window.innerWidth, window.innerHeight],
|
||||
data
|
||||
)
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
height: maxX - minX,
|
||||
width: maxY - minY,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bounding box that includes two bounding boxes.
|
||||
* @param a Bounding box
|
||||
|
|
Loading…
Reference in a new issue