Moves selectedIds into page state, state mounts only one page state / page at a time

This commit is contained in:
Steve Ruiz 2021-06-07 12:18:50 +01:00
parent 45fd645885
commit 350c1debde
34 changed files with 445 additions and 249 deletions

View file

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

View file

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

View file

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

View file

@ -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 }) => (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()
}, },
}) })
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 }) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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