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 { Edge, Corner, LineShape, ArrowShape } from 'types'
import { Edge, Corner } from 'types'
import { useSelector } from 'state'
import {
deepCompareArrays,
getCurrentCamera,
getPage,
getSelectedShapes,
isMobile,
@ -17,7 +18,7 @@ import Handles from './handles'
export default function Bounds() {
const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
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 selectedIds = useSelector(

View file

@ -1,10 +1,10 @@
import { getShapeUtils } from 'lib/shape-utils'
import { memo } from 'react'
import { useSelector } from 'state'
import { deepCompareArrays, getPage } from 'utils/utils'
import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
export default function Defs() {
const zoom = useSelector((s) => s.data.camera.zoom)
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
const currentPageShapeIds = useSelector(({ data }) => {
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 { useSelector } from 'state'
import styled from 'styles'
import PagePanel from './page-panel/page-panel'
export default function Editor() {
useKeyboardEvents()
@ -20,6 +21,7 @@ export default function Editor() {
return (
<Layout>
<Canvas />
<PagePanel />
<LeftPanels>
<CodePanel />
{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'
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 { Square } from 'react-feather'
import styled from 'styles'
import { DropdownContent } from './shared'
import { DropdownContent } from '../shared'
export default function ColorContent({
onChange,

View file

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

View file

@ -4,7 +4,7 @@ import {
DashDashedIcon,
DashDottedIcon,
DashSolidIcon,
} from './shared'
} from '../shared'
import * as RadioGroup from '@radix-ui/react-radio-group'
import { DashStyle } from 'types'
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 { strokes } from 'lib/shape-styles'
import { Square } from 'react-feather'
import { IconWrapper, RowButton } from './shared'
import { IconWrapper, RowButton } from '../shared'
interface Props {
isFilled: boolean

View file

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

View file

@ -4,7 +4,7 @@ import Tooltip from 'components/tooltip'
import { Circle } from 'react-feather'
import state, { useSelector } from 'state'
import { SizeStyle } from 'types'
import { DropdownContent, Item } from './shared'
import { DropdownContent, Item } from '../shared'
const sizes = {
[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 { ChangeEvent } from 'react'
import { Circle } from 'react-feather'

View file

@ -3,10 +3,8 @@ import state, { useSelector } from 'state'
import * as Panel from 'components/panel'
import { useRef } from 'react'
import { IconButton } from 'components/shared'
import * as Checkbox from '@radix-ui/react-checkbox'
import { ChevronDown, Square, Tool, Trash2, X } from 'react-feather'
import { ChevronDown, Trash2, X } from 'react-feather'
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
import { strokes } from 'lib/shape-styles'
import AlignDistribute from './align-distribute'
import { MoveType } from 'types'
import SizePicker from './size-picker'
@ -15,9 +13,7 @@ import {
ArrowUpIcon,
AspectRatioIcon,
BoxIcon,
CheckIcon,
CopyIcon,
DotsVerticalIcon,
EyeClosedIcon,
EyeOpenIcon,
LockClosedIcon,
@ -28,10 +24,7 @@ import {
} from '@radix-ui/react-icons'
import DashPicker from './dash-picker'
import QuickColorSelect from './quick-color-select'
import ColorContent from './color-content'
import { RowButton, IconWrapper } from './shared'
import ColorPicker from './color-picker'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import IsFilledPicker from './is-filled-picker'
import QuickSizeSelect from './quick-size-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 state, { useSelector } from 'state'
import styled from 'styles'
import { getCurrentCamera } from 'utils/utils'
import Tooltip from '../tooltip'
const zoomIn = () => state.send('ZOOMED_IN')
@ -30,10 +31,11 @@ export default function Zoom() {
}
function ZoomCounter() {
const camera = useSelector((s) => s.data.camera)
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
return (
<ZoomButton onClick={zoomToActual} onDoubleClick={zoomToFit}>
{Math.round(camera.zoom * 100)}%
{Math.round(zoom * 100)}%
</ZoomButton>
)
}

View file

@ -1,5 +1,6 @@
import React, { useEffect } from "react"
import state from "state"
import React, { useEffect } from 'react'
import state from 'state'
import { getCurrentCamera } from 'utils/utils'
/**
* 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>) {
useEffect(() => {
let { camera } = state.data
let prev = getCurrentCamera(state.data)
return state.onUpdate(({ data }) => {
return state.onUpdate(() => {
const g = ref.current
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(
"transform",
'transform',
`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])
}

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 history from '../history'
import { Data } from 'types'
import { getPage, getSelectedShapes } from 'utils/utils'
import { getCurrentCamera, getPage, getSelectedShapes } from 'utils/utils'
import { v4 as uuid } from 'uuid'
import { current } from 'immer'
import * as vec from 'utils/vec'
@ -12,7 +12,7 @@ export default function duplicateCommand(data: Data) {
const duplicates = selectedShapes.map((shape) => ({
...shape,
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(

View file

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

View file

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

View file

@ -106,7 +106,7 @@ class History extends BaseHistory<Data> {
}
restoreSavedData(data: any): Data {
const restoredData = { ...data }
const restoredData: Data = { ...data }
restoredData.selectedIds = new Set(restoredData.selectedIds)
@ -114,12 +114,15 @@ class History extends BaseHistory<Data> {
const cameraInfo = localStorage.getItem('code_slate_camera')
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
document.documentElement.style.setProperty(
'--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,
this.snapshot,
getTransformSingleSnapshot(data, this.transformType),
this.scaleX,
this.scaleY,
this.isCreating
)
}

View file

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

View file

@ -19,10 +19,6 @@ export interface Data {
isPenLocked: boolean
}
currentStyle: ShapeStyles
camera: {
point: number[]
zoom: number
}
activeTool: ShapeType | 'select'
brush?: Bounds
boundsRotation: number
@ -36,6 +32,15 @@ export interface Data {
pages: Record<string, Page>
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'
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
return AB <= Math.PI !== AC > AB
}
export function getCurrentCamera(data: Data) {
return data.pageStates[data.currentPageId].camera
}