diff --git a/components/canvas/bounds/bounding-box.tsx b/components/canvas/bounds/bounding-box.tsx index 96a34af5d..481037048 100644 --- a/components/canvas/bounds/bounding-box.tsx +++ b/components/canvas/bounds/bounding-box.tsx @@ -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( diff --git a/components/canvas/defs.tsx b/components/canvas/defs.tsx index b9bb36cc9..abd35d041 100644 --- a/components/canvas/defs.tsx +++ b/components/canvas/defs.tsx @@ -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) diff --git a/components/editor.tsx b/components/editor.tsx index 33a65e30b..97a0cb1e7 100644 --- a/components/editor.tsx +++ b/components/editor.tsx @@ -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 ( + {hasControls && } diff --git a/components/page-panel/page-panel.tsx b/components/page-panel/page-panel.tsx new file mode 100644 index 000000000..f0ee2f11d --- /dev/null +++ b/components/page-panel/page-panel.tsx @@ -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 ( + + + + + {documentPages[currentPageId].name} + + + + + + + + state.send('CHANGED_CURRENT_PAGE', { id }) + } + > + {sorted.map(({ id, name }) => ( + + {name} + + + + + ))} + + + + + + + ) +} + +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, +}) diff --git a/components/shared.tsx b/components/shared.tsx index 1930dcd06..3810f1c68 100644 --- a/components/shared.tsx +++ b/components/shared.tsx @@ -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 ( + + + + ) +} + +export function DashDashedIcon() { + return ( + + + + ) +} + +const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}` + +export function DashDottedIcon() { + return ( + + + + ) +} diff --git a/components/style-panel/color-content.tsx b/components/style-panel/color-content.tsx index ed169d5b1..e9ce63a42 100644 --- a/components/style-panel/color-content.tsx +++ b/components/style-panel/color-content.tsx @@ -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, diff --git a/components/style-panel/color-picker.tsx b/components/style-panel/color-picker.tsx index 5140c24a7..8566bd479 100644 --- a/components/style-panel/color-picker.tsx +++ b/components/style-panel/color-picker.tsx @@ -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' diff --git a/components/style-panel/dash-picker.tsx b/components/style-panel/dash-picker.tsx index 335e5e7dc..84506d3f4 100644 --- a/components/style-panel/dash-picker.tsx +++ b/components/style-panel/dash-picker.tsx @@ -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' diff --git a/components/style-panel/is-filled-picker.tsx b/components/style-panel/is-filled-picker.tsx index 27bc840c8..ab8a8d521 100644 --- a/components/style-panel/is-filled-picker.tsx +++ b/components/style-panel/is-filled-picker.tsx @@ -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 diff --git a/components/style-panel/quick-dash-select.tsx b/components/style-panel/quick-dash-select.tsx index cdf4380ea..51d0fbc41 100644 --- a/components/style-panel/quick-dash-select.tsx +++ b/components/style-panel/quick-dash-select.tsx @@ -9,7 +9,7 @@ import { DashDottedIcon, DashSolidIcon, DashDashedIcon, -} from './shared' +} from '../shared' const dashes = { [DashStyle.Solid]: , diff --git a/components/style-panel/quick-size-select.tsx b/components/style-panel/quick-size-select.tsx index 2ab35d4af..d3ec80e74 100644 --- a/components/style-panel/quick-size-select.tsx +++ b/components/style-panel/quick-size-select.tsx @@ -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, diff --git a/components/style-panel/shared.tsx b/components/style-panel/shared.tsx deleted file mode 100644 index b5507e4ea..000000000 --- a/components/style-panel/shared.tsx +++ /dev/null @@ -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 ( - - - - ) -} - -export function DashDashedIcon() { - return ( - - - - ) -} - -const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}` - -export function DashDottedIcon() { - return ( - - - - ) -} diff --git a/components/style-panel/size-picker.tsx b/components/style-panel/size-picker.tsx index ea883807c..226161c54 100644 --- a/components/style-panel/size-picker.tsx +++ b/components/style-panel/size-picker.tsx @@ -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' diff --git a/components/style-panel/style-panel.tsx b/components/style-panel/style-panel.tsx index c37594d21..1358b58fa 100644 --- a/components/style-panel/style-panel.tsx +++ b/components/style-panel/style-panel.tsx @@ -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' diff --git a/components/tools-panel/zoom.tsx b/components/tools-panel/zoom.tsx index 45f44ad58..90596ce9b 100644 --- a/components/tools-panel/zoom.tsx +++ b/components/tools-panel/zoom.tsx @@ -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 ( - {Math.round(camera.zoom * 100)}% + {Math.round(zoom * 100)}% ) } diff --git a/hooks/useCamera.ts b/hooks/useCamera.ts index 8b02bf78c..6547132df 100644 --- a/hooks/useCamera.ts +++ b/hooks/useCamera.ts @@ -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) { 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]) } diff --git a/state/commands/change-page.ts b/state/commands/change-page.ts new file mode 100644 index 000000000..7231c40ce --- /dev/null +++ b/state/commands/change-page.ts @@ -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 + }, + }) + ) +} diff --git a/state/commands/duplicate.ts b/state/commands/duplicate.ts index 5213dffe2..b4a3dc79c 100644 --- a/state/commands/duplicate.ts +++ b/state/commands/duplicate.ts @@ -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( diff --git a/state/commands/index.ts b/state/commands/index.ts index eea864a24..a12c43c17 100644 --- a/state/commands/index.ts +++ b/state/commands/index.ts @@ -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, diff --git a/state/commands/transform-single.ts b/state/commands/transform-single.ts index 10a8033ae..c98dcc3b3 100644 --- a/state/commands/transform-single.ts +++ b/state/commands/transform-single.ts @@ -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], - }) - } + shapes[id] = shape }, undo(data) { const { id, type, initialShapeBounds } = before diff --git a/state/data.ts b/state/data.ts index de717dcdd..9b37c1d15 100644 --- a/state/data.ts +++ b/state/data.ts @@ -128,6 +128,13 @@ export const defaultDocument: Data['document'] = { // }), }, }, + page1: { + id: 'page1', + type: 'page', + name: 'Page 1', + childIndex: 1, + shapes: {}, + }, }, code: { file0: { diff --git a/state/history.ts b/state/history.ts index 2700ea85f..4465258d4 100644 --- a/state/history.ts +++ b/state/history.ts @@ -106,7 +106,7 @@ class History extends BaseHistory { } 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 { 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() ) } diff --git a/state/sessions/transform-single-session.ts b/state/sessions/transform-single-session.ts index a298a760f..27e617cbf 100644 --- a/state/sessions/transform-single-session.ts +++ b/state/sessions/transform-single-session.ts @@ -85,8 +85,6 @@ export default class TransformSingleSession extends BaseSession { data, this.snapshot, getTransformSingleSnapshot(data, this.transformType), - this.scaleX, - this.scaleY, this.isCreating ) } diff --git a/state/state.ts b/state/state.ts index f2f4b1ea4..4bb0c221b 100644 --- a/state/state.ts +++ b/state/state.ts @@ -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') }, diff --git a/types.ts b/types.ts index b0430bf39..dc9b3ea57 100644 --- a/types.ts +++ b/types.ts @@ -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 code: Record } + pageStates: Record< + string, + { + camera: { + point: number[] + zoom: number + } + } + > } /* -------------------------------------------------- */ diff --git a/utils/utils.ts b/utils/utils.ts index 41c4b1045..b9375dc20 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -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 +}