Moves selectedIds into page state, state mounts only one page state / page at a time
This commit is contained in:
parent
45fd645885
commit
350c1debde
34 changed files with 445 additions and 249 deletions
|
@ -6,6 +6,7 @@ import {
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getCurrentCamera,
|
getCurrentCamera,
|
||||||
getPage,
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
isMobile,
|
isMobile,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
|
@ -28,7 +29,7 @@ export default function Bounds() {
|
||||||
)
|
)
|
||||||
|
|
||||||
const rotation = useSelector(({ data }) =>
|
const rotation = useSelector(({ data }) =>
|
||||||
data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
|
getSelectedIds(data).size === 1 ? getSelectedShapes(data)[0].rotation : 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const isAllLocked = useSelector((s) => {
|
const isAllLocked = useSelector((s) => {
|
||||||
|
|
|
@ -43,7 +43,6 @@ export default function Page() {
|
||||||
.filter((shape) => shape.parentId === page.id)
|
.filter((shape) => shape.parentId === page.id)
|
||||||
// .filter((shape) => {
|
// .filter((shape) => {
|
||||||
// const shapeBounds = getShapeUtils(shape).getBounds(shape)
|
// const shapeBounds = getShapeUtils(shape).getBounds(shape)
|
||||||
// console.log(shapeBounds, viewport)
|
|
||||||
// return boundsContain(viewport, shapeBounds)
|
// return boundsContain(viewport, shapeBounds)
|
||||||
// })
|
// })
|
||||||
.sort((a, b) => a.childIndex - b.childIndex)
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import { deepCompareArrays, getBoundsCenter, getPage } from 'utils/utils'
|
import {
|
||||||
|
deepCompareArrays,
|
||||||
|
getBoundsCenter,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
setToArray,
|
||||||
|
} from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import useShapeEvents from 'hooks/useShapeEvents'
|
import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
import { memo, useRef } from 'react'
|
import { memo, useRef } from 'react'
|
||||||
|
@ -8,9 +14,10 @@ import { ShapeType } from 'types'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
|
|
||||||
export default function Selected() {
|
export default function Selected() {
|
||||||
const currentSelectedShapeIds = useSelector(({ data }) => {
|
const currentSelectedShapeIds = useSelector(
|
||||||
return Array.from(data.selectedIds.values())
|
({ data }) => setToArray(getSelectedIds(data)),
|
||||||
}, deepCompareArrays)
|
deepCompareArrays
|
||||||
|
)
|
||||||
|
|
||||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default function PagePanel() {
|
||||||
value={currentPageId}
|
value={currentPageId}
|
||||||
onValueChange={(id) => {
|
onValueChange={(id) => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
state.send('CHANGED_CURRENT_PAGE', { id })
|
state.send('CHANGED_PAGE', { id })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sorted.map(({ id, name }) => (
|
{sorted.map(({ id, name }) => (
|
||||||
|
|
|
@ -4,7 +4,13 @@ import * as Panel from 'components/panel'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { IconButton } from 'components/shared'
|
import { IconButton } from 'components/shared'
|
||||||
import { ChevronDown, Trash2, X } from 'react-feather'
|
import { ChevronDown, Trash2, X } from 'react-feather'
|
||||||
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
|
import {
|
||||||
|
deepCompare,
|
||||||
|
deepCompareArrays,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
setToArray,
|
||||||
|
} from 'utils/utils'
|
||||||
import AlignDistribute from './align-distribute'
|
import AlignDistribute from './align-distribute'
|
||||||
import { MoveType } from 'types'
|
import { MoveType } from 'types'
|
||||||
import SizePicker from './size-picker'
|
import SizePicker from './size-picker'
|
||||||
|
@ -65,7 +71,7 @@ export default function StylePanel() {
|
||||||
|
|
||||||
function SelectedShapeStyles() {
|
function SelectedShapeStyles() {
|
||||||
const selectedIds = useSelector(
|
const selectedIds = useSelector(
|
||||||
(s) => Array.from(s.data.selectedIds.values()),
|
(s) => setToArray(getSelectedIds(s.data)),
|
||||||
deepCompareArrays
|
deepCompareArrays
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,10 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
if (shape.rotation === 0) {
|
if (shape.rotation === 0) {
|
||||||
return (
|
return (
|
||||||
boundsContain(brushBounds, this.getBounds(shape)) ||
|
boundsContain(brushBounds, this.getBounds(shape)) ||
|
||||||
intersectPolylineBounds(shape.points, brushBounds).length > 0
|
intersectPolylineBounds(
|
||||||
|
shape.points,
|
||||||
|
translateBounds(brushBounds, vec.neg(shape.point))
|
||||||
|
).length > 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,18 +107,19 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
const rBounds = this.getRotatedBounds(shape)
|
const rBounds = this.getRotatedBounds(shape)
|
||||||
|
|
||||||
if (!rotatedCache.has(shape)) {
|
if (!rotatedCache.has(shape)) {
|
||||||
const c = getBoundsCenter(rBounds)
|
const c = getBoundsCenter(getBoundsFromPoints(shape.points))
|
||||||
rotatedCache.set(
|
rotatedCache.set(
|
||||||
shape,
|
shape,
|
||||||
shape.points.map((pt) =>
|
shape.points.map((pt) => vec.rotWith(pt, c, shape.rotation))
|
||||||
vec.rotWith(vec.add(pt, shape.point), c, shape.rotation)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
boundsContain(brushBounds, rBounds) ||
|
boundsContain(brushBounds, rBounds) ||
|
||||||
intersectPolylineBounds(rotatedCache.get(shape), brushBounds).length > 0
|
intersectPolylineBounds(
|
||||||
|
rotatedCache.get(shape),
|
||||||
|
translateBounds(brushBounds, vec.neg(shape.point))
|
||||||
|
).length > 0
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -152,6 +156,18 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSessionComplete(shape) {
|
||||||
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
|
const [x1, y1] = vec.sub([bounds.minX, bounds.minY], shape.point)
|
||||||
|
|
||||||
|
shape.points = shape.points.map(([x0, y0, p]) => [x0 - x1, y0 - y1, p])
|
||||||
|
|
||||||
|
this.translateTo(shape, vec.add(shape.point, [x1, y1]))
|
||||||
|
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
canStyleFill: false,
|
canStyleFill: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -164,8 +180,8 @@ const simulatePressureSettings = {
|
||||||
const realPressureSettings = {
|
const realPressureSettings = {
|
||||||
easing: (t: number) => t * t,
|
easing: (t: number) => t * t,
|
||||||
simulatePressure: false,
|
simulatePressure: false,
|
||||||
// start: { taper: 1 },
|
start: { taper: 1 },
|
||||||
// end: { taper: 1 },
|
end: { taper: 1 },
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPath(shape: DrawShape, style: ShapeStyles) {
|
function renderPath(shape: DrawShape, style: ShapeStyles) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import { getPage } from 'utils/utils'
|
import { getPage, getSelectedIds } from 'utils/utils'
|
||||||
import { ArrowSnapshot } from 'state/sessions/arrow-session'
|
import { ArrowSnapshot } from 'state/sessions/arrow-session'
|
||||||
|
|
||||||
export default function arrowCommand(
|
export default function arrowCommand(
|
||||||
|
@ -24,8 +24,9 @@ export default function arrowCommand(
|
||||||
|
|
||||||
page.shapes[initialShape.id] = initialShape
|
page.shapes[initialShape.id] = initialShape
|
||||||
|
|
||||||
data.selectedIds.clear()
|
const selectedIds = getSelectedIds(data)
|
||||||
data.selectedIds.add(initialShape.id)
|
selectedIds.clear()
|
||||||
|
selectedIds.add(initialShape.id)
|
||||||
data.hoveredId = undefined
|
data.hoveredId = undefined
|
||||||
data.pointedId = undefined
|
data.pointedId = undefined
|
||||||
},
|
},
|
||||||
|
@ -35,7 +36,8 @@ export default function arrowCommand(
|
||||||
|
|
||||||
delete shapes[initialShape.id]
|
delete shapes[initialShape.id]
|
||||||
|
|
||||||
data.selectedIds.clear()
|
const selectedIds = getSelectedIds(data)
|
||||||
|
selectedIds.clear()
|
||||||
data.hoveredId = undefined
|
data.hoveredId = undefined
|
||||||
data.pointedId = undefined
|
data.pointedId = undefined
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import { getPage, getSelectedShapes } from 'utils/utils'
|
import storage from 'state/storage'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
|
||||||
import * as vec from 'utils/vec'
|
|
||||||
|
|
||||||
export default function changePage(data: Data, pageId: string) {
|
export default function changePage(data: Data, pageId: string) {
|
||||||
const { currentPageId: prevPageId } = data
|
const { currentPageId: prevPageId } = data
|
||||||
|
@ -13,11 +11,15 @@ export default function changePage(data: Data, pageId: string) {
|
||||||
new Command({
|
new Command({
|
||||||
name: 'change_page',
|
name: 'change_page',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
|
storage.savePage(data, data.currentPageId)
|
||||||
data.currentPageId = pageId
|
data.currentPageId = pageId
|
||||||
|
storage.loadPage(data, data.currentPageId)
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
data.currentPageId = prevPageId
|
data.currentPageId = prevPageId
|
||||||
|
storage.loadPage(data, prevPageId)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Data } from "types"
|
import { Data } from 'types'
|
||||||
|
import { getSelectedIds, setSelectedIds, setToArray } from 'utils/utils'
|
||||||
|
|
||||||
/* ------------------ Command Class ----------------- */
|
/* ------------------ Command Class ----------------- */
|
||||||
|
|
||||||
|
@ -52,6 +53,12 @@ export class BaseCommand<T extends any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
redo = (data: T, initial = false) => {
|
redo = (data: T, initial = false) => {
|
||||||
|
if (this.manualSelection) {
|
||||||
|
this.doFn(data, initial)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (initial) {
|
if (initial) {
|
||||||
this.restoreBeforeSelectionState = this.saveSelectionState(data)
|
this.restoreBeforeSelectionState = this.saveSelectionState(data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -76,11 +83,13 @@ export class BaseCommand<T extends any> {
|
||||||
*/
|
*/
|
||||||
export default class Command extends BaseCommand<Data> {
|
export default class Command extends BaseCommand<Data> {
|
||||||
saveSelectionState = (data: Data) => {
|
saveSelectionState = (data: Data) => {
|
||||||
const selectedIds = new Set(data.selectedIds)
|
const { currentPageId } = data
|
||||||
return (data: Data) => {
|
const selectedIds = setToArray(getSelectedIds(data))
|
||||||
data.hoveredId = undefined
|
return (next: Data) => {
|
||||||
data.pointedId = undefined
|
next.currentPageId = currentPageId
|
||||||
data.selectedIds = selectedIds
|
next.hoveredId = undefined
|
||||||
|
next.pointedId = undefined
|
||||||
|
setSelectedIds(next, selectedIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, Page } from 'types'
|
import { Data, Page, PageState } from 'types'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
|
import { getSelectedIds } from 'utils/utils'
|
||||||
|
import storage from 'state/storage'
|
||||||
|
|
||||||
export default function createPage(data: Data) {
|
export default function createPage(data: Data) {
|
||||||
const snapshot = getSnapshot(data)
|
const snapshot = getSnapshot(data)
|
||||||
|
@ -13,14 +15,13 @@ export default function createPage(data: Data) {
|
||||||
name: 'change_page',
|
name: 'change_page',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
data.selectedIds.clear()
|
|
||||||
const { page, pageState } = snapshot
|
const { page, pageState } = snapshot
|
||||||
data.document.pages[page.id] = page
|
data.document.pages[page.id] = page
|
||||||
data.pageStates[page.id] = pageState
|
data.pageStates[page.id] = pageState
|
||||||
data.currentPageId = page.id
|
data.currentPageId = page.id
|
||||||
|
storage.savePage(data, page.id)
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
data.selectedIds.clear()
|
|
||||||
const { page, currentPageId } = snapshot
|
const { page, currentPageId } = snapshot
|
||||||
delete data.document.pages[page.id]
|
delete data.document.pages[page.id]
|
||||||
delete data.pageStates[page.id]
|
delete data.pageStates[page.id]
|
||||||
|
@ -44,7 +45,8 @@ function getSnapshot(data: Data) {
|
||||||
childIndex: pages.length,
|
childIndex: pages.length,
|
||||||
shapes: {},
|
shapes: {},
|
||||||
}
|
}
|
||||||
const pageState = {
|
const pageState: PageState = {
|
||||||
|
selectedIds: new Set<string>(),
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
|
|
|
@ -2,22 +2,30 @@ import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { TranslateSnapshot } from 'state/sessions/translate-session'
|
import { TranslateSnapshot } from 'state/sessions/translate-session'
|
||||||
import { Data, ShapeType } from 'types'
|
import { Data, ShapeType } from 'types'
|
||||||
import { getDocumentBranch, getPage, updateParents } from 'utils/utils'
|
import {
|
||||||
|
getDocumentBranch,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
setSelectedIds,
|
||||||
|
setToArray,
|
||||||
|
updateParents,
|
||||||
|
} from 'utils/utils'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
export default function deleteSelected(data: Data) {
|
export default function deleteSelected(data: Data) {
|
||||||
const { currentPageId } = data
|
const { currentPageId } = data
|
||||||
|
|
||||||
const selectedIds = Array.from(data.selectedIds.values())
|
const selectedIds = getSelectedIds(data)
|
||||||
|
const selectedIdsArr = setToArray(selectedIds)
|
||||||
|
|
||||||
const page = getPage(current(data))
|
const page = getPage(current(data))
|
||||||
|
|
||||||
const childrenToDelete = selectedIds
|
const childrenToDelete = selectedIdsArr
|
||||||
.flatMap((id) => getDocumentBranch(data, id))
|
.flatMap((id) => getDocumentBranch(data, id))
|
||||||
.map((id) => page.shapes[id])
|
.map((id) => page.shapes[id])
|
||||||
|
|
||||||
data.selectedIds.clear()
|
selectedIds.clear()
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
|
@ -28,7 +36,7 @@ export default function deleteSelected(data: Data) {
|
||||||
do(data) {
|
do(data) {
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data, currentPageId)
|
||||||
|
|
||||||
for (let id of selectedIds) {
|
for (let id of selectedIdsArr) {
|
||||||
const shape = page.shapes[id]
|
const shape = page.shapes[id]
|
||||||
if (!shape) {
|
if (!shape) {
|
||||||
console.error('no shape ' + id)
|
console.error('no shape ' + id)
|
||||||
|
@ -54,7 +62,7 @@ export default function deleteSelected(data: Data) {
|
||||||
delete page.shapes[shape.id]
|
delete page.shapes[shape.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [])
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data, currentPageId)
|
||||||
|
@ -75,7 +83,7 @@ export default function deleteSelected(data: Data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.selectedIds = new Set(selectedIds)
|
setSelectedIds(data, selectedIdsArr)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, DrawShape } from 'types'
|
import { Data, DrawShape } from 'types'
|
||||||
import { getPage } from 'utils/utils'
|
import { getPage, setSelectedIds } from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
|
|
||||||
export default function drawCommand(
|
export default function drawCommand(data: Data, id: string) {
|
||||||
data: Data,
|
const restoreShape = getPage(current(data)).shapes[id] as DrawShape
|
||||||
id: string,
|
|
||||||
points: number[][]
|
|
||||||
) {
|
|
||||||
const restoreShape = current(getPage(data)).shapes[id] as DrawShape
|
|
||||||
getShapeUtils(restoreShape).setProperty(restoreShape, 'points', points)
|
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
|
@ -24,11 +18,11 @@ export default function drawCommand(
|
||||||
getPage(data).shapes[id] = restoreShape
|
getPage(data).shapes[id] = restoreShape
|
||||||
}
|
}
|
||||||
|
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [])
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
|
setSelectedIds(data, [])
|
||||||
delete getPage(data).shapes[id]
|
delete getPage(data).shapes[id]
|
||||||
data.selectedIds.clear()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import { getCurrentCamera, getPage, getSelectedShapes } from 'utils/utils'
|
import {
|
||||||
|
getCurrentCamera,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
getSelectedShapes,
|
||||||
|
setSelectedIds,
|
||||||
|
} from 'utils/utils'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
|
@ -24,24 +30,26 @@ export default function duplicateCommand(data: Data) {
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
for (const duplicate of duplicates) {
|
for (const duplicate of duplicates) {
|
||||||
shapes[duplicate.id] = duplicate
|
shapes[duplicate.id] = duplicate
|
||||||
data.selectedIds.add(duplicate.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedIds(
|
||||||
|
data,
|
||||||
|
duplicates.map((d) => d.id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
for (const duplicate of duplicates) {
|
for (const duplicate of duplicates) {
|
||||||
delete shapes[duplicate.id]
|
delete shapes[duplicate.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let id in selectedShapes) {
|
setSelectedIds(
|
||||||
data.selectedIds.add(id)
|
data,
|
||||||
}
|
selectedShapes.map((d) => d.id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Command from "./command"
|
import Command from './command'
|
||||||
import history from "../history"
|
import history from '../history'
|
||||||
import { CodeControl, Data, Shape } from "types"
|
import { CodeControl, Data, Shape } from 'types'
|
||||||
import { current } from "immer"
|
import { current } from 'immer'
|
||||||
import { getPage } from "utils/utils"
|
import { getPage, getSelectedIds, setSelectedIds } from 'utils/utils'
|
||||||
|
|
||||||
export default function generateCommand(
|
export default function generateCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
|
@ -33,12 +33,12 @@ export default function generateCommand(
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
new Command({
|
new Command({
|
||||||
name: "translate_shapes",
|
name: 'translate_shapes',
|
||||||
category: "canvas",
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [])
|
||||||
|
|
||||||
// Remove previous generated shapes
|
// Remove previous generated shapes
|
||||||
for (let id in shapes) {
|
for (let id in shapes) {
|
||||||
|
|
|
@ -4,8 +4,10 @@ import { Data, GroupShape, Shape, ShapeType } from 'types'
|
||||||
import {
|
import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getPage,
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
getShape,
|
getShape,
|
||||||
|
setSelectedIds,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import { createShape, getShapeUtils } from 'lib/shape-utils'
|
import { createShape, getShapeUtils } from 'lib/shape-utils'
|
||||||
|
@ -15,7 +17,9 @@ import commands from '.'
|
||||||
|
|
||||||
export default function groupCommand(data: Data) {
|
export default function groupCommand(data: Data) {
|
||||||
const cData = current(data)
|
const cData = current(data)
|
||||||
const { currentPageId, selectedIds } = cData
|
const { currentPageId } = cData
|
||||||
|
|
||||||
|
const oldSelectedIds = getSelectedIds(cData)
|
||||||
|
|
||||||
const initialShapes = getSelectedShapes(cData).sort(
|
const initialShapes = getSelectedShapes(cData).sort(
|
||||||
(a, b) => a.childIndex - b.childIndex
|
(a, b) => a.childIndex - b.childIndex
|
||||||
|
@ -108,7 +112,7 @@ export default function groupCommand(data: Data) {
|
||||||
getShapeUtils(oldParent).setProperty(
|
getShapeUtils(oldParent).setProperty(
|
||||||
oldParent,
|
oldParent,
|
||||||
'children',
|
'children',
|
||||||
oldParent.children.filter((id) => !selectedIds.has(id))
|
oldParent.children.filter((id) => !oldSelectedIds.has(id))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,8 +123,7 @@ export default function groupCommand(data: Data) {
|
||||||
.setProperty(shape, 'parentId', newGroupShape.id)
|
.setProperty(shape, 'parentId', newGroupShape.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [newGroupShape.id])
|
||||||
data.selectedIds.add(newGroupShape.id)
|
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
@ -157,7 +160,7 @@ export default function groupCommand(data: Data) {
|
||||||
delete shapes[newGroupShape.id]
|
delete shapes[newGroupShape.id]
|
||||||
|
|
||||||
// Reselect the children of the group
|
// Reselect the children of the group
|
||||||
data.selectedIds = new Set(initialShapeIds)
|
setSelectedIds(data, initialShapeIds)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, MoveType, Shape } from 'types'
|
import { Data, MoveType, Shape } from 'types'
|
||||||
import { forceIntegerChildIndices, getChildren, getPage } from 'utils/utils'
|
import {
|
||||||
|
forceIntegerChildIndices,
|
||||||
|
getChildren,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
setToArray,
|
||||||
|
} from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
export default function moveCommand(data: Data, type: MoveType) {
|
export default function moveCommand(data: Data, type: MoveType) {
|
||||||
|
@ -9,7 +15,7 @@ export default function moveCommand(data: Data, type: MoveType) {
|
||||||
|
|
||||||
const page = getPage(data)
|
const page = getPage(data)
|
||||||
|
|
||||||
const selectedIds = Array.from(data.selectedIds.values())
|
const selectedIds = setToArray(getSelectedIds(data))
|
||||||
|
|
||||||
const initialIndices = Object.fromEntries(
|
const initialIndices = Object.fromEntries(
|
||||||
selectedIds.map((id) => [id, page.shapes[id].childIndex])
|
selectedIds.map((id) => [id, page.shapes[id].childIndex])
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, ShapeStyles } from 'types'
|
import { Data, ShapeStyles } from 'types'
|
||||||
import { getDocumentBranch, getPage, getSelectedShapes } from 'utils/utils'
|
import {
|
||||||
|
getDocumentBranch,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
getSelectedShapes,
|
||||||
|
setToArray,
|
||||||
|
} from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
|
|
||||||
|
@ -10,7 +16,9 @@ export default function styleCommand(data: Data, styles: Partial<ShapeStyles>) {
|
||||||
const page = getPage(cData)
|
const page = getPage(cData)
|
||||||
const { currentPageId } = cData
|
const { currentPageId } = cData
|
||||||
|
|
||||||
const shapesToStyle = Array.from(data.selectedIds.values())
|
const selectedIds = setToArray(getSelectedIds(data))
|
||||||
|
|
||||||
|
const shapesToStyle = selectedIds
|
||||||
.flatMap((id) => getDocumentBranch(data, id))
|
.flatMap((id) => getDocumentBranch(data, id))
|
||||||
.map((id) => page.shapes[id])
|
.map((id) => page.shapes[id])
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,12 @@ import { Data, Corner, Edge } from 'types'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import { TransformSingleSnapshot } from 'state/sessions/transform-single-session'
|
import { TransformSingleSnapshot } from 'state/sessions/transform-single-session'
|
||||||
import { getPage, updateParents } from 'utils/utils'
|
import {
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
setSelectedIds,
|
||||||
|
updateParents,
|
||||||
|
} from 'utils/utils'
|
||||||
|
|
||||||
export default function transformSingleCommand(
|
export default function transformSingleCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
|
@ -25,8 +30,7 @@ export default function transformSingleCommand(
|
||||||
|
|
||||||
const { shapes } = getPage(data, after.currentPageId)
|
const { shapes } = getPage(data, after.currentPageId)
|
||||||
|
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [id])
|
||||||
data.selectedIds.add(id)
|
|
||||||
|
|
||||||
shapes[id] = shape
|
shapes[id] = shape
|
||||||
|
|
||||||
|
@ -38,13 +42,13 @@ export default function transformSingleCommand(
|
||||||
const { shapes } = getPage(data, before.currentPageId)
|
const { shapes } = getPage(data, before.currentPageId)
|
||||||
|
|
||||||
if (isCreating) {
|
if (isCreating) {
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [])
|
||||||
delete shapes[id]
|
delete shapes[id]
|
||||||
} else {
|
} else {
|
||||||
const page = getPage(data)
|
const page = getPage(data)
|
||||||
page.shapes[id] = initialShape
|
page.shapes[id] = initialShape
|
||||||
updateParents(data, [id])
|
updateParents(data, [id])
|
||||||
data.selectedIds = new Set([id])
|
setSelectedIds(data, [id])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,12 @@ import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { TranslateSnapshot } from 'state/sessions/translate-session'
|
import { TranslateSnapshot } from 'state/sessions/translate-session'
|
||||||
import { Data, GroupShape, Shape, ShapeType } from 'types'
|
import { Data, GroupShape, Shape, ShapeType } from 'types'
|
||||||
import { getDocumentBranch, getPage, updateParents } from 'utils/utils'
|
import {
|
||||||
|
getDocumentBranch,
|
||||||
|
getPage,
|
||||||
|
setSelectedIds,
|
||||||
|
updateParents,
|
||||||
|
} from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
|
@ -50,7 +55,10 @@ export default function translateCommand(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set selected shapes
|
// Set selected shapes
|
||||||
data.selectedIds = new Set(initialShapes.map((s) => s.id))
|
setSelectedIds(
|
||||||
|
data,
|
||||||
|
initialShapes.map((s) => s.id)
|
||||||
|
)
|
||||||
|
|
||||||
// Update parents
|
// Update parents
|
||||||
updateParents(
|
updateParents(
|
||||||
|
@ -72,7 +80,10 @@ export default function translateCommand(
|
||||||
if (isCloning) for (const { id } of clones) delete shapes[id]
|
if (isCloning) for (const { id } of clones) delete shapes[id]
|
||||||
|
|
||||||
// Set selected shapes
|
// Set selected shapes
|
||||||
data.selectedIds = new Set(initialShapes.map((s) => s.id))
|
setSelectedIds(
|
||||||
|
data,
|
||||||
|
initialShapes.map((s) => s.id)
|
||||||
|
)
|
||||||
|
|
||||||
// Restore children on parents
|
// Restore children on parents
|
||||||
initialParents.forEach(({ id, children }) => {
|
initialParents.forEach(({ id, children }) => {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
getShape,
|
getShape,
|
||||||
|
setSelectedIds,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import { createShape, getShapeUtils } from 'lib/shape-utils'
|
import { createShape, getShapeUtils } from 'lib/shape-utils'
|
||||||
|
@ -14,7 +15,7 @@ import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default function ungroupCommand(data: Data) {
|
export default function ungroupCommand(data: Data) {
|
||||||
const cData = current(data)
|
const cData = current(data)
|
||||||
const { currentPageId, selectedIds } = cData
|
const { currentPageId } = cData
|
||||||
|
|
||||||
const selectedGroups = getSelectedShapes(cData)
|
const selectedGroups = getSelectedShapes(cData)
|
||||||
.filter((shape) => shape.type === ShapeType.Group)
|
.filter((shape) => shape.type === ShapeType.Group)
|
||||||
|
@ -55,14 +56,11 @@ export default function ungroupCommand(data: Data) {
|
||||||
(oldGroupShape.children.length + 1)
|
(oldGroupShape.children.length + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
// Move shapes to page
|
// Move shapes to page
|
||||||
oldGroupShape.children
|
oldGroupShape.children
|
||||||
.map((id) => shapes[id])
|
.map((id) => shapes[id])
|
||||||
.forEach(({ id }, i) => {
|
.forEach(({ id }, i) => {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
data.selectedIds.add(id)
|
|
||||||
getShapeUtils(shape)
|
getShapeUtils(shape)
|
||||||
.setProperty(shape, 'parentId', oldGroupShape.parentId)
|
.setProperty(shape, 'parentId', oldGroupShape.parentId)
|
||||||
.setProperty(
|
.setProperty(
|
||||||
|
@ -72,14 +70,15 @@ export default function ungroupCommand(data: Data) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setSelectedIds(data, oldGroupShape.children)
|
||||||
|
|
||||||
delete shapes[oldGroupShape.id]
|
delete shapes[oldGroupShape.id]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
selectedIds.clear()
|
|
||||||
selectedGroups.forEach((group) => {
|
selectedGroups.forEach((group) => {
|
||||||
selectedIds.add(group.id)
|
|
||||||
shapes[group.id] = group
|
shapes[group.id] = group
|
||||||
group.children.forEach((id, i) => {
|
group.children.forEach((id, i) => {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
@ -88,6 +87,11 @@ export default function ungroupCommand(data: Data) {
|
||||||
.setProperty(shape, 'childIndex', i)
|
.setProperty(shape, 'childIndex', i)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setSelectedIds(
|
||||||
|
data,
|
||||||
|
selectedGroups.map((g) => g.id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { PointerInfo } from 'types'
|
||||||
import {
|
import {
|
||||||
getCameraZoom,
|
getCameraZoom,
|
||||||
getCurrentCamera,
|
getCurrentCamera,
|
||||||
|
getSelectedIds,
|
||||||
screenToWorld,
|
screenToWorld,
|
||||||
|
setToArray,
|
||||||
setZoomCSS,
|
setZoomCSS,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import session from './session'
|
import session from './session'
|
||||||
|
@ -26,7 +28,7 @@ export function fastDrawUpdate(info: PointerInfo) {
|
||||||
info.shiftKey
|
info.shiftKey
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedId = Array.from(data.selectedIds.values())[0]
|
const selectedId = setToArray(getSelectedIds(data))[0]
|
||||||
|
|
||||||
const shape = data.document.pages[data.currentPageId].shapes[selectedId]
|
const shape = data.document.pages[data.currentPageId].shapes[selectedId]
|
||||||
|
|
||||||
|
@ -88,7 +90,5 @@ export function fastBrushSelect(point: number[]) {
|
||||||
const data = { ...state.data }
|
const data = { ...state.data }
|
||||||
session.current.update(data, screenToWorld(point, data))
|
session.current.update(data, screenToWorld(point, data))
|
||||||
|
|
||||||
data.selectedIds = new Set(data.selectedIds)
|
|
||||||
|
|
||||||
state.forceData(Object.freeze(data))
|
state.forceData(Object.freeze(data))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Data } from 'types'
|
import { Data, Page, PageState } from 'types'
|
||||||
import { BaseCommand } from './commands/command'
|
import { BaseCommand } from './commands/command'
|
||||||
|
import storage from './storage'
|
||||||
const CURRENT_VERSION = 'code_slate_0.0.3'
|
|
||||||
|
|
||||||
// A singleton to manage history changes.
|
// A singleton to manage history changes.
|
||||||
|
|
||||||
class BaseHistory<T> {
|
class History<T extends Data> {
|
||||||
private stack: BaseCommand<T>[] = []
|
private stack: BaseCommand<T>[] = []
|
||||||
private pointer = -1
|
private pointer = -1
|
||||||
private maxLength = 100
|
private maxLength = 100
|
||||||
|
@ -24,7 +23,7 @@ class BaseHistory<T> {
|
||||||
this.pointer = this.maxLength - 1
|
this.pointer = this.maxLength - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save(data)
|
storage.save(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
undo = (data: T) => {
|
undo = (data: T) => {
|
||||||
|
@ -33,7 +32,7 @@ class BaseHistory<T> {
|
||||||
command.undo(data)
|
command.undo(data)
|
||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
this.pointer--
|
this.pointer--
|
||||||
this.save(data)
|
storage.save(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
redo = (data: T) => {
|
redo = (data: T) => {
|
||||||
|
@ -42,25 +41,7 @@ class BaseHistory<T> {
|
||||||
command.redo(data, false)
|
command.redo(data, false)
|
||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
this.pointer++
|
this.pointer++
|
||||||
this.save(data)
|
storage.save(data)
|
||||||
}
|
|
||||||
|
|
||||||
load(data: T, id = CURRENT_VERSION) {
|
|
||||||
if (typeof window === 'undefined') return
|
|
||||||
if (typeof localStorage === 'undefined') return
|
|
||||||
|
|
||||||
const savedData = localStorage.getItem(id)
|
|
||||||
|
|
||||||
if (savedData !== null) {
|
|
||||||
Object.assign(data, this.restoreSavedData(JSON.parse(savedData)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
save = (data: T, id = CURRENT_VERSION) => {
|
|
||||||
if (typeof window === 'undefined') return
|
|
||||||
if (typeof localStorage === 'undefined') return
|
|
||||||
|
|
||||||
localStorage.setItem(id, JSON.stringify(this.prepareDataForSave(data)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disable = () => {
|
disable = () => {
|
||||||
|
@ -71,14 +52,6 @@ class BaseHistory<T> {
|
||||||
this._enabled = true
|
this._enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareDataForSave(data: T): any {
|
|
||||||
return { ...data }
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreSavedData(data: any): T {
|
|
||||||
return { ...data }
|
|
||||||
}
|
|
||||||
|
|
||||||
pop() {
|
pop() {
|
||||||
if (this.stack.length > 0) {
|
if (this.stack.length > 0) {
|
||||||
this.stack.pop()
|
this.stack.pop()
|
||||||
|
@ -91,44 +64,4 @@ class BaseHistory<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// App-specific
|
|
||||||
|
|
||||||
class History extends BaseHistory<Data> {
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareDataForSave(data: Data): any {
|
|
||||||
const dataToSave: any = { ...data }
|
|
||||||
|
|
||||||
dataToSave.selectedIds = Array.from(data.selectedIds.values())
|
|
||||||
|
|
||||||
return dataToSave
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreSavedData(data: any): Data {
|
|
||||||
const restoredData: Data = { ...data }
|
|
||||||
|
|
||||||
restoredData.selectedIds = new Set(restoredData.selectedIds)
|
|
||||||
|
|
||||||
// Also restore camera position, which is saved separately in this app
|
|
||||||
const cameraInfo = localStorage.getItem('code_slate_camera')
|
|
||||||
|
|
||||||
if (cameraInfo !== null) {
|
|
||||||
Object.assign(
|
|
||||||
restoredData.pageStates[data.currentPageId].camera,
|
|
||||||
JSON.parse(cameraInfo)
|
|
||||||
)
|
|
||||||
|
|
||||||
// And update the CSS property
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
'--camera-zoom',
|
|
||||||
restoredData.pageStates[data.currentPageId].camera.zoom.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return restoredData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new History()
|
export default new History()
|
||||||
|
|
|
@ -3,7 +3,13 @@ import * as vec from 'utils/vec'
|
||||||
import BaseSession from './base-session'
|
import BaseSession from './base-session'
|
||||||
import commands from 'state/commands'
|
import commands from 'state/commands'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import { getBoundsFromPoints, getPage, updateParents } from 'utils/utils'
|
import {
|
||||||
|
getBoundsFromPoints,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
setToArray,
|
||||||
|
updateParents,
|
||||||
|
} from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
export default class ArrowSession extends BaseSession {
|
export default class ArrowSession extends BaseSession {
|
||||||
|
@ -116,7 +122,7 @@ export function getArrowSnapshot(data: Data, id: string) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
initialShape,
|
initialShape,
|
||||||
selectedIds: new Set(data.selectedIds),
|
selectedIds: setToArray(getSelectedIds(data)),
|
||||||
currentPageId: data.currentPageId,
|
currentPageId: data.currentPageId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,14 @@ import { current } from 'immer'
|
||||||
import { Bounds, Data, ShapeType } from 'types'
|
import { Bounds, Data, ShapeType } from 'types'
|
||||||
import BaseSession from './base-session'
|
import BaseSession from './base-session'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getBoundsFromPoints, getPage, getShapes } from 'utils/utils'
|
import {
|
||||||
|
getBoundsFromPoints,
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
getShapes,
|
||||||
|
setSelectedIds,
|
||||||
|
setToArray,
|
||||||
|
} from 'utils/utils'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
import state from 'state/state'
|
import state from 'state/state'
|
||||||
|
|
||||||
|
@ -25,6 +32,8 @@ export default class BrushSession extends BaseSession {
|
||||||
|
|
||||||
const hits = new Set<string>([])
|
const hits = new Set<string>([])
|
||||||
|
|
||||||
|
const selectedIds = getSelectedIds(data)
|
||||||
|
|
||||||
for (let id in snapshot.shapeHitTests) {
|
for (let id in snapshot.shapeHitTests) {
|
||||||
const { test, selectId } = snapshot.shapeHitTests[id]
|
const { test, selectId } = snapshot.shapeHitTests[id]
|
||||||
if (!hits.has(selectId)) {
|
if (!hits.has(selectId)) {
|
||||||
|
@ -32,11 +41,11 @@ export default class BrushSession extends BaseSession {
|
||||||
hits.add(selectId)
|
hits.add(selectId)
|
||||||
|
|
||||||
// When brushing a shape, select its top group parent.
|
// When brushing a shape, select its top group parent.
|
||||||
if (!data.selectedIds.has(selectId)) {
|
if (!selectedIds.has(selectId)) {
|
||||||
data.selectedIds.add(selectId)
|
selectedIds.add(selectId)
|
||||||
}
|
}
|
||||||
} else if (data.selectedIds.has(selectId)) {
|
} else if (selectedIds.has(selectId)) {
|
||||||
data.selectedIds.delete(selectId)
|
selectedIds.delete(selectId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +55,7 @@ export default class BrushSession extends BaseSession {
|
||||||
|
|
||||||
cancel = (data: Data) => {
|
cancel = (data: Data) => {
|
||||||
data.brush = undefined
|
data.brush = undefined
|
||||||
data.selectedIds = new Set(this.snapshot.selectedIds)
|
setSelectedIds(data, this.snapshot.selectedIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
complete = (data: Data) => {
|
complete = (data: Data) => {
|
||||||
|
@ -61,7 +70,7 @@ export default class BrushSession extends BaseSession {
|
||||||
*/
|
*/
|
||||||
export function getBrushSnapshot(data: Data) {
|
export function getBrushSnapshot(data: Data) {
|
||||||
return {
|
return {
|
||||||
selectedIds: new Set(data.selectedIds),
|
selectedIds: setToArray(getSelectedIds(data)),
|
||||||
shapeHitTests: Object.fromEntries(
|
shapeHitTests: Object.fromEntries(
|
||||||
getShapes(state.data)
|
getShapes(state.data)
|
||||||
.filter((shape) => shape.type !== ShapeType.Group)
|
.filter((shape) => shape.type !== ShapeType.Group)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Data, LineShape, RayShape } from "types"
|
import { Data, LineShape, RayShape } from 'types'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import BaseSession from "./base-session"
|
import BaseSession from './base-session'
|
||||||
import commands from "state/commands"
|
import commands from 'state/commands'
|
||||||
import { current } from "immer"
|
import { current } from 'immer'
|
||||||
import { getPage } from "utils/utils"
|
import { getPage, getSelectedIds } from 'utils/utils'
|
||||||
|
|
||||||
export default class DirectionSession extends BaseSession {
|
export default class DirectionSession extends BaseSession {
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
|
@ -47,9 +47,9 @@ export function getDirectionSnapshot(data: Data) {
|
||||||
|
|
||||||
let snapshapes: { id: string; direction: number[] }[] = []
|
let snapshapes: { id: string; direction: number[] }[] = []
|
||||||
|
|
||||||
data.selectedIds.forEach((id) => {
|
getSelectedIds(data).forEach((id) => {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
if ("direction" in shape) {
|
if ('direction' in shape) {
|
||||||
snapshapes.push({ id: shape.id, direction: shape.direction })
|
snapshapes.push({ id: shape.id, direction: shape.direction })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -95,31 +95,12 @@ export default class BrushSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
complete = (data: Data) => {
|
complete = (data: Data) => {
|
||||||
if (this.points.length > 1) {
|
const { snapshot } = this
|
||||||
let minX = Infinity
|
const page = getPage(data)
|
||||||
let minY = Infinity
|
const shape = page.shapes[snapshot.id] as DrawShape
|
||||||
const pts = [...this.points]
|
|
||||||
|
|
||||||
for (let pt of pts) {
|
getShapeUtils(shape).onSessionComplete(shape)
|
||||||
minX = Math.min(pt[0], minX)
|
commands.draw(data, this.snapshot.id)
|
||||||
minY = Math.min(pt[1], minY)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let pt of pts) {
|
|
||||||
pt[0] -= minX
|
|
||||||
pt[1] -= minY
|
|
||||||
}
|
|
||||||
|
|
||||||
const { snapshot } = this
|
|
||||||
const page = getPage(data)
|
|
||||||
const shape = page.shapes[snapshot.id] as DrawShape
|
|
||||||
|
|
||||||
getShapeUtils(shape)
|
|
||||||
.setProperty(shape, 'points', pts)
|
|
||||||
.setProperty(shape, 'point', vec.add(shape.point, [minX, minY]))
|
|
||||||
}
|
|
||||||
|
|
||||||
commands.draw(data, this.snapshot.id, this.points)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
getShapeBounds,
|
getShapeBounds,
|
||||||
updateParents,
|
updateParents,
|
||||||
getDocumentBranch,
|
getDocumentBranch,
|
||||||
|
setToArray,
|
||||||
|
getSelectedIds,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
|
@ -101,7 +103,7 @@ export function getRotateSnapshot(data: Data) {
|
||||||
const cData = current(data)
|
const cData = current(data)
|
||||||
const page = getPage(cData)
|
const page = getPage(cData)
|
||||||
|
|
||||||
const initialShapes = Array.from(cData.selectedIds.values())
|
const initialShapes = setToArray(getSelectedIds(data))
|
||||||
.flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
|
.flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
|
||||||
.filter((shape) => !shape.isLocked)
|
.filter((shape) => !shape.isLocked)
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,11 @@ import {
|
||||||
getDocumentBranch,
|
getDocumentBranch,
|
||||||
getPage,
|
getPage,
|
||||||
getRelativeTransformedBoundingBox,
|
getRelativeTransformedBoundingBox,
|
||||||
|
getSelectedIds,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
getShapes,
|
getShapes,
|
||||||
getTransformedBoundingBox,
|
getTransformedBoundingBox,
|
||||||
|
setToArray,
|
||||||
updateParents,
|
updateParents,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
||||||
const { currentPageId } = cData
|
const { currentPageId } = cData
|
||||||
const page = getPage(cData)
|
const page = getPage(cData)
|
||||||
|
|
||||||
const initialShapes = Array.from(cData.selectedIds.values())
|
const initialShapes = setToArray(getSelectedIds(data))
|
||||||
.flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
|
.flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
|
||||||
.filter((shape) => !shape.isLocked)
|
.filter((shape) => !shape.isLocked)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
getDocumentBranch,
|
getDocumentBranch,
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
|
setSelectedIds,
|
||||||
updateParents,
|
updateParents,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
@ -47,17 +48,13 @@ export default class TranslateSession extends BaseSession {
|
||||||
if (isCloning) {
|
if (isCloning) {
|
||||||
if (!this.isCloning) {
|
if (!this.isCloning) {
|
||||||
this.isCloning = true
|
this.isCloning = true
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
for (const { id, point } of initialShapes) {
|
for (const { id, point } of initialShapes) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
getShapeUtils(shape).translateTo(shape, point)
|
getShapeUtils(shape).translateTo(shape, point)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
for (const clone of clones) {
|
for (const clone of clones) {
|
||||||
data.selectedIds.add(clone.id)
|
|
||||||
shapes[clone.id] = { ...clone }
|
shapes[clone.id] = { ...clone }
|
||||||
const parent = shapes[clone.parentId]
|
const parent = shapes[clone.parentId]
|
||||||
if (!parent) continue
|
if (!parent) continue
|
||||||
|
@ -66,6 +63,11 @@ export default class TranslateSession extends BaseSession {
|
||||||
clone.id,
|
clone.id,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedIds(
|
||||||
|
data,
|
||||||
|
clones.map((c) => c.id)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { id, point } of clones) {
|
for (const { id, point } of clones) {
|
||||||
|
@ -80,11 +82,11 @@ export default class TranslateSession extends BaseSession {
|
||||||
} else {
|
} else {
|
||||||
if (this.isCloning) {
|
if (this.isCloning) {
|
||||||
this.isCloning = false
|
this.isCloning = false
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
for (const { id } of initialShapes) {
|
setSelectedIds(
|
||||||
data.selectedIds.add(id)
|
data,
|
||||||
}
|
initialShapes.map((c) => c.id)
|
||||||
|
)
|
||||||
|
|
||||||
for (const clone of clones) {
|
for (const clone of clones) {
|
||||||
delete shapes[clone.id]
|
delete shapes[clone.id]
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { createSelectorHook, createState } from '@state-designer/react'
|
import { createSelectorHook, createState } from '@state-designer/react'
|
||||||
|
import { updateFromCode } from 'lib/code/generate'
|
||||||
|
import { createShape, getShapeUtils } from 'lib/shape-utils'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
import inputs from './inputs'
|
import inputs from './inputs'
|
||||||
import { defaultDocument } from './data'
|
import { defaultDocument } from './data'
|
||||||
import { createShape, getShapeUtils } from 'lib/shape-utils'
|
import history from './history'
|
||||||
import history from 'state/history'
|
import storage from './storage'
|
||||||
import * as Sessions from './sessions'
|
import * as Sessions from './sessions'
|
||||||
import commands from './commands'
|
import commands from './commands'
|
||||||
import { updateFromCode } from 'lib/code/generate'
|
|
||||||
import {
|
import {
|
||||||
clamp,
|
clamp,
|
||||||
getChildren,
|
getChildren,
|
||||||
|
@ -26,6 +27,8 @@ import {
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getDocumentBranch,
|
getDocumentBranch,
|
||||||
getCameraZoom,
|
getCameraZoom,
|
||||||
|
getSelectedIds,
|
||||||
|
setSelectedIds,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
|
@ -69,7 +72,6 @@ const initialData: Data = {
|
||||||
boundsRotation: 0,
|
boundsRotation: 0,
|
||||||
pointedId: null,
|
pointedId: null,
|
||||||
hoveredId: null,
|
hoveredId: null,
|
||||||
selectedIds: new Set([]),
|
|
||||||
currentPageId: 'page1',
|
currentPageId: 'page1',
|
||||||
currentParentId: 'page1',
|
currentParentId: 'page1',
|
||||||
currentCodeFileId: 'file0',
|
currentCodeFileId: 'file0',
|
||||||
|
@ -77,12 +79,14 @@ const initialData: Data = {
|
||||||
document: defaultDocument,
|
document: defaultDocument,
|
||||||
pageStates: {
|
pageStates: {
|
||||||
page1: {
|
page1: {
|
||||||
|
selectedIds: new Set([]),
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
page2: {
|
page2: {
|
||||||
|
selectedIds: new Set([]),
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
|
@ -164,7 +168,7 @@ const state = createState({
|
||||||
do: 'deleteSelection',
|
do: 'deleteSelection',
|
||||||
else: ['selectAll', 'deleteSelection'],
|
else: ['selectAll', 'deleteSelection'],
|
||||||
},
|
},
|
||||||
CHANGED_CURRENT_PAGE: ['clearSelectedIds', 'setCurrentPage'],
|
CHANGED_PAGE: 'changePage',
|
||||||
CREATED_PAGE: ['clearSelectedIds', 'createPage'],
|
CREATED_PAGE: ['clearSelectedIds', 'createPage'],
|
||||||
DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
|
DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
|
||||||
},
|
},
|
||||||
|
@ -743,7 +747,7 @@ const state = createState({
|
||||||
return vec.dist2(payload.origin, payload.point) > 8
|
return vec.dist2(payload.origin, payload.point) > 8
|
||||||
},
|
},
|
||||||
isPointedShapeSelected(data) {
|
isPointedShapeSelected(data) {
|
||||||
return data.selectedIds.has(data.pointedId)
|
return getSelectedIds(data).has(data.pointedId)
|
||||||
},
|
},
|
||||||
isPressingShiftKey(data, payload: PointerInfo) {
|
isPressingShiftKey(data, payload: PointerInfo) {
|
||||||
return payload.shiftKey
|
return payload.shiftKey
|
||||||
|
@ -769,10 +773,10 @@ const state = createState({
|
||||||
return payload.target === 'rotate'
|
return payload.target === 'rotate'
|
||||||
},
|
},
|
||||||
hasSelection(data) {
|
hasSelection(data) {
|
||||||
return data.selectedIds.size > 0
|
return getSelectedIds(data).size > 0
|
||||||
},
|
},
|
||||||
hasMultipleSelection(data) {
|
hasMultipleSelection(data) {
|
||||||
return data.selectedIds.size > 1
|
return getSelectedIds(data).size > 1
|
||||||
},
|
},
|
||||||
isToolLocked(data) {
|
isToolLocked(data) {
|
||||||
return data.settings.isToolLocked
|
return data.settings.isToolLocked
|
||||||
|
@ -791,7 +795,7 @@ const state = createState({
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
/* ---------------------- Pages --------------------- */
|
/* ---------------------- Pages --------------------- */
|
||||||
setCurrentPage(data, payload: { id: string }) {
|
changePage(data, payload: { id: string }) {
|
||||||
commands.changePage(data, payload.id)
|
commands.changePage(data, payload.id)
|
||||||
},
|
},
|
||||||
createPage(data) {
|
createPage(data) {
|
||||||
|
@ -818,8 +822,7 @@ const state = createState({
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
getPage(data).shapes[shape.id] = shape
|
||||||
|
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [shape.id])
|
||||||
data.selectedIds.add(shape.id)
|
|
||||||
},
|
},
|
||||||
/* -------------------- Sessions -------------------- */
|
/* -------------------- Sessions -------------------- */
|
||||||
|
|
||||||
|
@ -902,7 +905,7 @@ const state = createState({
|
||||||
|
|
||||||
// Dragging Handle
|
// Dragging Handle
|
||||||
startHandleSession(data, payload: PointerInfo) {
|
startHandleSession(data, payload: PointerInfo) {
|
||||||
const shapeId = Array.from(data.selectedIds.values())[0]
|
const shapeId = Array.from(getSelectedIds(data).values())[0]
|
||||||
const handleId = payload.target
|
const handleId = payload.target
|
||||||
|
|
||||||
session.current = new Sessions.HandleSession(
|
session.current = new Sessions.HandleSession(
|
||||||
|
@ -939,7 +942,7 @@ const state = createState({
|
||||||
) {
|
) {
|
||||||
const point = screenToWorld(inputs.pointer.origin, data)
|
const point = screenToWorld(inputs.pointer.origin, data)
|
||||||
session.current =
|
session.current =
|
||||||
data.selectedIds.size === 1
|
getSelectedIds(data).size === 1
|
||||||
? new Sessions.TransformSingleSession(data, payload.target, point)
|
? new Sessions.TransformSingleSession(data, payload.target, point)
|
||||||
: new Sessions.TransformSession(data, payload.target, point)
|
: new Sessions.TransformSession(data, payload.target, point)
|
||||||
},
|
},
|
||||||
|
@ -981,7 +984,7 @@ const state = createState({
|
||||||
|
|
||||||
// Drawing
|
// Drawing
|
||||||
startDrawSession(data, payload: PointerInfo) {
|
startDrawSession(data, payload: PointerInfo) {
|
||||||
const id = Array.from(data.selectedIds.values())[0]
|
const id = Array.from(getSelectedIds(data).values())[0]
|
||||||
session.current = new Sessions.DrawSession(
|
session.current = new Sessions.DrawSession(
|
||||||
data,
|
data,
|
||||||
id,
|
id,
|
||||||
|
@ -1008,7 +1011,7 @@ const state = createState({
|
||||||
|
|
||||||
// Arrow
|
// Arrow
|
||||||
startArrowSession(data, payload: PointerInfo) {
|
startArrowSession(data, payload: PointerInfo) {
|
||||||
const id = Array.from(data.selectedIds.values())[0]
|
const id = Array.from(getSelectedIds(data).values())[0]
|
||||||
session.current = new Sessions.ArrowSession(
|
session.current = new Sessions.ArrowSession(
|
||||||
data,
|
data,
|
||||||
id,
|
id,
|
||||||
|
@ -1047,7 +1050,7 @@ const state = createState({
|
||||||
/* -------------------- Selection ------------------- */
|
/* -------------------- Selection ------------------- */
|
||||||
|
|
||||||
selectAll(data) {
|
selectAll(data) {
|
||||||
const { selectedIds } = data
|
const selectedIds = getSelectedIds(data)
|
||||||
const page = getPage(data)
|
const page = getPage(data)
|
||||||
selectedIds.clear()
|
selectedIds.clear()
|
||||||
for (let id in page.shapes) {
|
for (let id in page.shapes) {
|
||||||
|
@ -1078,14 +1081,15 @@ const state = createState({
|
||||||
data.pointedId = undefined
|
data.pointedId = undefined
|
||||||
},
|
},
|
||||||
clearSelectedIds(data) {
|
clearSelectedIds(data) {
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [])
|
||||||
},
|
},
|
||||||
pullPointedIdFromSelectedIds(data) {
|
pullPointedIdFromSelectedIds(data) {
|
||||||
const { selectedIds, pointedId } = data
|
const { pointedId } = data
|
||||||
|
const selectedIds = getSelectedIds(data)
|
||||||
selectedIds.delete(pointedId)
|
selectedIds.delete(pointedId)
|
||||||
},
|
},
|
||||||
pushPointedIdToSelectedIds(data) {
|
pushPointedIdToSelectedIds(data) {
|
||||||
data.selectedIds.add(data.pointedId)
|
getSelectedIds(data).add(data.pointedId)
|
||||||
},
|
},
|
||||||
moveSelection(data, payload: { type: MoveType }) {
|
moveSelection(data, payload: { type: MoveType }) {
|
||||||
commands.move(data, payload.type)
|
commands.move(data, payload.type)
|
||||||
|
@ -1311,9 +1315,6 @@ const state = createState({
|
||||||
popHistory() {
|
popHistory() {
|
||||||
history.pop()
|
history.pop()
|
||||||
},
|
},
|
||||||
forceSave(data) {
|
|
||||||
history.save(data)
|
|
||||||
},
|
|
||||||
enableHistory() {
|
enableHistory() {
|
||||||
history.enable()
|
history.enable()
|
||||||
},
|
},
|
||||||
|
@ -1377,7 +1378,7 @@ const state = createState({
|
||||||
|
|
||||||
history.disable()
|
history.disable()
|
||||||
|
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { shapes } = updateFromCode(
|
const { shapes } = updateFromCode(
|
||||||
|
@ -1407,13 +1408,25 @@ const state = createState({
|
||||||
|
|
||||||
/* ---------------------- Data ---------------------- */
|
/* ---------------------- Data ---------------------- */
|
||||||
|
|
||||||
|
forceSave(data) {
|
||||||
|
storage.save(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
savePage(data) {
|
||||||
|
storage.savePage(data, data.currentPageId)
|
||||||
|
},
|
||||||
|
|
||||||
|
loadPage(data) {
|
||||||
|
storage.loadPage(data, data.currentPageId)
|
||||||
|
},
|
||||||
|
|
||||||
saveCode(data, payload: { code: string }) {
|
saveCode(data, payload: { code: string }) {
|
||||||
data.document.code[data.currentCodeFileId].code = payload.code
|
data.document.code[data.currentCodeFileId].code = payload.code
|
||||||
history.save(data)
|
storage.save(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
restoreSavedData(data) {
|
restoreSavedData(data) {
|
||||||
history.load(data)
|
storage.load(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
clearBoundsRotation(data) {
|
clearBoundsRotation(data) {
|
||||||
|
@ -1422,10 +1435,10 @@ const state = createState({
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
selectedIds(data) {
|
selectedIds(data) {
|
||||||
return new Set(data.selectedIds)
|
return new Set(getSelectedIds(data))
|
||||||
},
|
},
|
||||||
selectedBounds(data) {
|
selectedBounds(data) {
|
||||||
const { selectedIds } = data
|
const selectedIds = getSelectedIds(data)
|
||||||
|
|
||||||
const page = getPage(data)
|
const page = getPage(data)
|
||||||
|
|
||||||
|
@ -1438,7 +1451,7 @@ const state = createState({
|
||||||
if (selectedIds.size === 1) {
|
if (selectedIds.size === 1) {
|
||||||
if (!shapes[0]) {
|
if (!shapes[0]) {
|
||||||
console.error('Could not find that shape! Clearing selected IDs.')
|
console.error('Could not find that shape! Clearing selected IDs.')
|
||||||
data.selectedIds.clear()
|
setSelectedIds(data, [])
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1497,7 +1510,7 @@ const state = createState({
|
||||||
return commonBounds
|
return commonBounds
|
||||||
},
|
},
|
||||||
selectedStyle(data) {
|
selectedStyle(data) {
|
||||||
const selectedIds = Array.from(data.selectedIds.values())
|
const selectedIds = Array.from(getSelectedIds(data).values())
|
||||||
const { currentStyle } = data
|
const { currentStyle } = data
|
||||||
|
|
||||||
if (selectedIds.length === 0) {
|
if (selectedIds.length === 0) {
|
||||||
|
|
139
state/storage.ts
Normal file
139
state/storage.ts
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import { Data, Page, PageState } from 'types'
|
||||||
|
import { setToArray } from 'utils/utils'
|
||||||
|
|
||||||
|
const CURRENT_VERSION = 'code_slate_0.0.4'
|
||||||
|
const DOCUMENT_ID = '0001'
|
||||||
|
|
||||||
|
function storageId(label: string, id: string) {
|
||||||
|
return `${CURRENT_VERSION}_doc_${DOCUMENT_ID}_${label}_${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
// Saving
|
||||||
|
load(data: Data, id = CURRENT_VERSION) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (typeof localStorage === 'undefined') return
|
||||||
|
|
||||||
|
// Load data from local storage
|
||||||
|
const savedData = localStorage.getItem(id)
|
||||||
|
|
||||||
|
if (savedData !== null) {
|
||||||
|
const restoredData = JSON.parse(savedData)
|
||||||
|
|
||||||
|
// Empty shapes in state for each page
|
||||||
|
for (let key in restoredData.document.pages) {
|
||||||
|
restoredData.document.pages[key].shapes = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty page states for each page
|
||||||
|
for (let key in restoredData.pageStates) {
|
||||||
|
restoredData.document.pages[key].shapes = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge restored data into state
|
||||||
|
Object.assign(data, restoredData)
|
||||||
|
|
||||||
|
// Load current page
|
||||||
|
this.loadPage(data, data.currentPageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save = (data: Data, id = CURRENT_VERSION) => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (typeof localStorage === 'undefined') return
|
||||||
|
|
||||||
|
const dataToSave: any = { ...data }
|
||||||
|
|
||||||
|
// Don't save pageStates
|
||||||
|
dataToSave.pageStates = {}
|
||||||
|
|
||||||
|
// Save current data to local storage
|
||||||
|
localStorage.setItem(id, JSON.stringify(dataToSave))
|
||||||
|
|
||||||
|
// Save current page
|
||||||
|
this.savePage(data, data.currentPageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
savePage(data: Data, pageId: string) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (typeof localStorage === 'undefined') return
|
||||||
|
|
||||||
|
// Save page
|
||||||
|
const page = data.document.pages[pageId]
|
||||||
|
|
||||||
|
localStorage.setItem(storageId('page', pageId), JSON.stringify(page))
|
||||||
|
|
||||||
|
// Save page state
|
||||||
|
|
||||||
|
let currentPageState = {
|
||||||
|
camera: {
|
||||||
|
point: [0, 0],
|
||||||
|
zoom: 1,
|
||||||
|
},
|
||||||
|
selectedIds: new Set([]),
|
||||||
|
...data.pageStates[pageId],
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageState = {
|
||||||
|
...currentPageState,
|
||||||
|
selectedIds: setToArray(currentPageState.selectedIds),
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
storageId('pageState', pageId),
|
||||||
|
JSON.stringify(pageState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPage(data: Data, pageId: string) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (typeof localStorage === 'undefined') return
|
||||||
|
|
||||||
|
// Load page and merge into state
|
||||||
|
const savedPage = localStorage.getItem(storageId('page', pageId))
|
||||||
|
|
||||||
|
if (savedPage !== null) {
|
||||||
|
const restored: Page = JSON.parse(savedPage)
|
||||||
|
data.document.pages[pageId] = restored
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load page state and merge into state
|
||||||
|
const savedPageState = localStorage.getItem(storageId('pageState', pageId))
|
||||||
|
|
||||||
|
if (savedPageState !== null) {
|
||||||
|
const restored: PageState = JSON.parse(savedPageState)
|
||||||
|
restored.selectedIds = new Set(restored.selectedIds)
|
||||||
|
data.pageStates[pageId] = restored
|
||||||
|
} else {
|
||||||
|
data.pageStates[pageId] = {
|
||||||
|
camera: {
|
||||||
|
point: [0, 0],
|
||||||
|
zoom: 1,
|
||||||
|
},
|
||||||
|
selectedIds: new Set([]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty shapes in state for other pages
|
||||||
|
for (let key in data.document.pages) {
|
||||||
|
if (key === pageId) continue
|
||||||
|
data.document.pages[key].shapes = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty page states for other pages
|
||||||
|
for (let key in data.pageStates) {
|
||||||
|
if (key === pageId) continue
|
||||||
|
data.document.pages[key].shapes = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update camera
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--camera-zoom',
|
||||||
|
data.pageStates[data.currentPageId].camera.zoom.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = new Storage()
|
||||||
|
|
||||||
|
export default storage
|
6
todo.md
6
todo.md
|
@ -19,3 +19,9 @@
|
||||||
|
|
||||||
- shift dragging arrow handles should lock to directions
|
- shift dragging arrow handles should lock to directions
|
||||||
- fix undo/redo on rotated arrows
|
- fix undo/redo on rotated arrows
|
||||||
|
- fix shift-rotation
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
- [x] Make selection part of page state
|
||||||
|
- [ ] Allow only one page to be in the document at a time
|
||||||
|
|
2
types.ts
2
types.ts
|
@ -22,7 +22,6 @@ export interface Data {
|
||||||
activeTool: ShapeType | 'select'
|
activeTool: ShapeType | 'select'
|
||||||
brush?: Bounds
|
brush?: Bounds
|
||||||
boundsRotation: number
|
boundsRotation: number
|
||||||
selectedIds: Set<string>
|
|
||||||
pointedId?: string
|
pointedId?: string
|
||||||
hoveredId?: string
|
hoveredId?: string
|
||||||
currentPageId: string
|
currentPageId: string
|
||||||
|
@ -49,6 +48,7 @@ export interface Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageState {
|
export interface PageState {
|
||||||
|
selectedIds: Set<string>
|
||||||
camera: {
|
camera: {
|
||||||
point: number[]
|
point: number[]
|
||||||
zoom: number
|
zoom: number
|
||||||
|
|
|
@ -1381,7 +1381,7 @@ export function getShapes(data: Data, pageId = data.currentPageId) {
|
||||||
|
|
||||||
export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
|
export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
|
||||||
const page = getPage(data, pageId)
|
const page = getPage(data, pageId)
|
||||||
const ids = Array.from(data.selectedIds.values())
|
const ids = setToArray(getSelectedIds(data))
|
||||||
return ids.map((id) => page.shapes[id])
|
return ids.map((id) => page.shapes[id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1664,3 +1664,16 @@ export function getDocumentBranch(data: Data, id: string): string[] {
|
||||||
...shape.children.flatMap((childId) => getDocumentBranch(data, childId)),
|
...shape.children.flatMap((childId) => getDocumentBranch(data, childId)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSelectedIds(data: Data) {
|
||||||
|
return data.pageStates[data.currentPageId].selectedIds
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSelectedIds(data: Data, ids: string[]) {
|
||||||
|
data.pageStates[data.currentPageId].selectedIds = new Set(ids)
|
||||||
|
return data.pageStates[data.currentPageId].selectedIds
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToArray<T>(set: Set<T>): T[] {
|
||||||
|
return Array.from(set.values())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue