Adds page control, pages
This commit is contained in:
parent
34256f992a
commit
5ba56216d0
26 changed files with 460 additions and 295 deletions
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 />}
|
||||||
|
|
100
components/page-panel/page-panel.tsx
Normal file
100
components/page-panel/page-panel.tsx
Normal 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,
|
||||||
|
})
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 />,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
24
state/commands/change-page.ts
Normal file
24
state/commands/change-page.ts
Normal 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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -128,6 +128,13 @@ export const defaultDocument: Data['document'] = {
|
||||||
// }),
|
// }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
page1: {
|
||||||
|
id: 'page1',
|
||||||
|
type: 'page',
|
||||||
|
name: 'Page 1',
|
||||||
|
childIndex: 1,
|
||||||
|
shapes: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
file0: {
|
file0: {
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
13
types.ts
13
types.ts
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue