Adds page control, pages

This commit is contained in:
Steve Ruiz 2021-06-03 13:06:39 +01:00
parent 34256f992a
commit 5ba56216d0
26 changed files with 460 additions and 295 deletions

View file

@ -1,8 +1,9 @@
import * as React from 'react' import * as React from 'react'
import { Edge, Corner, LineShape, ArrowShape } from 'types' import { Edge, Corner } from 'types'
import { useSelector } from 'state' import { useSelector } from 'state'
import { import {
deepCompareArrays, deepCompareArrays,
getCurrentCamera,
getPage, getPage,
getSelectedShapes, getSelectedShapes,
isMobile, isMobile,
@ -17,7 +18,7 @@ import Handles from './handles'
export default function Bounds() { export default function Bounds() {
const isBrushing = useSelector((s) => s.isIn('brushSelecting')) const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
const isSelecting = useSelector((s) => s.isIn('selecting')) const isSelecting = useSelector((s) => s.isIn('selecting'))
const zoom = useSelector((s) => s.data.camera.zoom) const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
const bounds = useSelector((s) => s.values.selectedBounds) const bounds = useSelector((s) => s.values.selectedBounds)
const selectedIds = useSelector( const selectedIds = useSelector(

View file

@ -1,10 +1,10 @@
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
import { memo } from 'react' import { memo } from 'react'
import { useSelector } from 'state' import { useSelector } from 'state'
import { deepCompareArrays, getPage } from 'utils/utils' import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
export default function Defs() { export default function Defs() {
const zoom = useSelector((s) => s.data.camera.zoom) const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
const currentPageShapeIds = useSelector(({ data }) => { const currentPageShapeIds = useSelector(({ data }) => {
return Object.values(getPage(data).shapes) return Object.values(getPage(data).shapes)

View file

@ -8,6 +8,7 @@ import ToolsPanel from './tools-panel/tools-panel'
import StylePanel from './style-panel/style-panel' import StylePanel from './style-panel/style-panel'
import { useSelector } from 'state' import { useSelector } from 'state'
import styled from 'styles' import styled from 'styles'
import PagePanel from './page-panel/page-panel'
export default function Editor() { export default function Editor() {
useKeyboardEvents() useKeyboardEvents()
@ -20,6 +21,7 @@ export default function Editor() {
return ( return (
<Layout> <Layout>
<Canvas /> <Canvas />
<PagePanel />
<LeftPanels> <LeftPanels>
<CodePanel /> <CodePanel />
{hasControls && <ControlsPanel />} {hasControls && <ControlsPanel />}

View file

@ -0,0 +1,100 @@
import styled from 'styles'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as RadioGroup from '@radix-ui/react-radio-group'
import { IconWrapper, RowButton } from 'components/shared'
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons'
import * as Panel from '../panel'
import state, { useSelector } from 'state'
import { getPage } from 'utils/utils'
export default function PagePanel() {
const currentPageId = useSelector((s) => s.data.currentPageId)
const documentPages = useSelector((s) => s.data.document.pages)
const sorted = Object.values(documentPages).sort(
(a, b) => a.childIndex - b.childIndex
)
return (
<OuterContainer>
<DropdownMenu.Root>
<PanelRoot>
<DropdownMenu.Trigger as={RowButton}>
<span>{documentPages[currentPageId].name}</span>
<IconWrapper size="small">
<ChevronDownIcon />
</IconWrapper>
</DropdownMenu.Trigger>
<DropdownMenu.Content sideOffset={8}>
<PanelRoot>
<DropdownMenu.RadioGroup
as={Content}
value={currentPageId}
onValueChange={(id) =>
state.send('CHANGED_CURRENT_PAGE', { id })
}
>
{sorted.map(({ id, name }) => (
<StyledRadioItem key={id} value={id}>
<span>{name}</span>
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
<CheckIcon />
</DropdownMenu.ItemIndicator>
</StyledRadioItem>
))}
</DropdownMenu.RadioGroup>
</PanelRoot>
</DropdownMenu.Content>
</PanelRoot>
</DropdownMenu.Root>
</OuterContainer>
)
}
const PanelRoot = styled('div', {
minWidth: 1,
width: 184,
maxWidth: 184,
overflow: 'hidden',
position: 'relative',
display: 'flex',
alignItems: 'center',
pointerEvents: 'all',
padding: '2px',
borderRadius: '4px',
backgroundColor: '$panel',
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
})
const Content = styled(Panel.Content, {
width: '100%',
})
const StyledRadioItem = styled(DropdownMenu.RadioItem, {
height: 32,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 6px 0 12px',
cursor: 'pointer',
borderRadius: '4px',
backgroundColor: 'transparent',
outline: 'none',
'&:hover': {
backgroundColor: '$hover',
},
})
const OuterContainer = styled('div', {
position: 'fixed',
top: 8,
left: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
zIndex: 200,
height: 44,
})

View file

@ -1,3 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as RadioGroup from '@radix-ui/react-radio-group'
import * as Panel from './panel'
import styled from 'styles' import styled from 'styles'
export const IconButton = styled('button', { export const IconButton = styled('button', {
@ -60,3 +63,236 @@ export const IconButton = styled('button', {
}, },
}, },
}) })
export const RowButton = styled('button', {
position: 'relative',
display: 'flex',
width: '100%',
background: 'none',
height: '32px',
border: 'none',
cursor: 'pointer',
outline: 'none',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 6px 4px 12px',
'&::before': {
content: "''",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
zIndex: -1,
},
'&:hover::before': {
backgroundColor: '$hover',
borderRadius: 4,
},
'& label': {
fontFamily: '$ui',
fontSize: '$2',
fontWeight: '$1',
margin: 0,
padding: 0,
},
'& svg': {
position: 'relative',
stroke: 'rgba(0,0,0,.2)',
strokeWidth: 1,
zIndex: 1,
},
variants: {
size: {
icon: {
padding: '4px ',
width: 'auto',
},
},
},
})
export const StylePanelRoot = styled(Panel.Root, {
minWidth: 1,
width: 184,
maxWidth: 184,
overflow: 'hidden',
position: 'relative',
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
variants: {
isOpen: {
true: {},
false: {
padding: 2,
height: 38,
width: 38,
},
},
},
})
export const Group = styled(RadioGroup.Root, {
display: 'flex',
})
export const Item = styled('button', {
height: '32px',
width: '32px',
backgroundColor: '$panel',
borderRadius: '4px',
padding: '0',
margin: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
outline: 'none',
border: 'none',
pointerEvents: 'all',
cursor: 'pointer',
'&:hover:not(:disabled)': {
backgroundColor: '$hover',
'& svg': {
stroke: '$text',
fill: '$text',
strokeWidth: '0',
},
},
'&:disabled': {
opacity: '0.5',
},
variants: {
isActive: {
true: {
'& svg': {
fill: '$text',
stroke: '$text',
},
},
false: {
'& svg': {
fill: '$inactive',
stroke: '$inactive',
},
},
},
},
})
export const IconWrapper = styled('div', {
height: '100%',
borderRadius: '4px',
marginRight: '1px',
display: 'grid',
alignItems: 'center',
justifyContent: 'center',
outline: 'none',
border: 'none',
pointerEvents: 'all',
cursor: 'pointer',
'& svg': {
height: 22,
width: 22,
strokeWidth: 1,
},
'& > *': {
gridRow: 1,
gridColumn: 1,
},
variants: {
size: {
small: {
'& svg': {
height: '16px',
width: '16px',
},
},
medium: {
'& svg': {
height: '22px',
width: '22px',
},
},
},
},
})
export const DropdownContent = styled(DropdownMenu.Content, {
display: 'grid',
padding: 4,
gridTemplateColumns: 'repeat(4, 1fr)',
backgroundColor: '$panel',
borderRadius: 4,
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.28)',
variants: {
direction: {
vertical: {
gridTemplateColumns: '1fr',
},
},
},
})
export function DashSolidIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2}
strokeLinecap="round"
/>
</svg>
)
}
export function DashDashedIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray={50.26548 * 0.1}
/>
</svg>
)
}
const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
export function DashDottedIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray={dottedDasharray}
/>
</svg>
)
}

View file

@ -4,7 +4,7 @@ import { ColorStyle } from 'types'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { Square } from 'react-feather' import { Square } from 'react-feather'
import styled from 'styles' import styled from 'styles'
import { DropdownContent } from './shared' import { DropdownContent } from '../shared'
export default function ColorContent({ export default function ColorContent({
onChange, onChange,

View file

@ -1,7 +1,7 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { strokes } from 'lib/shape-styles' import { strokes } from 'lib/shape-styles'
import { ColorStyle } from 'types' import { ColorStyle } from 'types'
import { IconWrapper, RowButton } from './shared' import { RowButton, IconWrapper } from '../shared'
import { Square } from 'react-feather' import { Square } from 'react-feather'
import ColorContent from './color-content' import ColorContent from './color-content'

View file

@ -4,7 +4,7 @@ import {
DashDashedIcon, DashDashedIcon,
DashDottedIcon, DashDottedIcon,
DashSolidIcon, DashSolidIcon,
} from './shared' } from '../shared'
import * as RadioGroup from '@radix-ui/react-radio-group' import * as RadioGroup from '@radix-ui/react-radio-group'
import { DashStyle } from 'types' import { DashStyle } from 'types'
import state from 'state' import state from 'state'

View file

@ -2,7 +2,7 @@ import * as Checkbox from '@radix-ui/react-checkbox'
import { CheckIcon } from '@radix-ui/react-icons' import { CheckIcon } from '@radix-ui/react-icons'
import { strokes } from 'lib/shape-styles' import { strokes } from 'lib/shape-styles'
import { Square } from 'react-feather' import { Square } from 'react-feather'
import { IconWrapper, RowButton } from './shared' import { IconWrapper, RowButton } from '../shared'
interface Props { interface Props {
isFilled: boolean isFilled: boolean

View file

@ -9,7 +9,7 @@ import {
DashDottedIcon, DashDottedIcon,
DashSolidIcon, DashSolidIcon,
DashDashedIcon, DashDashedIcon,
} from './shared' } from '../shared'
const dashes = { const dashes = {
[DashStyle.Solid]: <DashSolidIcon />, [DashStyle.Solid]: <DashSolidIcon />,

View file

@ -4,7 +4,7 @@ import Tooltip from 'components/tooltip'
import { Circle } from 'react-feather' import { Circle } from 'react-feather'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
import { SizeStyle } from 'types' import { SizeStyle } from 'types'
import { DropdownContent, Item } from './shared' import { DropdownContent, Item } from '../shared'
const sizes = { const sizes = {
[SizeStyle.Small]: 6, [SizeStyle.Small]: 6,

View file

@ -1,219 +0,0 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as RadioGroup from '@radix-ui/react-radio-group'
import * as Panel from '../panel'
import styled from 'styles'
export const StylePanelRoot = styled(Panel.Root, {
minWidth: 1,
width: 184,
maxWidth: 184,
overflow: 'hidden',
position: 'relative',
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
variants: {
isOpen: {
true: {},
false: {
padding: 2,
height: 38,
width: 38,
},
},
},
})
export const Group = styled(RadioGroup.Root, {
display: 'flex',
})
export const Item = styled('button', {
height: '32px',
width: '32px',
backgroundColor: '$panel',
borderRadius: '4px',
padding: '0',
margin: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
outline: 'none',
border: 'none',
pointerEvents: 'all',
cursor: 'pointer',
'&:hover:not(:disabled)': {
backgroundColor: '$hover',
'& svg': {
stroke: '$text',
fill: '$text',
strokeWidth: '0',
},
},
'&:disabled': {
opacity: '0.5',
},
variants: {
isActive: {
true: {
'& svg': {
fill: '$text',
stroke: '$text',
},
},
false: {
'& svg': {
fill: '$inactive',
stroke: '$inactive',
},
},
},
},
})
export const RowButton = styled('button', {
position: 'relative',
display: 'flex',
width: '100%',
background: 'none',
border: 'none',
cursor: 'pointer',
outline: 'none',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 6px 4px 12px',
'&::before': {
content: "''",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
zIndex: -1,
},
'&:hover::before': {
backgroundColor: '$hover',
borderRadius: 4,
},
'& label': {
fontFamily: '$ui',
fontSize: '$2',
fontWeight: '$1',
margin: 0,
padding: 0,
},
'& svg': {
position: 'relative',
stroke: 'rgba(0,0,0,.2)',
strokeWidth: 1,
zIndex: 1,
},
variants: {
size: {
icon: {
padding: '4px ',
width: 'auto',
},
},
},
})
export const IconWrapper = styled('div', {
height: '100%',
borderRadius: '4px',
marginRight: '1px',
display: 'grid',
alignItems: 'center',
justifyContent: 'center',
outline: 'none',
border: 'none',
pointerEvents: 'all',
cursor: 'pointer',
'& svg': {
height: 22,
width: 22,
strokeWidth: 1,
},
'& > *': {
gridRow: 1,
gridColumn: 1,
},
})
export const DropdownContent = styled(DropdownMenu.Content, {
display: 'grid',
padding: 4,
gridTemplateColumns: 'repeat(4, 1fr)',
backgroundColor: '$panel',
borderRadius: 4,
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.28)',
variants: {
direction: {
vertical: {
gridTemplateColumns: '1fr',
},
},
},
})
export function DashSolidIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2}
strokeLinecap="round"
/>
</svg>
)
}
export function DashDashedIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray={50.26548 * 0.1}
/>
</svg>
)
}
const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
export function DashDottedIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray={dottedDasharray}
/>
</svg>
)
}

View file

@ -1,4 +1,4 @@
import { Group, Item } from './shared' import { Group, Item } from '../shared'
import * as RadioGroup from '@radix-ui/react-radio-group' import * as RadioGroup from '@radix-ui/react-radio-group'
import { ChangeEvent } from 'react' import { ChangeEvent } from 'react'
import { Circle } from 'react-feather' import { Circle } from 'react-feather'

View file

@ -3,10 +3,8 @@ import state, { useSelector } from 'state'
import * as Panel from 'components/panel' 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 * as Checkbox from '@radix-ui/react-checkbox' import { ChevronDown, Trash2, X } from 'react-feather'
import { ChevronDown, Square, Tool, Trash2, X } from 'react-feather'
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils' import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
import { strokes } from 'lib/shape-styles'
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'
@ -15,9 +13,7 @@ import {
ArrowUpIcon, ArrowUpIcon,
AspectRatioIcon, AspectRatioIcon,
BoxIcon, BoxIcon,
CheckIcon,
CopyIcon, CopyIcon,
DotsVerticalIcon,
EyeClosedIcon, EyeClosedIcon,
EyeOpenIcon, EyeOpenIcon,
LockClosedIcon, LockClosedIcon,
@ -28,10 +24,7 @@ import {
} from '@radix-ui/react-icons' } from '@radix-ui/react-icons'
import DashPicker from './dash-picker' import DashPicker from './dash-picker'
import QuickColorSelect from './quick-color-select' import QuickColorSelect from './quick-color-select'
import ColorContent from './color-content'
import { RowButton, IconWrapper } from './shared'
import ColorPicker from './color-picker' import ColorPicker from './color-picker'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import IsFilledPicker from './is-filled-picker' import IsFilledPicker from './is-filled-picker'
import QuickSizeSelect from './quick-size-select' import QuickSizeSelect from './quick-size-select'
import QuickdashSelect from './quick-dash-select' import QuickdashSelect from './quick-dash-select'

View file

@ -2,6 +2,7 @@ import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons'
import { IconButton } from 'components/shared' import { IconButton } from 'components/shared'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
import styled from 'styles' import styled from 'styles'
import { getCurrentCamera } from 'utils/utils'
import Tooltip from '../tooltip' import Tooltip from '../tooltip'
const zoomIn = () => state.send('ZOOMED_IN') const zoomIn = () => state.send('ZOOMED_IN')
@ -30,10 +31,11 @@ export default function Zoom() {
} }
function ZoomCounter() { function ZoomCounter() {
const camera = useSelector((s) => s.data.camera) const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
return ( return (
<ZoomButton onClick={zoomToActual} onDoubleClick={zoomToFit}> <ZoomButton onClick={zoomToActual} onDoubleClick={zoomToFit}>
{Math.round(camera.zoom * 100)}% {Math.round(zoom * 100)}%
</ZoomButton> </ZoomButton>
) )
} }

View file

@ -1,5 +1,6 @@
import React, { useEffect } from "react" import React, { useEffect } from 'react'
import state from "state" import state from 'state'
import { getCurrentCamera } from 'utils/utils'
/** /**
* When the state's camera changes, update the transform of * When the state's camera changes, update the transform of
@ -8,24 +9,27 @@ import state from "state"
*/ */
export default function useCamera(ref: React.MutableRefObject<SVGGElement>) { export default function useCamera(ref: React.MutableRefObject<SVGGElement>) {
useEffect(() => { useEffect(() => {
let { camera } = state.data let prev = getCurrentCamera(state.data)
return state.onUpdate(({ data }) => { return state.onUpdate(() => {
const g = ref.current const g = ref.current
if (!g) return if (!g) return
const { point, zoom } = data.camera const { point, zoom } = getCurrentCamera(state.data)
if (point !== camera.point || zoom !== camera.zoom) { if (point !== prev.point || zoom !== prev.zoom) {
g.setAttribute( g.setAttribute(
"transform", 'transform',
`scale(${zoom}) translate(${point[0]} ${point[1]})` `scale(${zoom}) translate(${point[0]} ${point[1]})`
) )
localStorage.setItem("code_slate_camera", JSON.stringify(data.camera)) localStorage.setItem(
} 'code_slate_camera',
JSON.stringify({ point, zoom })
)
camera = data.camera prev = getCurrentCamera(state.data)
}
}) })
}, [state]) }, [state])
} }

View file

@ -0,0 +1,24 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { getPage, getSelectedShapes } from 'utils/utils'
import { getShapeUtils } from 'lib/shape-utils'
import * as vec from 'utils/vec'
export default function nudgeCommand(data: Data, pageId: string) {
const { currentPageId: prevPageId } = data
history.execute(
data,
new Command({
name: 'change_page',
category: 'canvas',
do(data) {
data.currentPageId = pageId
},
undo(data) {
data.currentPageId = prevPageId
},
})
)
}

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, getSelectedShapes } from 'utils/utils' import { getCurrentCamera, getPage, getSelectedShapes } 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'
@ -12,7 +12,7 @@ export default function duplicateCommand(data: Data) {
const duplicates = selectedShapes.map((shape) => ({ const duplicates = selectedShapes.map((shape) => ({
...shape, ...shape,
id: uuid(), id: uuid(),
point: vec.add(shape.point, vec.div([16, 16], data.camera.zoom)), point: vec.add(shape.point, vec.div([16, 16], getCurrentCamera(data).zoom)),
})) }))
history.execute( history.execute(

View file

@ -1,5 +1,6 @@
import align from './align' import align from './align'
import arrow from './arrow' import arrow from './arrow'
import changePage from './change-page'
import deleteSelected from './delete-selected' import deleteSelected from './delete-selected'
import direct from './direct' import direct from './direct'
import distribute from './distribute' import distribute from './distribute'
@ -21,6 +22,7 @@ import handle from './handle'
const commands = { const commands = {
align, align,
arrow, arrow,
changePage,
deleteSelected, deleteSelected,
direct, direct,
distribute, distribute,

View file

@ -10,8 +10,6 @@ export default function transformSingleCommand(
data: Data, data: Data,
before: TransformSingleSnapshot, before: TransformSingleSnapshot,
after: TransformSingleSnapshot, after: TransformSingleSnapshot,
scaleX: number,
scaleY: number,
isCreating: boolean isCreating: boolean
) { ) {
const shape = current(getPage(data, after.currentPageId).shapes[after.id]) const shape = current(getPage(data, after.currentPageId).shapes[after.id])
@ -23,24 +21,14 @@ export default function transformSingleCommand(
category: 'canvas', category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const { id, type, initialShapeBounds } = after const { id } = after
const { shapes } = getPage(data, after.currentPageId) const { shapes } = getPage(data, after.currentPageId)
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(id) data.selectedIds.add(id)
if (isCreating) {
shapes[id] = shape shapes[id] = shape
} else {
getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
type,
initialShape: before.initialShape,
scaleX,
scaleY,
transformOrigin: [0.5, 0.5],
})
}
}, },
undo(data) { undo(data) {
const { id, type, initialShapeBounds } = before const { id, type, initialShapeBounds } = before

View file

@ -128,6 +128,13 @@ export const defaultDocument: Data['document'] = {
// }), // }),
}, },
}, },
page1: {
id: 'page1',
type: 'page',
name: 'Page 1',
childIndex: 1,
shapes: {},
},
}, },
code: { code: {
file0: { file0: {

View file

@ -106,7 +106,7 @@ class History extends BaseHistory<Data> {
} }
restoreSavedData(data: any): Data { restoreSavedData(data: any): Data {
const restoredData = { ...data } const restoredData: Data = { ...data }
restoredData.selectedIds = new Set(restoredData.selectedIds) restoredData.selectedIds = new Set(restoredData.selectedIds)
@ -114,12 +114,15 @@ class History extends BaseHistory<Data> {
const cameraInfo = localStorage.getItem('code_slate_camera') const cameraInfo = localStorage.getItem('code_slate_camera')
if (cameraInfo !== null) { if (cameraInfo !== null) {
Object.assign(restoredData.camera, JSON.parse(cameraInfo)) Object.assign(
restoredData.pageStates[data.currentPageId].camera,
JSON.parse(cameraInfo)
)
// And update the CSS property // And update the CSS property
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
'--camera-zoom', '--camera-zoom',
restoredData.camera.zoom.toString() restoredData.pageStates[data.currentPageId].camera.zoom.toString()
) )
} }

View file

@ -85,8 +85,6 @@ export default class TransformSingleSession extends BaseSession {
data, data,
this.snapshot, this.snapshot,
getTransformSingleSnapshot(data, this.transformType), getTransformSingleSnapshot(data, this.transformType),
this.scaleX,
this.scaleY,
this.isCreating this.isCreating
) )
} }

View file

@ -12,6 +12,7 @@ import {
getChildren, getChildren,
getCommonBounds, getCommonBounds,
getCurrent, getCurrent,
getCurrentCamera,
getPage, getPage,
getSelectedBounds, getSelectedBounds,
getShape, getShape,
@ -54,10 +55,6 @@ const initialData: Data = {
dash: DashStyle.Solid, dash: DashStyle.Solid,
isFilled: false, isFilled: false,
}, },
camera: {
point: [0, 0],
zoom: 1,
},
activeTool: 'select', activeTool: 'select',
brush: undefined, brush: undefined,
boundsRotation: 0, boundsRotation: 0,
@ -68,6 +65,20 @@ const initialData: Data = {
currentCodeFileId: 'file0', currentCodeFileId: 'file0',
codeControls: {}, codeControls: {},
document: defaultDocument, document: defaultDocument,
pageStates: {
page0: {
camera: {
point: [0, 0],
zoom: 1,
},
},
page1: {
camera: {
point: [0, 0],
zoom: 1,
},
},
},
} }
const state = createState({ const state = createState({
@ -139,6 +150,7 @@ const state = createState({
USED_PEN_DEVICE: 'enablePenLock', USED_PEN_DEVICE: 'enablePenLock',
DISABLED_PEN_LOCK: 'disablePenLock', DISABLED_PEN_LOCK: 'disablePenLock',
CLEARED_PAGE: ['selectAll', 'deleteSelection'], CLEARED_PAGE: ['selectAll', 'deleteSelection'],
CHANGED_CURRENT_PAGE: ['clearSelectedIds', 'setCurrentPage'],
}, },
initial: 'selecting', initial: 'selecting',
states: { states: {
@ -732,6 +744,11 @@ const state = createState({
}, },
}, },
actions: { actions: {
/* ---------------------- Pages --------------------- */
setCurrentPage(data, payload: { id: string }) {
commands.changePage(data, payload.id)
},
/* --------------------- Shapes --------------------- */ /* --------------------- Shapes --------------------- */
createShape(data, payload, type: ShapeType) { createShape(data, payload, type: ShapeType) {
const shape = createShape(type, { const shape = createShape(type, {
@ -1062,7 +1079,7 @@ const state = createState({
/* --------------------- Camera --------------------- */ /* --------------------- Camera --------------------- */
zoomIn(data) { zoomIn(data) {
const { camera } = data const camera = getCurrentCamera(data)
const i = Math.round((camera.zoom * 100) / 25) const i = Math.round((camera.zoom * 100) / 25)
const center = [window.innerWidth / 2, window.innerHeight / 2] const center = [window.innerWidth / 2, window.innerHeight / 2]
@ -1074,7 +1091,7 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
zoomOut(data) { zoomOut(data) {
const { camera } = data const camera = getCurrentCamera(data)
const i = Math.round((camera.zoom * 100) / 25) const i = Math.round((camera.zoom * 100) / 25)
const center = [window.innerWidth / 2, window.innerHeight / 2] const center = [window.innerWidth / 2, window.innerHeight / 2]
@ -1086,8 +1103,7 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
zoomCameraToActual(data) { zoomCameraToActual(data) {
const { camera } = data const camera = getCurrentCamera(data)
const center = [window.innerWidth / 2, window.innerHeight / 2] const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data) const p0 = screenToWorld(center, data)
@ -1098,7 +1114,7 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
zoomCameraToSelectionActual(data) { zoomCameraToSelectionActual(data) {
const { camera } = data const camera = getCurrentCamera(data)
const bounds = getSelectedBounds(data) const bounds = getSelectedBounds(data)
@ -1111,8 +1127,7 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
zoomCameraToSelection(data) { zoomCameraToSelection(data) {
const { camera } = data const camera = getCurrentCamera(data)
const bounds = getSelectedBounds(data) const bounds = getSelectedBounds(data)
const zoom = getCameraZoom( const zoom = getCameraZoom(
@ -1130,7 +1145,7 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
zoomCameraToFit(data) { zoomCameraToFit(data) {
const { camera } = data const camera = getCurrentCamera(data)
const page = getPage(data) const page = getPage(data)
const shapes = Object.values(page.shapes) const shapes = Object.values(page.shapes)
@ -1160,7 +1175,7 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
zoomCamera(data, payload: { delta: number; point: number[] }) { zoomCamera(data, payload: { delta: number; point: number[] }) {
const { camera } = data const camera = getCurrentCamera(data)
const next = camera.zoom - (payload.delta / 100) * camera.zoom const next = camera.zoom - (payload.delta / 100) * camera.zoom
const p0 = screenToWorld(payload.point, data) const p0 = screenToWorld(payload.point, data)
@ -1171,7 +1186,7 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
panCamera(data, payload: { delta: number[] }) { panCamera(data, payload: { delta: number[] }) {
const { camera } = data const camera = getCurrentCamera(data)
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom)) camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
}, },
pinchCamera( pinchCamera(
@ -1183,8 +1198,7 @@ const state = createState({
point: number[] point: number[]
} }
) { ) {
const { camera } = data const camera = getCurrentCamera(data)
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom)) camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
@ -1197,9 +1211,9 @@ const state = createState({
setZoomCSS(camera.zoom) setZoomCSS(camera.zoom)
}, },
resetCamera(data) { resetCamera(data) {
data.camera.zoom = 1 const camera = getCurrentCamera(data)
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2] camera.zoom = 1
camera.point = [window.innerWidth / 2, window.innerHeight / 2]
document.documentElement.style.setProperty('--camera-zoom', '1') document.documentElement.style.setProperty('--camera-zoom', '1')
}, },

View file

@ -19,10 +19,6 @@ export interface Data {
isPenLocked: boolean isPenLocked: boolean
} }
currentStyle: ShapeStyles currentStyle: ShapeStyles
camera: {
point: number[]
zoom: number
}
activeTool: ShapeType | 'select' activeTool: ShapeType | 'select'
brush?: Bounds brush?: Bounds
boundsRotation: number boundsRotation: number
@ -36,6 +32,15 @@ export interface Data {
pages: Record<string, Page> pages: Record<string, Page>
code: Record<string, CodeFile> code: Record<string, CodeFile>
} }
pageStates: Record<
string,
{
camera: {
point: number[]
zoom: number
}
}
>
} }
/* -------------------------------------------------- */ /* -------------------------------------------------- */

View file

@ -6,7 +6,8 @@ import _isMobile from 'ismobilejs'
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
export function screenToWorld(point: number[], data: Data) { export function screenToWorld(point: number[], data: Data) {
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point) const camera = getCurrentCamera(data)
return vec.sub(vec.div(point, camera.zoom), camera.point)
} }
/** /**
@ -1581,3 +1582,7 @@ export function isAngleBetween(a: number, b: number, c: number) {
const AC = (c - a + PI2) % PI2 const AC = (c - a + PI2) % PI2
return AB <= Math.PI !== AC > AB return AB <= Math.PI !== AC > AB
} }
export function getCurrentCamera(data: Data) {
return data.pageStates[data.currentPageId].camera
}