Merge pull request #3 from steveruizok/coop

Coop
This commit is contained in:
Steve Ruiz 2021-06-29 16:01:30 +01:00 committed by GitHub
commit 5c11db8569
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1649 additions and 1166 deletions

View file

@ -1,7 +1,7 @@
import state from 'state'
import { generateFromCode } from 'state/code/generate'
import { getShape, getShapes } from 'utils'
import * as json from './__mocks__/document.json'
import tld from 'utils/tld'
jest.useRealTimers()
@ -19,7 +19,7 @@ describe('selection', () => {
})
it('saves changes to code', () => {
expect(getShapes(state.data).length).toBe(0)
expect(tld.getShapes(state.data).length).toBe(0)
const code = `// hello world!`
@ -49,7 +49,7 @@ describe('selection', () => {
state.send('GENERATED_FROM_CODE', { controls, shapes })
expect(getShapes(state.data)).toMatchSnapshot(
expect(tld.getShapes(state.data)).toMatchSnapshot(
'generated rectangle from code'
)
})
@ -106,7 +106,7 @@ describe('selection', () => {
'data in state after changing control'
)
expect(getShape(state.data, 'test-rectangle')).toMatchSnapshot(
expect(tld.getShape(state.data, 'test-rectangle')).toMatchSnapshot(
'rectangle in state after changing code control'
)
})
@ -116,7 +116,7 @@ describe('selection', () => {
it('does not saves changes to code when readonly', () => {
state.send('CLEARED_PAGE')
expect(getShapes(state.data).length).toBe(0)
expect(tld.getShapes(state.data).length).toBe(0)
const code = `// hello world!`
@ -190,7 +190,7 @@ describe('selection', () => {
state.send('GENERATED_FROM_CODE', { controls, shapes })
expect(getShapes(state.data)).toMatchSnapshot(
expect(tld.getShapes(state.data)).toMatchSnapshot(
'generated rectangle from code'
)
})
@ -220,7 +220,9 @@ describe('selection', () => {
state.send('GENERATED_FROM_CODE', { controls, shapes })
expect(getShapes(state.data)).toMatchSnapshot('generated ellipse from code')
expect(tld.getShapes(state.data)).toMatchSnapshot(
'generated ellipse from code'
)
})
it('generates a draw shape', async () => {
@ -242,7 +244,9 @@ describe('selection', () => {
state.send('GENERATED_FROM_CODE', { controls, shapes })
expect(getShapes(state.data)).toMatchSnapshot('generated draw from code')
expect(tld.getShapes(state.data)).toMatchSnapshot(
'generated draw from code'
)
})
it('generates an arrow shape', async () => {
@ -264,7 +268,9 @@ describe('selection', () => {
state.send('GENERATED_FROM_CODE', { controls, shapes })
expect(getShapes(state.data)).toMatchSnapshot('generated draw from code')
expect(tld.getShapes(state.data)).toMatchSnapshot(
'generated draw from code'
)
})
it('generates a text shape', async () => {
@ -287,6 +293,8 @@ describe('selection', () => {
state.send('GENERATED_FROM_CODE', { controls, shapes })
expect(getShapes(state.data)).toMatchSnapshot('generated draw from code')
expect(tld.getShapes(state.data)).toMatchSnapshot(
'generated draw from code'
)
})
})

View file

@ -1,7 +1,6 @@
import state from 'state'
import inputs from 'state/inputs'
import { ShapeType } from 'types'
import { getShape } from 'utils'
import {
idsAreSelected,
point,
@ -10,6 +9,7 @@ import {
getOnlySelectedShape,
assertShapeProps,
} from './test-utils'
import tld from 'utils/tld'
import * as json from './__mocks__/document.json'
describe('deleting single shapes', () => {
@ -27,16 +27,16 @@ describe('deleting single shapes', () => {
state.send('DELETED')
expect(idsAreSelected(state.data, [])).toBe(true)
expect(getShape(state.data, rectangleId)).toBe(undefined)
expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
state.send('UNDO')
expect(getShape(state.data, rectangleId)).toBeTruthy()
expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
state.send('REDO')
expect(getShape(state.data, rectangleId)).toBe(undefined)
expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
state.send('UNDO')
})
@ -69,7 +69,7 @@ describe('deletes and restores grouped shapes', () => {
// Should select the group
expect(assertShapeProps(group, { type: ShapeType.Group }))
const arrow = getShape(state.data, arrowId)
const arrow = tld.getShape(state.data, arrowId)
// The arrow should be have the group as its parent
expect(assertShapeProps(arrow, { parentId: group.id }))
@ -142,16 +142,16 @@ describe('deletes and restores grouped shapes', () => {
// // // Delete the rectangle first
// // state.send('UNDO')
// // expect(getShape(state.data, rectangleId)).toBeTruthy()
// // expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
// // expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
// // state.send('REDO')
// // expect(getShape(state.data, rectangleId)).toBe(undefined)
// // expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
// // state.send('UNDO')
// // expect(getShape(state.data, rectangleId)).toBeTruthy()
// // expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
// // expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
// // })
// })

View file

@ -1,5 +1,5 @@
import { Data, Shape, ShapeType } from 'types'
import { getSelectedIds, getSelectedShapes, getShape } from 'utils'
import tld from 'utils/tld'
export const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
@ -40,7 +40,7 @@ export function idsAreSelected(
ids: string[],
strict = true
): boolean {
const selectedIds = getSelectedIds(data)
const selectedIds = tld.getSelectedIds(data)
return (
(strict ? selectedIds.size === ids.length : true) &&
ids.every((id) => selectedIds.has(id))
@ -52,11 +52,11 @@ export function hasParent(
childId: string,
parentId: string
): boolean {
return getShape(data, childId).parentId === parentId
return tld.getShape(data, childId).parentId === parentId
}
export function getOnlySelectedShape(data: Data): Shape {
const selectedShapes = getSelectedShapes(data)
const selectedShapes = tld.getSelectedShapes(data)
return selectedShapes.length === 1 ? selectedShapes[0] : undefined
}
@ -65,7 +65,7 @@ export function assertShapeType(
shapeId: string,
type: ShapeType
): boolean {
const shape = getShape(data, shapeId)
const shape = tld.getShape(data, shapeId)
if (shape.type !== type) {
throw new TypeError(
`expected shape ${shapeId} to be of type ${type}, found ${shape?.type} instead`

View file

@ -1,13 +1,8 @@
import * as React from 'react'
import { Edge, Corner } from 'types'
import { useSelector } from 'state'
import {
getBoundsCenter,
getCurrentCamera,
getPage,
getSelectedShapes,
isMobile,
} from 'utils'
import { getBoundsCenter, isMobile } from 'utils'
import tld from 'utils/tld'
import CenterHandle from './center-handle'
import CornerHandle from './corner-handle'
import EdgeHandle from './edge-handle'
@ -18,23 +13,23 @@ export default function Bounds(): JSX.Element {
const isSelecting = useSelector((s) => s.isIn('selecting'))
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
const zoom = useSelector((s) => tld.getCurrentCamera(s.data).zoom)
const bounds = useSelector((s) => s.values.selectedBounds)
const rotation = useSelector((s) =>
s.values.selectedIds.length === 1
? getSelectedShapes(s.data)[0].rotation
? tld.getSelectedShapes(s.data)[0].rotation
: 0
)
const isAllLocked = useSelector((s) => {
const page = getPage(s.data)
const page = tld.getPage(s.data)
return s.values.selectedIds.every((id) => page.shapes[id]?.isLocked)
})
const isSingleHandles = useSelector((s) => {
const page = getPage(s.data)
const page = tld.getPage(s.data)
return (
s.values.selectedIds.length === 1 &&
page.shapes[s.values.selectedIds[0]]?.handles !== undefined

View file

@ -2,7 +2,7 @@ import { useRef } from 'react'
import state, { useSelector } from 'state'
import inputs from 'state/inputs'
import styled from 'styles'
import { getPage } from 'utils'
import tld from 'utils/tld'
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
if (!inputs.canAccept(e.pointerId)) return
@ -36,7 +36,7 @@ export default function BoundsBg(): JSX.Element {
if (selectedIds.length === 1) {
const selected = selectedIds[0]
const page = getPage(s.data)
const page = tld.getPage(s.data)
return page.shapes[selected]?.rotation
} else {
@ -48,7 +48,7 @@ export default function BoundsBg(): JSX.Element {
const selectedIds = s.values.selectedIds
if (selectedIds.length === 1) {
const page = getPage(s.data)
const page = tld.getPage(s.data)
const selected = selectedIds[0]
return (

View file

@ -3,14 +3,14 @@ import { getShapeUtils } from 'state/shape-utils'
import { useRef } from 'react'
import { useSelector } from 'state'
import styled from 'styles'
import { getPage } from 'utils'
import tld from 'utils/tld'
import vec from 'utils/vec'
export default function Handles(): JSX.Element {
const shape = useSelector(
(s) =>
s.values.selectedIds.length === 1 &&
getPage(s.data).shapes[s.values.selectedIds[0]]
tld.getPage(s.data).shapes[s.values.selectedIds[0]]
)
const isSelecting = useSelector((s) =>

View file

@ -7,11 +7,13 @@ import useCamera from 'hooks/useCamera'
import Defs from './defs'
import Page from './page'
import Brush from './brush'
import Cursor from './cursor'
import Bounds from './bounds/bounding-box'
import BoundsBg from './bounds/bounds-bg'
import Handles from './bounds/handles'
import useCanvasEvents from 'hooks/useCanvasEvents'
import ContextMenu from './context-menu/context-menu'
import { deepCompareArrays } from 'utils'
function resetError() {
null
@ -41,6 +43,7 @@ export default function Canvas(): JSX.Element {
<Bounds />
<Handles />
<Brush />
<Peers />
</g>
)}
</ErrorBoundary>
@ -49,6 +52,31 @@ export default function Canvas(): JSX.Element {
)
}
function Peers(): JSX.Element {
const peerIds = useSelector((s) => {
return s.data.room ? Object.keys(s.data.room?.peers) : []
}, deepCompareArrays)
return (
<>
{peerIds.map((id) => (
<Peer key={id} id={id} />
))}
</>
)
}
function Peer({ id }: { id: string }): JSX.Element {
const hasPeer = useSelector((s) => {
return s.data.room && s.data.room.peers[id] !== undefined
})
const point = useSelector(
(s) => hasPeer && s.data.room.peers[id].cursor.point
)
return <Cursor point={point} />
}
const MainSVG = styled('svg', {
position: 'fixed',
overflow: 'hidden',

View file

@ -4,8 +4,9 @@ import {
IconWrapper,
IconButton as _IconButton,
RowButton,
breakpoints,
} from 'components/shared'
import { commandKey, deepCompareArrays, getShape, isMobile } from 'utils'
import { commandKey, deepCompareArrays, isMobile } from 'utils'
import state, { useSelector } from 'state'
import {
AlignType,
@ -14,6 +15,7 @@ import {
ShapeType,
StretchType,
} from 'types'
import tld from 'utils/tld'
import React, { useRef } from 'react'
import {
ChevronRightIcon,
@ -82,7 +84,9 @@ export default function ContextMenu({
const rContent = useRef<HTMLDivElement>(null)
const hasGroupSelected = useSelector((s) =>
selectedShapeIds.some((id) => getShape(s.data, id).type === ShapeType.Group)
selectedShapeIds.some(
(id) => tld.getShape(s.data, id).type === ShapeType.Group
)
)
const hasTwoOrMore = selectedShapeIds.length > 1
@ -300,7 +304,7 @@ function Button({
<_ContextMenu.Item
as={RowButton}
disabled={disabled}
bp={{ '@initial': 'mobile', '@sm': 'small' }}
bp={breakpoints}
onSelect={onSelect}
>
{children}
@ -320,7 +324,7 @@ function IconButton({
return (
<_ContextMenu.Item
as={_IconButton}
bp={{ '@initial': 'mobile', '@sm': 'small' }}
bp={breakpoints}
disabled={disabled}
onSelect={onSelect}
>
@ -338,10 +342,7 @@ function SubMenu({
}) {
return (
<_ContextMenu.Root dir="ltr">
<_ContextMenu.TriggerItem
as={RowButton}
bp={{ '@initial': 'mobile', '@sm': 'small' }}
>
<_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
<span>{label}</span>
<IconWrapper size="small">
<ChevronRightIcon />
@ -363,10 +364,7 @@ function AlignDistributeSubMenu({
}) {
return (
<_ContextMenu.Root dir="ltr">
<_ContextMenu.TriggerItem
as={RowButton}
bp={{ '@initial': 'mobile', '@sm': 'small' }}
>
<_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
<span>Align / Distribute</span>
<IconWrapper size="small">
<ChevronRightIcon />
@ -447,10 +445,7 @@ function MoveToPageMenu() {
return (
<_ContextMenu.Root dir="ltr">
<_ContextMenu.TriggerItem
as={RowButton}
bp={{ '@initial': 'mobile', '@sm': 'small' }}
>
<_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
<span>Move To Page</span>
<IconWrapper size="small">
<ChevronRightIcon />

View file

@ -1,28 +1,19 @@
import React, { useEffect, useRef } from 'react'
import React from 'react'
import styled from 'styles'
export default function Cursor(): JSX.Element {
const rCursor = useRef<SVGSVGElement>(null)
useEffect(() => {
function updatePosition(e: PointerEvent) {
const cursor = rCursor.current
cursor.setAttribute(
'transform',
`translate(${e.clientX - 12} ${e.clientY - 10})`
)
}
document.body.addEventListener('pointermove', updatePosition)
return () => {
document.body.removeEventListener('pointermove', updatePosition)
}
}, [])
export default function Cursor({
color = 'dodgerblue',
point = [0, 0],
}: {
color?: string
point: number[]
}): JSX.Element {
const transform = `translate(${point[0] - 12} ${point[1] - 10})`
return (
<StyledCursor
ref={rCursor}
color={color}
transform={transform}
width="35px"
height="35px"
viewBox="0 0 35 35"
@ -33,23 +24,19 @@ export default function Cursor(): JSX.Element {
>
<path
d="M12,24.4219 L12,8.4069 L23.591,20.0259 L16.81,20.0259 L16.399,20.1499 L12,24.4219 Z"
id="point-border"
fill="#FFFFFF"
fill="#ffffff"
/>
<path
d="M21.0845,25.0962 L17.4795,26.6312 L12.7975,15.5422 L16.4835,13.9892 L21.0845,25.0962 Z"
id="stem-border"
fill="#FFFFFF"
fill="#ffffff"
/>
<path
d="M19.751,24.4155 L17.907,25.1895 L14.807,17.8155 L16.648,17.0405 L19.751,24.4155 Z"
id="stem"
fill="#000000"
fill="currentColor"
/>
<path
d="M13,10.814 L13,22.002 L15.969,19.136 L16.397,18.997 L21.165,18.997 L13,10.814 Z"
id="point"
fill="#000000"
fill="currentColor"
/>
</StyledCursor>
)

View file

@ -2,10 +2,11 @@ import { getShapeStyle } from 'state/shape-styles'
import { getShapeUtils } from 'state/shape-utils'
import React from 'react'
import { useSelector } from 'state'
import { getCurrentCamera } from 'utils'
import tld from 'utils/tld'
import { DotCircle, Handle } from './misc'
import useShapeDef from 'hooks/useShape'
import useShapesToRender from 'hooks/useShapesToRender'
import styled from 'styles'
export default function Defs(): JSX.Element {
const shapeIdsToRender = useShapesToRender()
@ -15,6 +16,7 @@ export default function Defs(): JSX.Element {
<DotCircle id="dot" r={4} />
<Handle id="handle" r={4} />
<ExpandDef />
<ShadowDef />
{shapeIdsToRender.map((id) => (
<Def key={id} id={id} />
))}
@ -40,10 +42,23 @@ function Def({ id }: { id: string }) {
}
function ExpandDef() {
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
const zoom = useSelector((s) => tld.getCurrentCamera(s.data).zoom)
return (
<filter id="expand">
<feMorphology operator="dilate" radius={2 / zoom} />
</filter>
)
}
function ShadowDef() {
return (
<filter id="hover">
<StyledShadow dx="0" dy="0" stdDeviation="1.2" floodOpacity="1" />
</filter>
)
}
const StyledShadow = styled('feDropShadow', {
floodColor: '$selected',
})

View file

@ -1,5 +1,5 @@
import { memo } from 'react'
import { getShape } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
import vec from 'utils/vec'
import styled from 'styles'
@ -8,10 +8,10 @@ import { getShapeStyle } from 'state/shape-styles'
function HoveredShape({ id }: { id: string }) {
const transform = useSelector((s) => {
const shape = getShape(s.data, id)
const shape = tld.getShape(s.data, id)
const center = getShapeUtils(shape).getCenter(shape)
const rotation = shape.rotation * (180 / Math.PI)
const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0]
const parentPoint = tld.getShape(s.data, shape.parentId)?.point || [0, 0]
return `
translate(${vec.neg(parentPoint)})
@ -21,7 +21,7 @@ function HoveredShape({ id }: { id: string }) {
})
const strokeWidth = useSelector((s) => {
const shape = getShape(s.data, id)
const shape = tld.getShape(s.data, id)
const style = getShapeStyle(shape.style)
return +style.strokeWidth
})
@ -37,7 +37,6 @@ function HoveredShape({ id }: { id: string }) {
const StyledHoverShape = styled('use', {
stroke: '$selected',
filter: 'url(#expand)',
opacity: 0.1,
})

View file

@ -1,6 +1,7 @@
import styled from 'styles'
import { useSelector } from 'state'
import { deepCompareArrays, getPage } from 'utils'
import tld from 'utils/tld'
import { deepCompareArrays } from 'utils'
import { getShapeUtils } from 'state/shape-utils'
import { memo } from 'react'
@ -26,7 +27,7 @@ export default function Selected(): JSX.Element {
export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
// const rIndicator = useRef<SVGUseElement>(null)
const shape = useSelector((s) => getPage(s.data).shapes[id])
const shape = useSelector((s) => tld.getShape(s.data, id))
// const events = useShapeEvents(id, shape?.type === ShapeType.Group, rIndicator)

View file

@ -1,8 +1,9 @@
import React, { useRef, memo, useEffect } from 'react'
import { useSelector } from 'state'
import React, { useRef, memo, useEffect, useState } from 'react'
import state, { useSelector } from 'state'
import styled from 'styles'
import { getShapeUtils } from 'state/shape-utils'
import { deepCompareArrays, getPage, getShape } from 'utils'
import { deepCompareArrays } from 'utils'
import tld from 'utils/tld'
import useShapeEvents from 'hooks/useShapeEvents'
import vec from 'utils/vec'
import { getShapeStyle } from 'state/shape-styles'
@ -16,36 +17,32 @@ interface ShapeProps {
function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
const rGroup = useRef<SVGGElement>(null)
const shapeUtils = useSelector((s) => {
const shape = getShape(s.data, id)
return getShapeUtils(shape)
})
const isHidden = useSelector((s) => {
const shape = getShape(s.data, id)
return shape.isHidden
const shape = tld.getShape(s.data, id)
return shape?.isHidden || false
})
const children = useSelector((s) => {
const shape = getShape(s.data, id)
return shape.children
const shape = tld.getShape(s.data, id)
return shape?.children || []
}, deepCompareArrays)
const isParent = shapeUtils.isParent
const isForeignObject = shapeUtils.isForeignObject
const strokeWidth = useSelector((s) => {
const shape = getShape(s.data, id)
const style = getShapeStyle(shape.style)
const shape = tld.getShape(s.data, id)
const style = getShapeStyle(shape?.style)
return +style.strokeWidth
})
const shapeUtils = useSelector((s) => {
const shape = tld.getShape(s.data, id)
return getShapeUtils(shape)
})
const transform = useSelector((s) => {
const shape = getShape(s.data, id)
const center = shapeUtils.getCenter(shape)
const shape = tld.getShape(s.data, id)
const center = getShapeUtils(shape).getCenter(shape)
const rotation = shape.rotation * (180 / Math.PI)
const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0]
const parentPoint = tld.getShape(s.data, shape.parentId)?.point || [0, 0]
return `
translate(${vec.neg(parentPoint)})
@ -54,7 +51,15 @@ function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
`
})
const events = useShapeEvents(id, isParent, rGroup)
const events = useShapeEvents(id, shapeUtils?.isParent, rGroup)
const hasShape = useMissingShapeTest(id)
if (!hasShape) return null
const isParent = shapeUtils.isParent
const isForeignObject = shapeUtils.isForeignObject
return (
<StyledGroup
@ -117,7 +122,7 @@ const ForeignObjectHover = memo(function ForeignObjectHover({
id: string
}) {
const size = useSelector((s) => {
const shape = getPage(s.data).shapes[id]
const shape = tld.getPage(s.data).shapes[id]
const bounds = getShapeUtils(shape).getBounds(shape)
return [bounds.width, bounds.height]
@ -192,3 +197,17 @@ const StyledGroup = styled('g', {
})
export default memo(Shape)
function useMissingShapeTest(id: string) {
const [isShape, setIsShape] = useState(true)
useEffect(() => {
return state.onUpdate((s) => {
if (isShape && !tld.getShape(s.data, id)) {
setIsShape(false)
}
})
}, [isShape, id])
return isShape
}

View file

@ -2,18 +2,14 @@
import styled from 'styles'
import React, { useRef } from 'react'
import state, { useSelector } from 'state'
import { X, Code } from 'react-feather'
import { X } from 'react-feather'
import { breakpoints, IconButton } from 'components/shared'
import * as Panel from '../panel'
import Control from './control'
import { deepCompareArrays } from 'utils'
function openCodePanel() {
state.send('CLOSED_CODE_PANEL')
}
function closeCodePanel() {
state.send('OPENED_CODE_PANEL')
function handleClose() {
state.send('CLOSED_CONTROLS')
}
const stopKeyboardPropagation = (e: KeyboardEvent | React.KeyboardEvent) =>
@ -22,11 +18,16 @@ const stopKeyboardPropagation = (e: KeyboardEvent | React.KeyboardEvent) =>
export default function ControlPanel(): JSX.Element {
const rContainer = useRef<HTMLDivElement>(null)
const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
const codeControls = useSelector(
(state) => Object.keys(state.data.codeControls),
deepCompareArrays
)
if (codeControls.length === 0) {
return null
}
return (
<Panel.Root
ref={rContainer}
@ -37,25 +38,19 @@ export default function ControlPanel(): JSX.Element {
onKeyDown={stopKeyboardPropagation}
onKeyUp={stopKeyboardPropagation}
>
{isOpen ? (
<Panel.Layout>
<Panel.Header>
<IconButton bp={breakpoints} size="small" onClick={closeCodePanel}>
<X />
</IconButton>
<h3>Controls</h3>
</Panel.Header>
<ControlsList>
{codeControls.map((id) => (
<Control key={id} id={id} />
))}
</ControlsList>
</Panel.Layout>
) : (
<IconButton bp={breakpoints} size="small" onClick={openCodePanel}>
<Code />
</IconButton>
)}
<Panel.Layout>
<Panel.Header>
<IconButton bp={breakpoints} size="small" onClick={handleClose}>
<X />
</IconButton>
<h3>Controls</h3>
</Panel.Header>
<ControlsList>
{codeControls.map((id) => (
<Control key={id} id={id} />
))}
</ControlsList>
</Panel.Layout>
</Panel.Root>
)
}

View file

@ -9,9 +9,9 @@ import PagePanel from './page-panel/page-panel'
import CodePanel from './code-panel/code-panel'
import ControlsPanel from './controls-panel/controls-panel'
export default function Editor(): JSX.Element {
export default function Editor({ roomId }: { roomId?: string }): JSX.Element {
useKeyboardEvents()
useLoadOnMount()
useLoadOnMount(roomId)
return (
<Layout>

View file

@ -2,6 +2,8 @@ import { useStateDesigner } from '@state-designer/react'
import state from 'state'
import styled from 'styles'
const size: any = { '@sm': 'small' }
export default function StatusBar(): JSX.Element {
const local = useStateDesigner(state)
@ -9,16 +11,13 @@ export default function StatusBar(): JSX.Element {
const states = s.split('.')
return states[states.length - 1]
})
const log = local.log[0]
return (
<StatusBarContainer
size={{
'@sm': 'small',
}}
>
<StatusBarContainer size={size}>
<Section>
{active.join(' | ')} | {log}
{active.join(' | ')} | {log} | {local.data.room?.status}
</Section>
</StatusBarContainer>
)

View file

@ -1,11 +1,11 @@
import tld from 'utils/tld'
import state, { useSelector } from 'state'
import { IconButton, breakpoints } from 'components/shared'
import { memo } from 'react'
import styled from 'styles'
import { MoveType } from 'types'
import { Trash2 } from 'react-feather'
import state, { useSelector } from 'state'
import Tooltip from 'components/tooltip'
import {
ArrowDownIcon,
ArrowUpIcon,
@ -20,7 +20,6 @@ import {
PinTopIcon,
RotateCounterClockwiseIcon,
} from '@radix-ui/react-icons'
import { getPage, getSelectedIds } from 'utils'
function handleRotateCcw() {
state.send('ROTATED_CCW')
@ -64,24 +63,24 @@ function handleDelete() {
function ShapesFunctions() {
const isAllLocked = useSelector((s) => {
const page = getPage(s.data)
const page = tld.getPage(s.data)
return s.values.selectedIds.every((id) => page.shapes[id].isLocked)
})
const isAllAspectLocked = useSelector((s) => {
const page = getPage(s.data)
const page = tld.getPage(s.data)
return s.values.selectedIds.every(
(id) => page.shapes[id].isAspectRatioLocked
)
})
const isAllHidden = useSelector((s) => {
const page = getPage(s.data)
const page = tld.getPage(s.data)
return s.values.selectedIds.every((id) => page.shapes[id].isHidden)
})
const hasSelection = useSelector((s) => {
return getSelectedIds(s.data).size > 0
return tld.getSelectedIds(s.data).size > 0
})
return (

View file

@ -2,8 +2,8 @@ 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'
import Tooltip from '../tooltip'
import tld from 'utils/tld'
const zoomIn = () => state.send('ZOOMED_IN')
const zoomOut = () => state.send('ZOOMED_OUT')
@ -31,7 +31,7 @@ export default function Zoom(): JSX.Element {
}
function ZoomCounter() {
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
const zoom = useSelector((s) => tld.getCurrentCamera(s.data).zoom)
return (
<ZoomButton

View file

@ -2,7 +2,7 @@
import React, { useEffect } from 'react'
import state from 'state'
import storage from 'state/storage'
import { getCurrentCamera } from 'utils'
import tld from 'utils/tld'
/**
* When the state's camera changes, update the transform of
@ -11,13 +11,13 @@ import { getCurrentCamera } from 'utils'
*/
export default function useCamera(ref: React.MutableRefObject<SVGGElement>) {
useEffect(() => {
let prev = getCurrentCamera(state.data)
let prev = tld.getCurrentCamera(state.data)
return state.onUpdate(() => {
const g = ref.current
if (!g) return
const { point, zoom } = getCurrentCamera(state.data)
const { point, zoom } = tld.getCurrentCamera(state.data)
if (point !== prev.point || zoom !== prev.zoom) {
g.setAttribute(
@ -27,7 +27,7 @@ export default function useCamera(ref: React.MutableRefObject<SVGGElement>) {
storage.savePageState(state.data)
prev = getCurrentCamera(state.data)
prev = tld.getCurrentCamera(state.data)
}
})
}, [state])

View file

@ -2,16 +2,18 @@
import { useEffect } from 'react'
import state from 'state'
export default function useLoadOnMount() {
export default function useLoadOnMount(roomId: string) {
useEffect(() => {
const fonts = (document as any).fonts
fonts
.load('12px Verveine Regular', 'Fonts are loaded!')
.then(() => state.send('MOUNTED'))
fonts.load('12px Verveine Regular', 'Fonts are loaded!').then(() => {
state.send('MOUNTED')
state.send('RT_LOADED_ROOM', { id: roomId })
})
return () => {
state.send('UNMOUNTED')
state.send('RT_UNLOADED_ROOM', { id: roomId })
}
}, [])
}, [roomId])
}

View file

@ -7,9 +7,8 @@ import {
boundsContain,
debounce,
deepCompareArrays,
getPageState,
getViewport,
} from 'utils'
import tld from 'utils/tld'
const viewportCache = new WeakMap<PageState, Bounds>()
@ -27,10 +26,10 @@ export default function usePageShapes(): string[] {
// Get the shapes that fit into the current window
const visiblePageShapeIds = useSelector((s) => {
const pageState = getPageState(s.data)
const pageState = tld.getPageState(s.data)
if (!viewportCache.has(pageState)) {
const viewport = getViewport(s.data)
const viewport = tld.getViewport(s.data)
viewportCache.set(pageState, viewport)
}

View file

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { useSelector } from 'state'
import { getShapeUtils } from 'state/shape-utils'
import { getShape } from 'utils'
import tld from 'utils/tld'
export default function useShapeDef(id: string) {
return useSelector(
(s) => getShape(s.data, id),
(s) => tld.getShape(s.data, id),
(prev, next) => {
const shouldSkip = !(
prev &&

View file

@ -1,11 +1,12 @@
import { useSelector } from 'state'
import { getShapeUtils } from 'state/shape-utils'
import { deepCompareArrays, getPage } from 'utils'
import { deepCompareArrays } from 'utils'
import tld from 'utils/tld'
export default function useShapesToRender(): string[] {
return useSelector(
(s) =>
Object.values(getPage(s.data).shapes)
Object.values(tld.getPage(s.data).shapes)
.filter((shape) => shape && !getShapeUtils(shape).isForeignObject)
.map((shape) => shape.id),
deepCompareArrays

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { useRef } from 'react'
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { useRef } from 'react'
import state from 'state'
import inputs from 'state/inputs'
import vec from 'utils/vec'

View file

@ -57,6 +57,8 @@
"next-auth": "^3.27.0",
"next-pwa": "^5.2.21",
"perfect-freehand": "^0.4.91",
"pusher": "^5.0.0",
"pusher-js": "^7.0.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",

33
pages/api/pusher-auth.ts Normal file
View file

@ -0,0 +1,33 @@
import { NextApiHandler } from 'next'
import Pusher from 'pusher'
import { v4 as uuid } from 'uuid'
const pusher = new Pusher({
key: '5dc87c88b8684bda655a',
appId: '1226484',
secret: process.env.PUSHER_SECRET,
cluster: 'eu',
})
const PusherAuth: NextApiHandler = (req, res) => {
try {
const { socket_id, channel_name } = req.body
const presenceData = {
user_id: uuid(),
user_info: { name: 'Anonymous' },
}
const auth = pusher.authenticate(
socket_id.toString(),
channel_name.toString(),
presenceData
)
return res.send(auth)
} catch (err) {
res.status(403).end()
}
}
export default PusherAuth

22
pages/room/[id].tsx Normal file
View file

@ -0,0 +1,22 @@
import dynamic from 'next/dynamic'
import { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/client'
const Editor = dynamic(() => import('components/editor'), { ssr: false })
export default function Room({ id }: { id: string }): JSX.Element {
return <Editor roomId={id} />
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
const { id } = context.query
return {
props: {
session,
id,
},
}
}

23
pages/room/index.tsx Normal file
View file

@ -0,0 +1,23 @@
import { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/client'
import { v4 as uuid } from 'uuid'
export default function CreateNewRoom(): JSX.Element {
return <div>You should not see this one</div>
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
if (!session?.user) {
context.res.setHeader('Location', `/sponsorware`)
context.res.statusCode = 307
}
context.res.setHeader('Location', `/room/${uuid()}`)
context.res.statusCode = 307
return {
props: {},
}
}

View file

@ -1,6 +1,7 @@
import { getShapeUtils } from './shape-utils'
import { Data, Shape } from 'types'
import { getCommonBounds, getSelectedShapes } from 'utils'
import { getCommonBounds } from 'utils'
import tld from 'utils/tld'
import state from './state'
class Clipboard {
@ -47,7 +48,7 @@ class Clipboard {
}
copySelectionToSvg(data: Data) {
const shapes = getSelectedShapes(data)
const shapes = tld.getSelectedShapes(data)
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')

View file

@ -27,7 +27,7 @@ import {
SizeStyle,
CodeError,
} from 'types'
import { getPage, getShapes } from 'utils'
import tld from 'utils/tld'
import { transform } from 'sucrase'
import { getErrorWithLineAndColumn, getFormattedCode } from 'utils/code'
@ -89,7 +89,8 @@ export async function generateFromCode(
)
const startingChildIndex =
getShapes(data)
tld
.getShapes(data)
.filter((shape) => shape.parentId === data.currentPageId)
.sort((a, b) => a.childIndex - b.childIndex)[0]?.childIndex || 1
@ -98,7 +99,7 @@ export async function generateFromCode(
.map((instance, i) => ({
...instance.shape,
isGenerated: true,
parentId: getPage(data).id,
parentId: tld.getPage(data).id,
childIndex: startingChildIndex + i,
}))
@ -141,7 +142,8 @@ export async function updateFromCode(
}
const startingChildIndex =
getShapes(data)
tld
.getShapes(data)
.filter((shape) => shape.parentId === data.currentPageId)
.sort((a, b) => a.childIndex - b.childIndex)[0]?.childIndex || 1
@ -156,7 +158,7 @@ export async function updateFromCode(
.map((instance, i) => ({
...instance.shape,
isGenerated: true,
parentId: getPage(data).id,
parentId: tld.getPage(data).id,
childIndex: startingChildIndex + i,
}))

View file

@ -1,11 +1,12 @@
import Command from './command'
import history from '../history'
import { AlignType, Data } from 'types'
import { getCommonBounds, getPage, getSelectedShapes } from 'utils'
import { getCommonBounds } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
export default function alignCommand(data: Data, type: AlignType): void {
const selectedShapes = getSelectedShapes(data)
const selectedShapes = tld.getSelectedShapes(data)
const entries = selectedShapes.map(
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
)
@ -20,7 +21,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
name: 'aligned',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
switch (type) {
case AlignType.Top: {
@ -86,7 +87,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
}
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in boundsForShapes) {
const shape = shapes[id]
const initialBounds = boundsForShapes[id]

View file

@ -1,7 +1,7 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { getPage, getSelectedIds } from 'utils'
import tld from 'utils/tld'
import { ArrowSnapshot } from 'state/sessions/arrow-session'
export default function arrowCommand(
@ -20,11 +20,11 @@ export default function arrowCommand(
const { initialShape } = after
const page = getPage(data)
const page = tld.getPage(data)
page.shapes[initialShape.id] = initialShape
const selectedIds = getSelectedIds(data)
const selectedIds = tld.getSelectedIds(data)
selectedIds.clear()
selectedIds.add(initialShape.id)
data.hoveredId = undefined
@ -32,11 +32,11 @@ export default function arrowCommand(
},
undo(data) {
const { initialShape } = before
const shapes = getPage(data).shapes
const shapes = tld.getPage(data).shapes
delete shapes[initialShape.id]
const selectedIds = getSelectedIds(data)
const selectedIds = tld.getSelectedIds(data)
selectedIds.clear()
data.hoveredId = undefined
data.pointedId = undefined

View file

@ -1,5 +1,6 @@
import { Data } from 'types'
import { getSelectedIds, setSelectedIds, setToArray } from 'utils'
import { setToArray } from 'utils'
import tld from 'utils/tld'
/* ------------------ Command Class ----------------- */
@ -84,12 +85,12 @@ export class BaseCommand<T extends any> {
export default class Command extends BaseCommand<Data> {
saveSelectionState = (data: Data): ((next: Data) => void) => {
const { currentPageId } = data
const selectedIds = setToArray(getSelectedIds(data))
const selectedIds = setToArray(tld.getSelectedIds(data))
return (next: Data) => {
next.currentPageId = currentPageId
next.hoveredId = undefined
next.pointedId = undefined
setSelectedIds(next, selectedIds)
tld.setSelectedIds(next, selectedIds)
}
}
}

View file

@ -2,7 +2,8 @@ import Command from './command'
import history from '../history'
import { Data } from 'types'
import storage from 'state/storage'
import { deepClone, getPage, getPageState } from 'utils'
import { deepClone } from 'utils'
import tld from 'utils/tld'
export default function deletePage(data: Data, pageId: string): void {
const snapshot = getSnapshot(data, pageId)
@ -31,9 +32,9 @@ export default function deletePage(data: Data, pageId: string): void {
function getSnapshot(data: Data, pageId: string) {
const { currentPageId, document } = data
const page = deepClone(getPage(data))
const page = deepClone(tld.getPage(data))
const pageState = deepClone(getPageState(data))
const pageState = deepClone(tld.getPageState(data))
const isCurrent = data.currentPageId === pageId

View file

@ -1,24 +1,19 @@
import Command from './command'
import history from '../history'
import { Data, Shape } from 'types'
import {
deepClone,
getDocumentBranch,
getPage,
getSelectedShapes,
setSelectedIds,
} from 'utils'
import { deepClone } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
export default function deleteSelected(data: Data): void {
const selectedShapes = getSelectedShapes(data)
const selectedShapes = tld.getSelectedShapes(data)
const selectedIdsArr = selectedShapes
.filter((shape) => !shape.isLocked)
.map((shape) => shape.id)
const shapeIdsToDelete = selectedIdsArr.flatMap((id) =>
getDocumentBranch(data, id)
tld.getDocumentBranch(data, id)
)
const remainingIds = selectedShapes
@ -35,16 +30,16 @@ export default function deleteSelected(data: Data): void {
manualSelection: true,
do(data) {
// Update selected ids
setSelectedIds(data, remainingIds)
tld.setSelectedIds(data, remainingIds)
// Recursively delete shapes (and maybe their parents too)
deletedShapes = deleteShapes(data, shapeIdsToDelete)
},
undo(data) {
const page = getPage(data)
const page = tld.getPage(data)
// Update selected ids
setSelectedIds(data, selectedIdsArr)
tld.setSelectedIds(data, selectedIdsArr)
// Restore deleted shapes
deletedShapes.forEach((shape) => (page.shapes[shape.id] = shape))
@ -76,7 +71,7 @@ function deleteShapes(
): Shape[] {
const parentsToDelete: string[] = []
const page = getPage(data)
const page = tld.getPage(data)
const parentIds = new Set(shapeIds.map((id) => page.shapes[id].parentId))

View file

@ -1,37 +0,0 @@
import Command from './command'
import history from '../history'
import { DirectionSnapshot } from 'state/sessions/direction-session'
import { Data, LineShape, RayShape } from 'types'
import { getPage } from 'utils'
export default function directCommand(
data: Data,
before: DirectionSnapshot,
after: DirectionSnapshot
): void {
history.execute(
data,
new Command({
name: 'set_direction',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
for (const { id, direction } of after.shapes) {
const shape = shapes[id] as RayShape | LineShape
shape.direction = direction
}
},
undo(data) {
const { shapes } = getPage(data)
for (const { id, direction } of after.shapes) {
const shape = shapes[id] as RayShape | LineShape
shape.direction = direction
}
},
})
)
}

View file

@ -1,21 +1,17 @@
import Command from './command'
import history from '../history'
import { Data, DistributeType } from 'types'
import {
getBoundsCenter,
getCommonBounds,
getPage,
getSelectedShapes,
} from 'utils'
import { getBoundsCenter, getCommonBounds } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
export default function distributeCommand(
data: Data,
type: DistributeType
): void {
const selectedShapes = getSelectedShapes(data).filter(
(shape) => !shape.isLocked
)
const selectedShapes = tld
.getSelectedShapes(data)
.filter((shape) => !shape.isLocked)
const entries = selectedShapes.map(
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
@ -38,7 +34,7 @@ export default function distributeCommand(
name: 'distribute_shapes',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
const len = entries.length
switch (type) {
@ -130,7 +126,7 @@ export default function distributeCommand(
}
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in boundsForShapes) {
const shape = shapes[id]
const initialBounds = boundsForShapes[id]

View file

@ -2,14 +2,15 @@ import Command from './command'
import history from '../history'
import { Data, PointerInfo } from 'types'
import { getShapeUtils } from 'state/shape-utils'
import { deepClone, getPage, getShape, updateParents } from 'utils'
import { deepClone } from 'utils'
import tld from 'utils/tld'
export default function doublePointHandleCommand(
data: Data,
id: string,
payload: PointerInfo
): void {
const initialShape = deepClone(getShape(data, id))
const initialShape = deepClone(tld.getShape(data, id))
history.execute(
data,
@ -17,16 +18,16 @@ export default function doublePointHandleCommand(
name: 'double_point_handle',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
const shape = shapes[id]
getShapeUtils(shape).onDoublePointHandle(shape, payload.target, payload)
updateParents(data, [id])
tld.updateParents(data, [id])
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
shapes[id] = initialShape
updateParents(data, [id])
tld.updateParents(data, [id])
},
})
)

View file

@ -1,10 +1,11 @@
import Command from './command'
import history from '../history'
import { Data, DrawShape } from 'types'
import { deepClone, getPage, getShape, setSelectedIds } from 'utils'
import tld from 'utils/tld'
import { deepClone } from 'utils'
export default function drawCommand(data: Data, id: string): void {
const restoreShape = deepClone(getShape(data, id)) as DrawShape
const restoreShape = deepClone(tld.getShape(data, id)) as DrawShape
history.execute(
data,
@ -14,14 +15,14 @@ export default function drawCommand(data: Data, id: string): void {
manualSelection: true,
do(data, initial) {
if (!initial) {
getPage(data).shapes[id] = restoreShape
tld.getPage(data).shapes[id] = restoreShape
}
setSelectedIds(data, [])
tld.setSelectedIds(data, [])
},
undo(data) {
setSelectedIds(data, [])
delete getPage(data).shapes[id]
tld.setSelectedIds(data, [])
delete tld.getPage(data).shapes[id]
},
})
)

View file

@ -1,23 +1,21 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import {
deepClone,
getCurrentCamera,
getPage,
getSelectedShapes,
setSelectedIds,
} from 'utils'
import { deepClone } from 'utils'
import tld from 'utils/tld'
import { uniqueId } from 'utils'
import vec from 'utils/vec'
export default function duplicateCommand(data: Data): void {
const selectedShapes = getSelectedShapes(data).map(deepClone)
const selectedShapes = tld.getSelectedShapes(data).map(deepClone)
const duplicates = selectedShapes.map((shape) => ({
...shape,
id: uniqueId(),
point: vec.add(shape.point, vec.div([16, 16], getCurrentCamera(data).zoom)),
point: vec.add(
shape.point,
vec.div([16, 16], tld.getCurrentCamera(data).zoom)
),
isGenerated: false,
}))
@ -28,25 +26,25 @@ export default function duplicateCommand(data: Data): void {
category: 'canvas',
manualSelection: true,
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const duplicate of duplicates) {
shapes[duplicate.id] = duplicate
}
setSelectedIds(
tld.setSelectedIds(
data,
duplicates.map((d) => d.id)
)
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const duplicate of duplicates) {
delete shapes[duplicate.id]
}
setSelectedIds(
tld.setSelectedIds(
data,
selectedShapes.map((d) => d.id)
)

View file

@ -1,7 +1,7 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { getPage } from 'utils'
import tld from 'utils/tld'
import { EditSnapshot } from 'state/sessions/edit-session'
import { getShapeUtils } from 'state/shape-utils'
@ -18,7 +18,7 @@ export default function editCommand(
do(data) {
const { initialShape } = after
const page = getPage(data)
const page = tld.getPage(data)
page.shapes[initialShape.id] = initialShape
@ -31,7 +31,7 @@ export default function editCommand(
undo(data) {
const { initialShape } = before
const page = getPage(data)
const page = tld.getPage(data)
page.shapes[initialShape.id] = initialShape
},

View file

@ -1,13 +1,15 @@
import Command from './command'
import history from '../history'
import { Data, Shape } from 'types'
import { deepClone, getPage, getShapes, setSelectedIds } from 'utils'
import { deepClone } from 'utils'
import tld from 'utils/tld'
export default function generateCommand(
data: Data,
generatedShapes: Shape[]
): void {
const initialShapes = getShapes(data)
const initialShapes = tld
.getShapes(data)
.filter((shape) => shape.isGenerated)
.map(deepClone)
@ -17,16 +19,16 @@ export default function generateCommand(
name: 'generate_shapes',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
initialShapes.forEach((shape) => delete shapes[shape.id])
generatedShapes.forEach((shape) => (shapes[shape.id] = shape))
setSelectedIds(data, [])
tld.setSelectedIds(data, [])
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
generatedShapes.forEach((shape) => delete shapes[shape.id])
initialShapes.forEach((shape) => (shapes[shape.id] = shape))
setSelectedIds(data, [])
tld.setSelectedIds(data, [])
},
})
)

View file

@ -1,32 +1,34 @@
import Command from './command'
import history from '../history'
import { Data, GroupShape, ShapeType } from 'types'
import {
getCommonBounds,
getPage,
getSelectedIds,
getSelectedShapes,
getShape,
setSelectedIds,
} from 'utils'
import { current } from 'immer'
import { deepClone, getCommonBounds } from 'utils'
import tld from 'utils/tld'
import { createShape, getShapeUtils } from 'state/shape-utils'
import commands from '.'
export default function groupCommand(data: Data): void {
const cData = current(data)
const { currentPageId } = cData
const { currentPageId } = data
const oldSelectedIds = getSelectedIds(cData)
const oldSelectedIds = tld.getSelectedIds(data)
const initialShapes = getSelectedShapes(cData).sort(
(a, b) => a.childIndex - b.childIndex
)
const initialShapes = tld
.getSelectedShapes(data)
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => deepClone(shape))
const isAllSameParent = initialShapes.every(
(shape, i) => i === 0 || shape.parentId === initialShapes[i - 1].parentId
)
// Do we need to ungroup the selected shapes shapes, rather than group them?
if (isAllSameParent && initialShapes[0]?.parentId !== currentPageId) {
const parent = tld.getShape(data, initialShapes[0]?.parentId) as GroupShape
if (parent.children.length === initialShapes.length) {
commands.ungroup(data)
return
}
}
let newGroupParentId: string
const initialShapeIds = initialShapes.map((s) => s.id)
@ -40,19 +42,11 @@ export default function groupCommand(data: Data): void {
if (isAllSameParent) {
const parentId = initialShapes[0].parentId
if (parentId === currentPageId) {
// Create the new group on the current page
newGroupParentId = currentPageId
} else {
// Are all of the parent's children selected?
const parent = getShape(data, parentId) as GroupShape
if (parent.children.length === initialShapes.length) {
// !!! Hey! We're not going any further. We need to ungroup those shapes.
commands.ungroup(data)
return
} else {
// Make the group inside of the current group
newGroupParentId = parentId
}
// Create the new group as a child of the shapes' current parent group
newGroupParentId = parentId
}
} else {
// Find the least-deep parent among the shapes and add the group as a child
@ -82,7 +76,7 @@ export default function groupCommand(data: Data): void {
category: 'canvas',
manualSelection: true,
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
// Create the new group
shapes[newGroupShape.id] = newGroupShape
@ -115,10 +109,10 @@ export default function groupCommand(data: Data): void {
.setProperty(shape, 'parentId', newGroupShape.id)
})
setSelectedIds(data, [newGroupShape.id])
tld.setSelectedIds(data, [newGroupShape.id])
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
const group = shapes[newGroupShape.id]
@ -152,7 +146,7 @@ export default function groupCommand(data: Data): void {
delete shapes[newGroupShape.id]
// Reselect the children of the group
setSelectedIds(data, initialShapeIds)
tld.setSelectedIds(data, initialShapeIds)
},
})
)
@ -163,5 +157,5 @@ function getShapeDepth(data: Data, id: string, depth = 0) {
return depth
}
return getShapeDepth(data, getShape(data, id).parentId, depth + 1)
return getShapeDepth(data, tld.getShape(data, id).parentId, depth + 1)
}

View file

@ -1,7 +1,7 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { getPage } from 'utils'
import tld from 'utils/tld'
import { HandleSnapshot } from 'state/sessions/handle-session'
import { getShapeUtils } from 'state/shape-utils'
@ -18,7 +18,7 @@ export default function handleCommand(
do(data) {
const { initialShape } = after
const page = getPage(data)
const page = tld.getPage(data)
const shape = page.shapes[initialShape.id]
getShapeUtils(shape)
@ -28,7 +28,7 @@ export default function handleCommand(
undo(data) {
const { initialShape } = before
const page = getPage(data)
const page = tld.getPage(data)
page.shapes[initialShape.id] = initialShape
},
})

View file

@ -4,7 +4,6 @@ import changePage from './change-page'
import createPage from './create-page'
import deletePage from './delete-page'
import deleteSelected from './delete-selected'
import direct from './direct'
import distribute from './distribute'
import doublePointHandle from './double-point-handle'
import draw from './draw'
@ -36,7 +35,6 @@ const commands = {
createPage,
deletePage,
deleteSelected,
direct,
distribute,
doublePointHandle,
draw,

View file

@ -1,24 +1,18 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import {
getDocumentBranch,
getPage,
getPageState,
getSelectedIds,
setToArray,
uniqueArray,
} from 'utils'
import { setToArray, uniqueArray } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
import storage from 'state/storage'
export default function moveToPageCommand(data: Data, newPageId: string): void {
const { currentPageId: oldPageId } = data
const oldPage = getPage(data)
const selectedIds = setToArray(getSelectedIds(data))
const oldPage = tld.getPage(data)
const selectedIds = setToArray(tld.getSelectedIds(data))
const idsToMove = uniqueArray(
...selectedIds.flatMap((id) => getDocumentBranch(data, id))
...selectedIds.flatMap((id) => tld.getDocumentBranch(data, id))
)
const oldParentIds = Object.fromEntries(
@ -39,7 +33,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
const fromPageId = oldPageId
const toPageId = newPageId
const fromPage = getPage(data)
const fromPage = tld.getPage(data)
// Get all of the selected shapes and their descendents
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
@ -65,7 +59,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
})
// Clear the current page state's selected ids
getPageState(data).selectedIds.clear()
tld.getPageState(data).selectedIds.clear()
// Save the "from" page
storage.savePage(data, data.document.id, fromPageId)
@ -74,7 +68,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
storage.loadPage(data, toPageId)
// The page we're moving the shapes to
const toPage = getPage(data)
const toPage = tld.getPage(data)
// Add all of the selected shapes to the "from" page.
shapesToMove.forEach((shape) => {
@ -89,7 +83,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
})
// Select the selected ids on the new page
getPageState(data).selectedIds = new Set(selectedIds)
tld.getPageState(data).selectedIds = new Set(selectedIds)
// Move to the new page
data.currentPageId = toPageId
@ -98,7 +92,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
const fromPageId = newPageId
const toPageId = oldPageId
const fromPage = getPage(data)
const fromPage = tld.getPage(data)
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
@ -119,13 +113,13 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
delete fromPage.shapes[shape.id]
})
getPageState(data).selectedIds.clear()
tld.getPageState(data).selectedIds.clear()
storage.savePage(data, data.document.id, fromPageId)
storage.loadPage(data, toPageId)
const toPage = getPage(data)
const toPage = tld.getPage(data)
shapesToMove.forEach((shape) => {
toPage.shapes[shape.id] = shape
@ -144,7 +138,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
}
})
getPageState(data).selectedIds = new Set(selectedIds)
tld.getPageState(data).selectedIds = new Set(selectedIds)
data.currentPageId = toPageId
},

View file

@ -1,19 +1,14 @@
import Command from './command'
import history from '../history'
import { Data, MoveType, Shape } from 'types'
import {
forceIntegerChildIndices,
getChildren,
getPage,
getSelectedIds,
setToArray,
} from 'utils'
import { setToArray } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
export default function moveCommand(data: Data, type: MoveType): void {
const page = getPage(data)
const page = tld.getPage(data)
const selectedIds = setToArray(getSelectedIds(data))
const selectedIds = setToArray(tld.getSelectedIds(data))
const initialIndices = Object.fromEntries(
selectedIds.map((id) => [id, page.shapes[id].childIndex])
@ -26,7 +21,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
category: 'canvas',
manualSelection: true,
do(data) {
const page = getPage(data)
const page = tld.getPage(data)
const shapes = selectedIds.map((id) => page.shapes[id])
@ -44,20 +39,20 @@ export default function moveCommand(data: Data, type: MoveType): void {
switch (type) {
case MoveType.ToFront: {
for (const id in shapesByParentId) {
moveToFront(shapesByParentId[id], getChildren(data, id))
moveToFront(shapesByParentId[id], tld.getChildren(data, id))
}
break
}
case MoveType.ToBack: {
for (const id in shapesByParentId) {
moveToBack(shapesByParentId[id], getChildren(data, id))
moveToBack(shapesByParentId[id], tld.getChildren(data, id))
}
break
}
case MoveType.Forward: {
for (const id in shapesByParentId) {
const visited = new Set<string>()
const siblings = getChildren(data, id)
const siblings = tld.getChildren(data, id)
shapesByParentId[id]
.sort((a, b) => b.childIndex - a.childIndex)
.forEach((shape) => moveForward(shape, siblings, visited))
@ -67,7 +62,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
case MoveType.Backward: {
for (const id in shapesByParentId) {
const visited = new Set<string>()
const siblings = getChildren(data, id)
const siblings = tld.getChildren(data, id)
shapesByParentId[id]
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((shape) => moveBackward(shape, siblings, visited))
@ -77,7 +72,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
}
},
undo(data) {
const page = getPage(data)
const page = tld.getPage(data)
for (const id of selectedIds) {
const shape = page.shapes[id]
@ -143,7 +138,7 @@ function moveForward(shape: Shape, siblings: Shape[], visited: Set<string>) {
: Math.ceil(nextSibling.childIndex + 1)
if (nextIndex === nextSibling.childIndex) {
forceIntegerChildIndices(siblings)
tld.forceIntegerChildIndices(siblings)
nextIndex = nextNextSibling
? (nextSibling.childIndex + nextNextSibling.childIndex) / 2
@ -169,7 +164,7 @@ function moveBackward(shape: Shape, siblings: Shape[], visited: Set<string>) {
: nextSibling.childIndex / 2
if (shape.childIndex === nextSibling.childIndex) {
forceIntegerChildIndices(siblings)
tld.forceIntegerChildIndices(siblings)
nextNextSibling
? (nextSibling.childIndex + nextNextSibling.childIndex) / 2

View file

@ -2,7 +2,7 @@ import Command from './command'
import history from '../history'
import { Data, Shape } from 'types'
import { getShapeUtils } from 'state/shape-utils'
import { getPage, updateParents } from 'utils'
import tld from 'utils/tld'
// Used when changing the properties of one or more shapes,
// without changing selection or deleting any shapes.
@ -19,27 +19,27 @@ export default function mutateShapesCommand(
name,
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
after.forEach((shape) => {
shapes[shape.id] = shape
getShapeUtils(shape).onSessionComplete(shape)
})
// updateParents(
// data,
// after.map((shape) => shape.id)
// )
tld.updateParents(
data,
after.map((shape) => shape.id)
)
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
before.forEach((shape) => {
shapes[shape.id] = shape
getShapeUtils(shape).onSessionComplete(shape)
})
updateParents(
tld.updateParents(
data,
before.map((shape) => shape.id)
)

View file

@ -1,17 +1,11 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { getPage, getSelectedShapes } from 'utils'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
import vec from 'utils/vec'
export default function nudgeCommand(data: Data, delta: number[]): void {
const selectedShapes = getSelectedShapes(data)
const shapeBounds = Object.fromEntries(
selectedShapes.map(
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
)
)
const initialShapes = tld.getSelectedShapeSnapshot(data, () => null)
history.execute(
data,
@ -19,28 +13,22 @@ export default function nudgeCommand(data: Data, delta: number[]): void {
name: 'nudge_shapes',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
for (const id in shapeBounds) {
const shape = shapes[id]
getShapeUtils(shape).setProperty(
shape,
'point',
vec.add(shape.point, delta)
)
}
tld.mutateShapes(
data,
initialShapes.map((shape) => shape.id),
(shape, utils) => {
utils.setProperty(shape, 'point', vec.add(shape.point, delta))
}
)
},
undo(data) {
const { shapes } = getPage(data)
for (const id in shapeBounds) {
const shape = shapes[id]
getShapeUtils(shape).setProperty(
shape,
'point',
vec.sub(shape.point, delta)
)
}
tld.mutateShapes(
data,
initialShapes.map((shape) => shape.id),
(shape, utils) => {
utils.setProperty(shape, 'point', vec.sub(shape.point, delta))
}
)
},
})
)

View file

@ -1,21 +1,15 @@
import Command from './command'
import history from '../history'
import { Data, Shape } from 'types'
import {
getCommonBounds,
getPage,
getSelectedIds,
screenToWorld,
setSelectedIds,
setToArray,
} from 'utils'
import { getCommonBounds, setToArray } from 'utils'
import tld from 'utils/tld'
import { uniqueId } from 'utils'
import vec from 'utils/vec'
import { getShapeUtils } from 'state/shape-utils'
import state from 'state/state'
export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
const center = screenToWorld(
const center = tld.screenToWorld(
[window.innerWidth / 2, window.innerHeight / 2],
data
)
@ -32,7 +26,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
initialShapes.map((shape) => [shape.id, uniqueId()])
)
const oldSelectedIds = setToArray(getSelectedIds(data))
const oldSelectedIds = setToArray(tld.getSelectedIds(data))
history.execute(
data,
@ -41,7 +35,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
category: 'canvas',
manualSelection: true,
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
let childIndex =
(state.values.currentShapes[state.values.currentShapes.length - 1]
@ -62,14 +56,14 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
}
}
setSelectedIds(data, Object.values(newIdMap))
tld.setSelectedIds(data, Object.values(newIdMap))
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
Object.values(newIdMap).forEach((id) => delete shapes[id])
setSelectedIds(data, oldSelectedIds)
tld.setSelectedIds(data, oldSelectedIds)
},
})
)

View file

@ -1,14 +1,10 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { getPage, getSelectedShapes, updateParents } from 'utils'
import { current } from 'immer'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
export default function resetBoundsCommand(data: Data): void {
const initialShapes = Object.fromEntries(
getSelectedShapes(current(data)).map((shape) => [shape.id, shape])
)
const initialShapes = tld.getSelectedShapeSnapshot(data)
history.execute(
data,
@ -16,21 +12,18 @@ export default function resetBoundsCommand(data: Data): void {
name: 'reset_bounds',
category: 'canvas',
do(data) {
getSelectedShapes(data).forEach((shape) => {
if (shape.isLocked) return
getShapeUtils(shape).onBoundsReset(shape)
})
updateParents(data, Object.keys(initialShapes))
tld.mutateShapes(
data,
initialShapes.map((shape) => shape.id),
(shape, utils) => void utils.onBoundsReset(shape)
)
},
undo(data) {
const page = getPage(data)
getSelectedShapes(data).forEach((shape) => {
if (shape.isLocked) return
page.shapes[shape.id] = initialShapes[shape.id]
})
updateParents(data, Object.keys(initialShapes))
tld.mutateShapes(
data,
initialShapes.map((shape) => shape.id),
(_, __, i) => initialShapes[i]
)
},
})
)

View file

@ -1,12 +1,8 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import {
getBoundsCenter,
getCommonBounds,
getPage,
getSelectedShapes,
} from 'utils'
import { getBoundsCenter, getCommonBounds } from 'utils'
import tld from 'utils/tld'
import vec from 'utils/vec'
import { getShapeUtils } from 'state/shape-utils'
@ -15,10 +11,10 @@ const PI2 = Math.PI * 2
export default function rotateCcwCommand(data: Data): void {
const { boundsRotation } = data
const page = getPage(data)
const page = tld.getPage(data)
const initialShapes = Object.fromEntries(
getSelectedShapes(data).map((shape) => {
tld.getSelectedShapes(data).map((shape) => {
const bounds = getShapeUtils(shape).getBounds(shape)
return [
shape.id,
@ -63,7 +59,7 @@ export default function rotateCcwCommand(data: Data): void {
name: 'rotate_ccw',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in nextShapes) {
const shape = shapes[id]
@ -77,7 +73,7 @@ export default function rotateCcwCommand(data: Data): void {
data.boundsRotation = nextboundsRotation
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in initialShapes) {
const { point, rotation } = initialShapes[id]

View file

@ -2,8 +2,8 @@ import Command from './command'
import history from '../history'
import { Data } from 'types'
import { RotateSnapshot } from 'state/sessions/rotate-session'
import { getPage } from 'utils'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
export default function rotateCommand(
data: Data,
@ -16,7 +16,7 @@ export default function rotateCommand(
name: 'rotate_shapes',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const { id, point, rotation } of after.initialShapes) {
const shape = shapes[id]
@ -29,7 +29,7 @@ export default function rotateCommand(
data.boundsRotation = after.boundsRotation
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const { id, point, rotation } of before.initialShapes) {
const shape = shapes[id]

View file

@ -1,11 +1,14 @@
import Command from './command'
import history from '../history'
import { StretchType, Data, Corner } from 'types'
import { deepClone, getCommonBounds, getPage, getSelectedShapes } from 'utils'
import { deepClone, getCommonBounds } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
export default function stretchCommand(data: Data, type: StretchType): void {
const initialShapes = getSelectedShapes(data).map((shape) => deepClone(shape))
const initialShapes = tld
.getSelectedShapes(data)
.map((shape) => deepClone(shape))
const snapshot = Object.fromEntries(
initialShapes.map((shape) => [
@ -27,7 +30,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
name: 'stretched_shapes',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
switch (type) {
case StretchType.Horizontal: {
@ -75,7 +78,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
}
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
initialShapes.forEach((shape) => (shapes[shape.id] = shape))
},
})

View file

@ -1,22 +1,21 @@
import Command from './command'
import history from '../history'
import { Data, ShapeStyles } from 'types'
import { getDocumentBranch, getPage, getSelectedIds, setToArray } from 'utils'
import tld from 'utils/tld'
import { deepClone, setToArray } from 'utils'
import { getShapeUtils } from 'state/shape-utils'
import { current } from 'immer'
export default function styleCommand(
data: Data,
styles: Partial<ShapeStyles>
): void {
const cData = current(data)
const page = getPage(cData)
const page = tld.getPage(data)
const selectedIds = setToArray(getSelectedIds(data))
const selectedIds = setToArray(tld.getSelectedIds(data))
const shapesToStyle = selectedIds
.flatMap((id) => getDocumentBranch(data, id))
.map((id) => page.shapes[id])
.flatMap((id) => tld.getDocumentBranch(data, id))
.map((id) => deepClone(page.shapes[id]))
history.execute(
data,
@ -25,7 +24,7 @@ export default function styleCommand(
category: 'canvas',
manualSelection: true,
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const { id } of shapesToStyle) {
const shape = shapes[id]
@ -33,7 +32,7 @@ export default function styleCommand(
}
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const { id, style } of shapesToStyle) {
const shape = shapes[id]

View file

@ -1,7 +1,7 @@
import Command from './command'
import history from '../history'
import { Data, Shape } from 'types'
import { getPage, getSelectedShapes } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
import { PropsOfType } from 'types'
@ -9,7 +9,7 @@ export default function toggleCommand(
data: Data,
prop: PropsOfType<Shape>
): void {
const selectedShapes = getSelectedShapes(data)
const selectedShapes = tld.getSelectedShapes(data)
const isAllToggled = selectedShapes.every((shape) => shape[prop])
const initialShapes = Object.fromEntries(
selectedShapes.map((shape) => [shape.id, shape[prop]])
@ -21,7 +21,7 @@ export default function toggleCommand(
name: 'toggle_prop',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in initialShapes) {
const shape = shapes[id]
@ -33,7 +33,7 @@ export default function toggleCommand(
}
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in initialShapes) {
const shape = shapes[id]

View file

@ -1,9 +1,9 @@
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { current } from 'immer'
import { TransformSingleSnapshot } from 'state/sessions/transform-single-session'
import { getPage, setSelectedIds, updateParents } from 'utils'
import tld from 'utils/tld'
import { deepClone } from 'utils'
export default function transformSingleCommand(
data: Data,
@ -11,7 +11,7 @@ export default function transformSingleCommand(
after: TransformSingleSnapshot,
isCreating: boolean
): void {
const shape = current(getPage(data).shapes[after.id])
const shape = deepClone(tld.getPage(data).shapes[after.id])
history.execute(
data,
@ -22,27 +22,27 @@ export default function transformSingleCommand(
do(data) {
const { id } = after
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
setSelectedIds(data, [id])
tld.setSelectedIds(data, [id])
shapes[id] = shape
updateParents(data, [id])
tld.updateParents(data, [id])
},
undo(data) {
const { id, initialShape } = before
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
if (isCreating) {
setSelectedIds(data, [])
tld.setSelectedIds(data, [])
delete shapes[id]
} else {
const page = getPage(data)
const page = tld.getPage(data)
page.shapes[id] = initialShape
updateParents(data, [id])
setSelectedIds(data, [id])
tld.updateParents(data, [id])
tld.setSelectedIds(data, [id])
}
},
})

View file

@ -2,7 +2,7 @@ import Command from './command'
import history from '../history'
import { Data } from 'types'
import { TransformSnapshot } from 'state/sessions/transform-session'
import { getPage, updateParents } from 'utils'
import tld from 'utils/tld'
export default function transformCommand(
data: Data,
@ -17,23 +17,23 @@ export default function transformCommand(
do(data) {
const { shapeBounds } = after
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in shapeBounds) {
shapes[id] = shapeBounds[id].initialShape
}
updateParents(data, Object.keys(shapeBounds))
tld.updateParents(data, Object.keys(shapeBounds))
},
undo(data) {
const { shapeBounds } = before
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in shapeBounds) {
shapes[id] = shapeBounds[id].initialShape
}
updateParents(data, Object.keys(shapeBounds))
tld.updateParents(data, Object.keys(shapeBounds))
},
})
)

View file

@ -2,12 +2,7 @@ import Command from './command'
import history from '../history'
import { TranslateSnapshot } from 'state/sessions/translate-session'
import { Data } from 'types'
import {
getDocumentBranch,
getPage,
setSelectedIds,
updateParents,
} from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
export default function translateCommand(
@ -26,7 +21,7 @@ export default function translateCommand(
if (initial) return
const { initialShapes } = after
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
// Restore clones to document
if (isCloning) {
@ -47,31 +42,31 @@ export default function translateCommand(
// Move shapes (these initialShapes will include clones if any)
for (const { id, point } of initialShapes) {
getDocumentBranch(data, id).forEach((id) => {
tld.getDocumentBranch(data, id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateTo(shape, point)
})
}
// Set selected shapes
setSelectedIds(
tld.setSelectedIds(
data,
initialShapes.map((s) => s.id)
)
// Update parents
updateParents(
tld.updateParents(
data,
initialShapes.map((s) => s.id)
)
},
undo(data) {
const { initialShapes, clones, initialParents } = before
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
// Move shapes back to where they started
for (const { id, point } of initialShapes) {
getDocumentBranch(data, id).forEach((id) => {
tld.getDocumentBranch(data, id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateTo(shape, point)
})
@ -81,7 +76,7 @@ export default function translateCommand(
if (isCloning) for (const { id } of clones) delete shapes[id]
// Set selected shapes
setSelectedIds(
tld.setSelectedIds(
data,
initialShapes.map((s) => s.id)
)
@ -93,7 +88,7 @@ export default function translateCommand(
})
// Update parents
updateParents(
tld.updateParents(
data,
initialShapes.map((s) => s.id)
)

View file

@ -1,17 +1,18 @@
import Command from './command'
import history from '../history'
import { Data, ShapeType } from 'types'
import { getPage, getSelectedShapes, setSelectedIds } from 'utils'
import { current } from 'immer'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
import { deepClone } from 'utils'
export default function ungroupCommand(data: Data): void {
const cData = current(data)
const { currentPageId } = cData
const { currentPageId } = data
const selectedGroups = getSelectedShapes(cData)
const selectedGroups = tld
.getSelectedShapes(data)
.filter((shape) => shape.type === ShapeType.Group)
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => deepClone(shape))
// Are all of the shapes already in the same group?
// - ungroup the shapes
@ -24,7 +25,7 @@ export default function ungroupCommand(data: Data): void {
name: 'ungroup_shapes',
category: 'canvas',
do(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
// Remove shapes from old parents
for (const oldGroupShape of selectedGroups) {
@ -62,13 +63,13 @@ export default function ungroupCommand(data: Data): void {
)
})
setSelectedIds(data, oldGroupShape.children)
tld.setSelectedIds(data, oldGroupShape.children)
delete shapes[oldGroupShape.id]
}
},
undo(data) {
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
selectedGroups.forEach((group) => {
shapes[group.id] = group
@ -80,7 +81,7 @@ export default function ungroupCommand(data: Data): void {
})
})
setSelectedIds(
tld.setSelectedIds(
data,
selectedGroups.map((g) => g.id)
)
@ -94,5 +95,5 @@ export default function ungroupCommand(data: Data): void {
// return depth
// }
// return getShapeDepth(data, getShape(data, id).parentId, depth + 1)
// return getShapeDepth(data, tld.getShape(data, id).parentId, depth + 1)
// }

123
state/coop/client-pusher.ts Normal file
View file

@ -0,0 +1,123 @@
import Pusher from 'pusher-js'
import * as PusherTypes from 'pusher-js'
import state from 'state/state'
import { Shape } from 'types'
class RoomClient {
room: string
pusher: Pusher
channel: PusherTypes.PresenceChannel
lastCursorEventTime = 0
id: string
constructor() {
// Create pusher instance and bind events
this.pusher = new Pusher('5dc87c88b8684bda655a', {
cluster: 'eu',
authEndpoint: 'http://localhost:3000/api/pusher-auth',
})
this.pusher.connection.bind('connecting', () =>
state.send('RT_CHANGED_STATUS', { status: 'connecting' })
)
this.pusher.connection.bind('connected', () =>
state.send('RT_CHANGED_STATUS', { status: 'connected' })
)
this.pusher.connection.bind('unavailable', () =>
state.send('RT_CHANGED_STATUS', { status: 'unavailable' })
)
this.pusher.connection.bind('failed', () => {
state.send('RT_CHANGED_STATUS', { status: 'failed' })
})
this.pusher.connection.bind('disconnected', () => {
state.send('RT_CHANGED_STATUS', { status: 'disconnected' })
})
}
connect(roomId: string) {
this.room = 'presence-' + roomId
// Subscribe to channel
this.channel = this.pusher.subscribe(
this.room
) as PusherTypes.PresenceChannel
this.channel.bind('pusher:subscription_error', (err: string) => {
console.warn(err)
state.send('RT_CHANGED_STATUS', { status: 'subscription-error' })
})
this.channel.bind('pusher:subscription_succeeded', () => {
const me = this.channel.members.me
const userId = me.id
this.id = userId
state.send('RT_CHANGED_STATUS', { status: 'subscribed' })
})
this.channel.bind(
'created_shape',
(payload: { id: string; pageId: string; shape: Shape }) => {
if (payload.id === this.id) return
state.send('RT_CREATED_SHAPE', payload)
}
)
this.channel.bind(
'deleted_shape',
(payload: { id: string; pageId: string; shape: Shape }) => {
if (payload.id === this.id) return
state.send('RT_DELETED_SHAPE', payload)
}
)
this.channel.bind(
'edited_shape',
(payload: { id: string; pageId: string; change: Partial<Shape> }) => {
if (payload.id === this.id) return
state.send('RT_EDITED_SHAPE', payload)
}
)
this.channel.bind(
'client-moved-cursor',
(payload: { id: string; pageId: string; point: number[] }) => {
if (payload.id === this.id) return
state.send('RT_MOVED_CURSOR', payload)
}
)
}
disconnect() {
this.pusher.unsubscribe(this.room)
}
reconnect() {
this.pusher.subscribe(this.room)
}
moveCursor(pageId: string, point: number[]) {
if (!this.channel) return
const now = Date.now()
if (now - this.lastCursorEventTime > 200) {
this.lastCursorEventTime = now
this.channel?.trigger('client-moved-cursor', {
id: this.id,
pageId,
point,
})
}
}
}
export default new RoomClient()

1
state/coop/server.ts Normal file
View file

@ -0,0 +1 @@
export {}

View file

@ -1,11 +1,6 @@
import { DrawShape, PointerInfo } from 'types'
import {
getCameraZoom,
getCurrentCamera,
getSelectedIds,
screenToWorld,
setToArray,
} from 'utils'
import { setToArray } from 'utils'
import tld from 'utils/tld'
import { freeze } from 'immer'
import session from './session'
import state from './state'
@ -24,12 +19,12 @@ export function fastDrawUpdate(info: PointerInfo): void {
session.update<Session.DrawSession>(
data,
screenToWorld(info.point, data),
tld.screenToWorld(info.point, data),
info.pressure,
info.shiftKey
)
const selectedId = setToArray(getSelectedIds(data))[0]
const selectedId = setToArray(tld.getSelectedIds(data))[0]
const shape = data.document.pages[data.currentPageId].shapes[
selectedId
@ -45,7 +40,7 @@ export function fastDrawUpdate(info: PointerInfo): void {
export function fastPanUpdate(delta: number[]): void {
const data = { ...state.data }
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
camera.point = vec.sub(camera.point, vec.div(delta, camera.zoom))
data.pageStates[data.currentPageId].camera = { ...camera }
@ -55,13 +50,13 @@ export function fastPanUpdate(delta: number[]): void {
export function fastZoomUpdate(point: number[], delta: number): void {
const data = { ...state.data }
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
const next = camera.zoom - (delta / 100) * camera.zoom
const p0 = screenToWorld(point, data)
camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(point, data)
const p0 = tld.screenToWorld(point, data)
camera.zoom = tld.getCameraZoom(next)
const p1 = tld.screenToWorld(point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
data.pageStates[data.currentPageId].camera = { ...camera }
@ -75,15 +70,15 @@ export function fastPinchCamera(
distanceDelta: number
): void {
const data = { ...state.data }
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
camera.point = vec.sub(camera.point, vec.div(delta, camera.zoom))
const next = camera.zoom - (distanceDelta / 350) * camera.zoom
const p0 = screenToWorld(point, data)
camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(point, data)
const p0 = tld.screenToWorld(point, data)
camera.zoom = tld.getCameraZoom(next)
const p1 = tld.screenToWorld(point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
const pageState = data.pageStates[data.currentPageId]
@ -97,7 +92,7 @@ export function fastPinchCamera(
export function fastBrushSelect(point: number[]): void {
const data = { ...state.data }
session.update<Session.BrushSession>(data, screenToWorld(point, data))
session.update<Session.BrushSession>(data, tld.screenToWorld(point, data))
state.forceData(freeze(data))
}
@ -107,7 +102,7 @@ export function fastTranslate(info: PointerInfo): void {
session.update<Session.TranslateSession>(
data,
screenToWorld(info.point, data),
tld.screenToWorld(info.point, data),
info.shiftKey,
info.altKey
)
@ -120,7 +115,7 @@ export function fastTransform(info: PointerInfo): void {
session.update<Session.TransformSession | Session.TransformSingleSession>(
data,
screenToWorld(info.point, data),
tld.screenToWorld(info.point, data),
info.shiftKey
)

View file

@ -2,15 +2,9 @@ import { ArrowShape, Data } from 'types'
import vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import {
getBoundsFromPoints,
getPage,
getSelectedIds,
setToArray,
updateParents,
} from 'utils'
import { deepClone, getBoundsFromPoints, setToArray } from 'utils'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
export default class ArrowSession extends BaseSession {
points: number[][]
@ -56,7 +50,7 @@ export default class ArrowSession extends BaseSession {
}
}
const shape = getPage(data).shapes[id] as ArrowShape
const shape = tld.getPage(data).shapes[id] as ArrowShape
getShapeUtils(shape).onHandleChange(shape, {
end: {
@ -65,25 +59,25 @@ export default class ArrowSession extends BaseSession {
},
})
updateParents(data, [shape.id])
tld.updateParents(data, [shape.id])
}
cancel(data: Data): void {
const { id, initialShape } = this.snapshot
const shape = getPage(data).shapes[id] as ArrowShape
const shape = tld.getPage(data).shapes[id] as ArrowShape
getShapeUtils(shape)
.onHandleChange(shape, { end: initialShape.handles.end })
.setProperty(shape, 'point', initialShape.point)
updateParents(data, [shape.id])
tld.updateParents(data, [shape.id])
}
complete(data: Data): void {
const { id } = this.snapshot
const shape = getPage(data).shapes[id] as ArrowShape
const shape = tld.getPage(data).shapes[id] as ArrowShape
const { start, end, bend } = shape.handles
@ -115,12 +109,12 @@ export default class ArrowSession extends BaseSession {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getArrowSnapshot(data: Data, id: string) {
const initialShape = getPage(current(data)).shapes[id] as ArrowShape
const initialShape = deepClone(tld.getPage(data).shapes[id]) as ArrowShape
return {
id,
initialShape,
selectedIds: setToArray(getSelectedIds(data)),
selectedIds: setToArray(tld.getSelectedIds(data)),
currentPageId: data.currentPageId,
}
}

View file

@ -1,16 +1,9 @@
import { current } from 'immer'
import { Bounds, Data, ShapeType } from 'types'
import BaseSession from './base-session'
import { getShapeUtils } from 'state/shape-utils'
import {
getBoundsFromPoints,
getPageState,
getShapes,
getTopParentId,
setSelectedIds,
setToArray,
} from 'utils'
import { deepClone, getBoundsFromPoints, setToArray } from 'utils'
import vec from 'utils/vec'
import tld from 'utils/tld'
export default class BrushSession extends BaseSession {
origin: number[]
@ -51,14 +44,14 @@ export default class BrushSession extends BaseSession {
}
}
getPageState(data).selectedIds = selectedIds
tld.getPageState(data).selectedIds = selectedIds
data.brush = brushBounds
}
cancel = (data: Data): void => {
data.brush = undefined
setSelectedIds(data, this.snapshot.selectedIds)
tld.setSelectedIds(data, this.snapshot.selectedIds)
}
complete = (data: Data): void => {
@ -73,14 +66,16 @@ export default class BrushSession extends BaseSession {
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getBrushSnapshot(data: Data) {
const cData = current(data)
const { selectedIds } = getPageState(cData)
const cData = data
const { selectedIds } = tld.getPageState(cData)
const shapesToTest = getShapes(cData)
const shapesToTest = tld
.getShapes(cData)
.filter((shape) => shape.type !== ShapeType.Group && !shape.isHidden)
.filter(
(shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId))
)
.map(deepClone)
return {
selectedIds: setToArray(selectedIds),
@ -89,9 +84,10 @@ export function getBrushSnapshot(data: Data) {
return [
shape.id,
{
selectId: getTopParentId(cData, shape.id),
test: (bounds: Bounds) =>
getShapeUtils(shape).hitTestBounds(shape, bounds),
selectId: tld.getTopParentId(data, shape.id),
test: (bounds: Bounds) => {
return getShapeUtils(shape).hitTestBounds(shape, bounds)
},
},
]
})

View file

@ -1,9 +1,10 @@
import { Data, LineShape, RayShape } from 'types'
import { Data, Shape } from 'types'
import vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import { getPage, getSelectedIds } from 'utils'
import tld from 'utils/tld'
import { deepClone } from 'utils'
import { getShapeUtils } from 'state/shape-utils'
export default class DirectionSession extends BaseSession {
delta = [0, 0]
@ -17,48 +18,52 @@ export default class DirectionSession extends BaseSession {
}
update(data: Data, point: number[]): void {
const { shapes } = this.snapshot
const page = tld.getPage(data)
const page = getPage(data)
this.snapshot.forEach((initialShape) => {
const shape = page.shapes[initialShape.id]
for (const { id } of shapes) {
const shape = page.shapes[id] as RayShape | LineShape
shape.direction = vec.uni(vec.vec(shape.point, point))
}
if ('direction' in shape) {
getShapeUtils(shape).setProperty(
shape,
'direction',
vec.uni(vec.vec(shape.point, point))
)
}
})
}
cancel(data: Data): void {
const page = getPage(data)
const page = tld.getPage(data)
for (const { id, direction } of this.snapshot.shapes) {
const shape = page.shapes[id] as RayShape | LineShape
shape.direction = direction
}
this.snapshot.forEach((initialShape) => {
const shape = page.shapes[initialShape.id]
if ('direction' in shape && 'direction' in initialShape) {
getShapeUtils(shape).setProperty(
shape,
'direction',
initialShape.direction
)
}
})
}
complete(data: Data): void {
commands.direct(data, this.snapshot, getDirectionSnapshot(data))
commands.mutate(
data,
this.snapshot,
getDirectionSnapshot(data),
'change_direction'
)
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getDirectionSnapshot(data: Data) {
const { shapes } = getPage(current(data))
const snapshapes: { id: string; direction: number[] }[] = []
getSelectedIds(data).forEach((id) => {
const shape = shapes[id]
if ('direction' in shape) {
snapshapes.push({ id: shape.id, direction: shape.direction })
}
})
return {
currentPageId: data.currentPageId,
shapes: snapshapes,
}
export function getDirectionSnapshot(data: Data): Shape[] {
return tld
.getSelectedShapes(data)
.filter((shape) => 'direction' in shape)
.map((shape) => deepClone(shape))
}
export type DirectionSnapshot = ReturnType<typeof getDirectionSnapshot>

View file

@ -1,8 +1,8 @@
import { current } from 'immer'
import { Data, DrawShape } from 'types'
import BaseSession from './base-session'
import { getShapeUtils } from 'state/shape-utils'
import { getBoundsFromPoints, getPage, getShape, updateParents } from 'utils'
import { deepClone, getBoundsFromPoints } from 'utils'
import tld from 'utils/tld'
import vec from 'utils/vec'
import commands from 'state/commands'
@ -27,11 +27,11 @@ export default class DrawSession extends BaseSession {
// points, this single point will be interpreted as a "dot" shape.
this.points = [[0, 0, 0.5]]
const shape = getPage(data).shapes[id]
const shape = tld.getPage(data).shapes[id]
getShapeUtils(shape).translateTo(shape, point)
updateParents(data, [shape.id])
tld.updateParents(data, [shape.id])
}
update = (
@ -103,26 +103,26 @@ export default class DrawSession extends BaseSession {
if (this.points.length <= 2) return
// Update the points and update the shape's parents.
const shape = getShape(data, snapshot.id) as DrawShape
const shape = tld.getShape(data, snapshot.id) as DrawShape
// Note: Normally we would want to spread the points to create a new
// array, however we create the new array in hacks/fastDrawUpdate.
getShapeUtils(shape).setProperty(shape, 'points', this.points)
updateParents(data, [shape.id])
tld.updateParents(data, [shape.id])
}
cancel = (data: Data): void => {
const { snapshot } = this
const shape = getShape(data, snapshot.id) as DrawShape
const shape = tld.getShape(data, snapshot.id) as DrawShape
getShapeUtils(shape).translateTo(shape, snapshot.point)
getShapeUtils(shape).setProperty(shape, 'points', snapshot.points)
updateParents(data, [shape.id])
tld.updateParents(data, [shape.id])
}
complete = (data: Data): void => {
const { snapshot } = this
const page = getPage(data)
const page = tld.getPage(data)
const shape = page.shapes[snapshot.id] as DrawShape
if (shape.points.length < this.points.length) {
@ -131,7 +131,7 @@ export default class DrawSession extends BaseSession {
getShapeUtils(shape).onSessionComplete(shape)
updateParents(data, [shape.id])
tld.updateParents(data, [shape.id])
commands.draw(data, this.snapshot.id)
}
@ -139,8 +139,8 @@ export default class DrawSession extends BaseSession {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getDrawSnapshot(data: Data, shapeId: string) {
const page = getPage(current(data))
const { points, point } = page.shapes[shapeId] as DrawShape
const page = tld.getPage(data)
const { points, point } = deepClone(page.shapes[shapeId]) as DrawShape
return {
id: shapeId,

View file

@ -1,9 +1,9 @@
import { Data, Shape } from 'types'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import { getPage, getSelectedShapes, getShape } from 'utils'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
import { deepClone } from 'utils'
export default class EditSession extends BaseSession {
snapshot: EditSnapshot
@ -15,7 +15,7 @@ export default class EditSession extends BaseSession {
update(data: Data, change: Partial<Shape>): void {
const initialShape = this.snapshot.initialShape
const shape = getShape(data, initialShape.id)
const shape = tld.getShape(data, initialShape.id)
const utils = getShapeUtils(shape)
Object.entries(change).forEach(([key, value]) => {
utils.setProperty(shape, key as keyof Shape, value as Shape[keyof Shape])
@ -24,7 +24,7 @@ export default class EditSession extends BaseSession {
cancel(data: Data): void {
const initialShape = this.snapshot.initialShape
const page = getPage(data)
const page = tld.getPage(data)
page.shapes[initialShape.id] = initialShape
}
@ -35,7 +35,7 @@ export default class EditSession extends BaseSession {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getEditSnapshot(data: Data) {
const initialShape = getSelectedShapes(current(data))[0]
const initialShape = deepClone(tld.getSelectedShapes(data)[0])
return {
currentPageId: data.currentPageId,

View file

@ -2,9 +2,9 @@ import { Data } from 'types'
import vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import { getPage } from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
import { deepClone } from 'utils'
export default class HandleSession extends BaseSession {
delta = [0, 0]
@ -19,7 +19,7 @@ export default class HandleSession extends BaseSession {
update(data: Data, point: number[], isAligned: boolean): void {
const { handleId, initialShape } = this.snapshot
const shape = getPage(data).shapes[initialShape.id]
const shape = tld.getPage(data).shapes[initialShape.id]
const delta = vec.vec(this.origin, point)
@ -47,7 +47,7 @@ export default class HandleSession extends BaseSession {
cancel(data: Data): void {
const { initialShape } = this.snapshot
getPage(data).shapes[initialShape.id] = initialShape
tld.getPage(data).shapes[initialShape.id] = initialShape
}
complete(data: Data): void {
@ -69,7 +69,7 @@ export function getHandleSnapshot(
shapeId: string,
handleId: string
) {
const initialShape = getPage(current(data)).shapes[shapeId]
const initialShape = deepClone(tld.getShape(data, shapeId))
return {
currentPageId: data.currentPageId,

View file

@ -2,19 +2,12 @@ import { Data, ShapeType } from 'types'
import vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import {
clampToRotationToSegments,
getBoundsCenter,
getCommonBounds,
getPage,
getRotatedBounds,
getShapeBounds,
updateParents,
getDocumentBranch,
setToArray,
getSelectedIds,
} from 'utils'
import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils'
const PI2 = Math.PI * 2
@ -34,7 +27,7 @@ export default class RotateSession extends BaseSession {
update(data: Data, point: number[], isLocked: boolean): void {
const { commonBoundsCenter, initialShapes } = this.snapshot
const page = getPage(data)
const page = tld.getPage(data)
const a1 = vec.angle(commonBoundsCenter, this.origin)
const a2 = vec.angle(commonBoundsCenter, point)
@ -69,7 +62,7 @@ export default class RotateSession extends BaseSession {
.translateTo(shape, nextPoint)
}
updateParents(
tld.updateParents(
data,
initialShapes.map((s) => s.id)
)
@ -77,7 +70,7 @@ export default class RotateSession extends BaseSession {
cancel(data: Data): void {
const { initialShapes } = this.snapshot
const page = getPage(data)
const page = tld.getPage(data)
for (const { id, point, rotation } of initialShapes) {
const shape = page.shapes[id]
@ -86,7 +79,7 @@ export default class RotateSession extends BaseSession {
.translateTo(shape, point)
}
updateParents(
tld.updateParents(
data,
initialShapes.map((s) => s.id)
)
@ -100,17 +93,12 @@ export default class RotateSession extends BaseSession {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getRotateSnapshot(data: Data) {
const cData = current(data)
const page = getPage(cData)
const initialShapes = setToArray(getSelectedIds(data))
.flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
.filter((shape) => !shape.isLocked)
const initialShapes = tld.getSelectedBranchSnapshot(data)
const hasUnlockedShapes = initialShapes.length > 0
const shapesBounds = Object.fromEntries(
initialShapes.map((shape) => [shape.id, getShapeBounds(shape)])
initialShapes.map((shape) => [shape.id, tld.getShapeBounds(shape)])
)
const bounds = getCommonBounds(...Object.values(shapesBounds))
@ -131,7 +119,7 @@ export function getRotateSnapshot(data: Data) {
const rotationOffset = vec.sub(
center,
getBoundsCenter(getRotatedBounds(shape))
getBoundsCenter(tld.getRotatedBounds(shape))
)
return {

View file

@ -3,18 +3,14 @@ import vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
import {
deepClone,
getBoundsCenter,
getBoundsFromPoints,
getCommonBounds,
getDocumentBranch,
getPage,
getRelativeTransformedBoundingBox,
getSelectedIds,
getTransformedBoundingBox,
setToArray,
updateParents,
} from 'utils'
export default class TransformSession extends BaseSession {
@ -36,7 +32,7 @@ export default class TransformSession extends BaseSession {
const { shapeBounds, initialBounds, isAllAspectRatioLocked } = this.snapshot
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
const newBoundingBox = getTransformedBoundingBox(
initialBounds,
@ -76,13 +72,13 @@ export default class TransformSession extends BaseSession {
shapes[id] = { ...shape }
}
updateParents(data, Object.keys(shapeBounds))
tld.updateParents(data, Object.keys(shapeBounds))
}
cancel(data: Data): void {
const { shapeBounds } = this.snapshot
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const id in shapeBounds) {
const shape = shapes[id]
@ -98,7 +94,7 @@ export default class TransformSession extends BaseSession {
transformOrigin,
})
updateParents(data, Object.keys(shapeBounds))
tld.updateParents(data, Object.keys(shapeBounds))
}
}
@ -107,7 +103,7 @@ export default class TransformSession extends BaseSession {
if (!hasUnlockedShapes) return
const page = getPage(data)
const page = tld.getPage(data)
const finalShapes = initialShapes.map((shape) =>
deepClone(page.shapes[shape.id])
@ -120,12 +116,8 @@ export default class TransformSession extends BaseSession {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
const { currentPageId } = data
const page = getPage(data)
const initialShapes = setToArray(getSelectedIds(data))
.flatMap((id) => getDocumentBranch(data, id).map((id) => page.shapes[id]))
.filter((shape) => !shape.isLocked)
.map((shape) => deepClone(shape))
const initialShapes = tld.getSelectedBranchSnapshot(data)
const hasUnlockedShapes = initialShapes.length > 0

View file

@ -2,15 +2,9 @@ import { Data, Edge, Corner } from 'types'
import vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import { getShapeUtils } from 'state/shape-utils'
import {
getTransformedBoundingBox,
getPage,
getShape,
getSelectedShapes,
updateParents,
} from 'utils'
import { deepClone, getTransformedBoundingBox } from 'utils'
import tld from 'utils/tld'
export default class TransformSingleSession extends BaseSession {
transformType: Edge | Corner
@ -38,7 +32,7 @@ export default class TransformSingleSession extends BaseSession {
const { initialShapeBounds, initialShape, id } = this.snapshot
const shape = getShape(data, id)
const shape = tld.getShape(data, id)
const newBoundingBox = getTransformedBoundingBox(
initialShapeBounds,
@ -61,16 +55,16 @@ export default class TransformSingleSession extends BaseSession {
data.document.pages[data.currentPageId].shapes[shape.id] = { ...shape }
updateParents(data, [id])
tld.updateParents(data, [id])
}
cancel(data: Data): void {
const { id, initialShape } = this.snapshot
const page = getPage(data)
const page = tld.getPage(data)
page.shapes[id] = initialShape
updateParents(data, [id])
tld.updateParents(data, [id])
}
complete(data: Data): void {
@ -90,7 +84,7 @@ export function getTransformSingleSnapshot(
data: Data,
transformType: Edge | Corner
) {
const shape = getSelectedShapes(current(data))[0]
const shape = deepClone(tld.getSelectedShapes(data)[0])
const bounds = getShapeUtils(shape).getBounds(shape)
return {

View file

@ -2,17 +2,9 @@ import { Data, GroupShape, Shape, ShapeType } from 'types'
import vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import { uniqueId } from 'utils'
import {
getChildIndexAbove,
getDocumentBranch,
getPage,
getSelectedShapes,
setSelectedIds,
updateParents,
} from 'utils'
import { getShapeUtils } from 'state/shape-utils'
import tld from 'utils/tld'
export default class TranslateSession extends BaseSession {
delta = [0, 0]
@ -34,7 +26,7 @@ export default class TranslateSession extends BaseSession {
isCloning: boolean
): void {
const { clones, initialShapes, initialParents } = this.snapshot
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
const delta = vec.vec(this.origin, point)
@ -89,12 +81,12 @@ export default class TranslateSession extends BaseSession {
shapes[id] = { ...shape }
}
setSelectedIds(
tld.setSelectedIds(
data,
clones.map((c) => c.id)
)
updateParents(
tld.updateParents(
data,
clones.map((c) => c.id)
)
@ -102,7 +94,7 @@ export default class TranslateSession extends BaseSession {
if (this.isCloning) {
this.isCloning = false
setSelectedIds(
tld.setSelectedIds(
data,
initialShapes.map((c) => c.id)
)
@ -112,7 +104,7 @@ export default class TranslateSession extends BaseSession {
}
for (const initialShape of initialShapes) {
getDocumentBranch(data, initialShape.id).forEach((id) => {
tld.getDocumentBranch(data, initialShape.id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateBy(shape, delta)
shapes[id] = { ...shape }
@ -126,7 +118,7 @@ export default class TranslateSession extends BaseSession {
}
for (const initialShape of initialShapes) {
getDocumentBranch(data, initialShape.id).forEach((id) => {
tld.getDocumentBranch(data, initialShape.id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateBy(shape, trueDelta)
@ -134,7 +126,7 @@ export default class TranslateSession extends BaseSession {
})
}
updateParents(
tld.updateParents(
data,
initialShapes.map((s) => s.id)
)
@ -143,10 +135,10 @@ export default class TranslateSession extends BaseSession {
cancel(data: Data): void {
const { initialShapes, initialParents, clones } = this.snapshot
const { shapes } = getPage(data)
const { shapes } = tld.getPage(data)
for (const { id } of initialShapes) {
getDocumentBranch(data, id).forEach((id) => {
tld.getDocumentBranch(data, id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateBy(shape, vec.neg(this.delta))
})
@ -161,7 +153,7 @@ export default class TranslateSession extends BaseSession {
getShapeUtils(shape).setProperty(shape, 'children', children)
})
updateParents(
tld.updateParents(
data,
initialShapes.map((s) => s.id)
)
@ -181,32 +173,28 @@ export default class TranslateSession extends BaseSession {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getTranslateSnapshot(data: Data) {
const cData = current(data)
const page = tld.getPage(data)
// Get selected shapes
// Filter out the locked shapes
// Collect the branch children for each remaining shape
// Filter out doubles using a set
// End up with an array of ids for all of the shapes that will change
// Map into shapes from data snapshot
const page = getPage(cData)
const selectedShapes = getSelectedShapes(cData).filter(
(shape) => !shape.isLocked
)
const selectedShapes = tld.getSelectedShapeSnapshot(data)
const hasUnlockedShapes = selectedShapes.length > 0
const parents = Array.from(
const initialParents = Array.from(
new Set(selectedShapes.map((s) => s.parentId)).values()
)
.filter((id) => id !== data.currentPageId)
.map((id) => page.shapes[id])
.map((id) => {
const shape = page.shapes[id]
return {
id: shape.id,
children: shape.children,
}
})
return {
hasUnlockedShapes,
currentPageId: data.currentPageId,
initialParents: parents.map(({ id, children }) => ({ id, children })),
initialParents,
initialShapes: selectedShapes.map(({ id, point, parentId }) => ({
id,
point,
@ -218,9 +206,8 @@ export function getTranslateSnapshot(data: Data) {
const clone: Shape = {
...shape,
id: uniqueId(),
parentId: shape.parentId,
childIndex: getChildIndexAbove(cData, shape.id),
childIndex: tld.getChildIndexAbove(data, shape.id),
isGenerated: false,
}
@ -236,7 +223,7 @@ export type TranslateSnapshot = ReturnType<typeof getTranslateSnapshot>
// return [clone]
// }
// const page = getPage(data)
// const page = tld.getPage(data)
// const childClones = clone.children.flatMap((id) => {
// const newId = uniqueId()
// const source = page.shapes[id]

View file

@ -7,29 +7,18 @@ import history from './history'
import storage from './storage'
import clipboard from './clipboard'
import * as Sessions from './sessions'
import coopClient from './coop/client-pusher'
import commands from './commands'
import {
getChildren,
getCommonBounds,
getCurrentCamera,
getPage,
getSelectedBounds,
getSelectedShapes,
getShape,
screenToWorld,
setZoomCSS,
rotateBounds,
getBoundsCenter,
getDocumentBranch,
getCameraZoom,
getSelectedIds,
setSelectedIds,
getPageState,
setToArray,
deepClone,
pointInBounds,
uniqueId,
} from 'utils'
import tld from 'utils/tld'
import {
Data,
PointerInfo,
@ -173,6 +162,20 @@ const state = createState({
else: ['zoomCameraToActual'],
},
on: {
// Network-Related
RT_LOADED_ROOM: [
'clearRoom',
{ if: 'hasRoom', do: ['clearDocument', 'connectToRoom'] },
],
RT_UNLOADED_ROOM: ['clearRoom', 'clearDocument'],
RT_DISCONNECTED_ROOM: ['clearRoom', 'clearDocument'],
RT_CREATED_SHAPE: 'addRtShape',
RT_CHANGED_STATUS: 'setRtStatus',
RT_DELETED_SHAPE: 'deleteRtShape',
RT_EDITED_SHAPE: 'editRtShape',
RT_MOVED_CURSOR: 'moveRtCursor',
// Client
MOVED_POINTER: { secretlyDo: 'sendRtCursorMove' },
RESIZED_WINDOW: 'resetPageState',
RESET_PAGE: 'resetPage',
TOGGLED_READ_ONLY: 'toggleReadOnly',
@ -1025,13 +1028,16 @@ const state = createState({
return ShapeType.Rectangle
},
firstSelectedShape(data) {
return getSelectedShapes(data)[0]
return tld.getSelectedShapes(data)[0]
},
editingShape(data) {
return getShape(data, data.editingId)
return tld.getShape(data, data.editingId)
},
},
conditions: {
hasRoom(_, payload: { id?: string }) {
return payload.id !== undefined
},
shouldDeleteShape(data, payload, shape: Shape) {
return getShapeUtils(shape).shouldDelete(shape)
},
@ -1039,7 +1045,7 @@ const state = createState({
return payload.target === 'canvas'
},
isPointingBounds(data, payload: PointerInfo) {
return getSelectedIds(data).size > 0 && payload.target === 'bounds'
return tld.getSelectedIds(data).size > 0 && payload.target === 'bounds'
},
isPointingShape(data, payload: PointerInfo) {
return (
@ -1064,7 +1070,7 @@ const state = createState({
return payload.target !== undefined
},
isPointedShapeSelected(data) {
return getSelectedIds(data).has(data.pointedId)
return tld.getSelectedIds(data).has(data.pointedId)
},
isPressingShiftKey(data, payload: PointerInfo) {
return payload.shiftKey
@ -1080,18 +1086,18 @@ const state = createState({
if (!bounds) return false
return pointInBounds(screenToWorld(payload.point, data), bounds)
return pointInBounds(tld.screenToWorld(payload.point, data), bounds)
},
pointHitsShape(data, payload: PointerInfo) {
const shape = getShape(data, payload.target)
const shape = tld.getShape(data, payload.target)
return getShapeUtils(shape).hitTest(
shape,
screenToWorld(payload.point, data)
tld.screenToWorld(payload.point, data)
)
},
hasPointedId(data, payload: PointerInfo) {
return getShape(data, payload.target) !== undefined
return tld.getShape(data, payload.target) !== undefined
},
isPointingRotationHandle(
data,
@ -1100,13 +1106,13 @@ const state = createState({
return payload.target === 'rotate'
},
hasSelection(data) {
return getSelectedIds(data).size > 0
return tld.getSelectedIds(data).size > 0
},
hasSingleSelection(data) {
return getSelectedIds(data).size === 1
return tld.getSelectedIds(data).size === 1
},
hasMultipleSelection(data) {
return getSelectedIds(data).size > 1
return tld.getSelectedIds(data).size > 1
},
isToolLocked(data) {
return data.settings.isToolLocked
@ -1118,12 +1124,99 @@ const state = createState({
return Object.keys(data.document.pages).length === 1
},
selectionIncludesGroups(data) {
return getSelectedShapes(data).some(
(shape) => shape.type === ShapeType.Group
)
return tld
.getSelectedShapes(data)
.some((shape) => shape.type === ShapeType.Group)
},
},
actions: {
// Networked Room
setRtStatus(data, payload: { id: string; status: string }) {
const { status } = payload
if (!data.room) {
data.room = {
id: null,
status: '',
peers: {},
}
}
data.room.peers = {}
data.room.status = status
},
addRtShape(data, payload: { pageId: string; shape: Shape }) {
const { pageId, shape } = payload
// What if the page is in storage?
data.document.pages[pageId].shapes[shape.id] = shape
},
deleteRtShape(data, payload: { pageId: string; shapeId: string }) {
const { pageId, shapeId } = payload
// What if the page is in storage?
delete data.document[pageId].shapes[shapeId]
},
editRtShape(data, payload: { pageId: string; shape: Shape }) {
const { pageId, shape } = payload
// What if the page is in storage?
Object.assign(data.document[pageId].shapes[shape.id], shape)
},
sendRtCursorMove(data, payload: PointerInfo) {
const point = tld.screenToWorld(payload.point, data)
coopClient.moveCursor(data.currentPageId, point)
},
moveRtCursor(
data,
payload: { id: string; pageId: string; point: number[] }
) {
const { room } = data
if (room.peers[payload.id] === undefined) {
room.peers[payload.id] = {
id: payload.id,
cursor: {
point: payload.point,
},
}
}
room.peers[payload.id].cursor.point = payload.point
},
clearRoom(data) {
data.room = undefined
},
clearDocument(data) {
data.document.id = uniqueId()
const newId = 'page1'
data.currentPageId = newId
data.document.pages = {
[newId]: {
id: newId,
name: 'Page 1',
type: 'page',
shapes: {},
childIndex: 1,
},
}
data.pageStates = {
[newId]: {
id: newId,
selectedIds: new Set(),
camera: {
point: [0, 0],
zoom: 1,
},
},
}
},
connectToRoom(data, payload: { id: string }) {
data.room = { id: payload.id, status: 'connecting', peers: {} }
coopClient.connect(payload.id)
},
resetPageState(data) {
const pageState = data.pageStates[data.currentPageId]
data.pageStates[data.currentPageId] = { ...pageState }
@ -1144,7 +1237,7 @@ const state = createState({
},
/* --------------------- Shapes --------------------- */
resetShapes(data) {
const page = getPage(data)
const page = tld.getPage(data)
Object.values(page.shapes).forEach((shape) => {
page.shapes[shape.id] = { ...shape }
})
@ -1153,11 +1246,11 @@ const state = createState({
createShape(data, payload, type: ShapeType) {
const shape = createShape(type, {
parentId: data.currentPageId,
point: vec.round(screenToWorld(payload.point, data)),
point: vec.round(tld.screenToWorld(payload.point, data)),
style: deepClone(data.currentStyle),
})
const siblings = getChildren(data, shape.parentId)
const siblings = tld.getChildren(data, shape.parentId)
const childIndex = siblings.length
? siblings[siblings.length - 1].childIndex + 1
: 1
@ -1166,9 +1259,9 @@ const state = createState({
getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
getPage(data).shapes[shape.id] = shape
tld.getPage(data).shapes[shape.id] = shape
setSelectedIds(data, [shape.id])
tld.setSelectedIds(data, [shape.id])
},
/* -------------------- Sessions -------------------- */
@ -1197,33 +1290,33 @@ const state = createState({
// Brushing
startBrushSession(data, payload: PointerInfo) {
session.begin(
new Sessions.BrushSession(data, screenToWorld(payload.point, data))
new Sessions.BrushSession(data, tld.screenToWorld(payload.point, data))
)
},
updateBrushSession(data, payload: PointerInfo) {
session.update<Sessions.BrushSession>(
data,
screenToWorld(payload.point, data)
tld.screenToWorld(payload.point, data)
)
},
// Rotating
startRotateSession(data, payload: PointerInfo) {
session.begin(
new Sessions.RotateSession(data, screenToWorld(payload.point, data))
new Sessions.RotateSession(data, tld.screenToWorld(payload.point, data))
)
},
keyUpdateRotateSession(data, payload: PointerInfo) {
session.update<Sessions.RotateSession>(
data,
screenToWorld(inputs.pointer.point, data),
tld.screenToWorld(inputs.pointer.point, data),
payload.shiftKey
)
},
updateRotateSession(data, payload: PointerInfo) {
session.update<Sessions.RotateSession>(
data,
screenToWorld(payload.point, data),
tld.screenToWorld(payload.point, data),
payload.shiftKey
)
},
@ -1233,7 +1326,7 @@ const state = createState({
session.begin(
new Sessions.TranslateSession(
data,
screenToWorld(inputs.pointer.origin, data)
tld.screenToWorld(inputs.pointer.origin, data)
)
)
},
@ -1243,7 +1336,7 @@ const state = createState({
) {
session.update<Sessions.TranslateSession>(
data,
screenToWorld(inputs.pointer.point, data),
tld.screenToWorld(inputs.pointer.point, data),
payload.shiftKey,
payload.altKey
)
@ -1251,7 +1344,7 @@ const state = createState({
updateTranslateSession(data, payload: PointerInfo) {
session.update<Sessions.TranslateSession>(
data,
screenToWorld(payload.point, data),
tld.screenToWorld(payload.point, data),
payload.shiftKey,
payload.altKey
)
@ -1259,13 +1352,13 @@ const state = createState({
// Handles
doublePointHandle(data, payload: PointerInfo) {
const id = setToArray(getSelectedIds(data))[0]
const id = setToArray(tld.getSelectedIds(data))[0]
commands.doublePointHandle(data, id, payload)
},
// Dragging Handle
startHandleSession(data, payload: PointerInfo) {
const shapeId = Array.from(getSelectedIds(data).values())[0]
const shapeId = Array.from(tld.getSelectedIds(data).values())[0]
const handleId = payload.target
session.begin(
@ -1273,7 +1366,7 @@ const state = createState({
data,
shapeId,
handleId,
screenToWorld(inputs.pointer.origin, data)
tld.screenToWorld(inputs.pointer.origin, data)
)
)
},
@ -1283,14 +1376,14 @@ const state = createState({
) {
session.update<Sessions.HandleSession>(
data,
screenToWorld(inputs.pointer.point, data),
tld.screenToWorld(inputs.pointer.point, data),
payload.shiftKey
)
},
updateHandleSession(data, payload: PointerInfo) {
session.update<Sessions.HandleSession>(
data,
screenToWorld(payload.point, data),
tld.screenToWorld(payload.point, data),
payload.shiftKey
)
},
@ -1300,9 +1393,9 @@ const state = createState({
data,
payload: PointerInfo & { target: Corner | Edge }
) {
const point = screenToWorld(inputs.pointer.origin, data)
const point = tld.screenToWorld(inputs.pointer.origin, data)
session.begin(
getSelectedIds(data).size === 1
tld.getSelectedIds(data).size === 1
? new Sessions.TransformSingleSession(data, payload.target, point)
: new Sessions.TransformSession(data, payload.target, point)
)
@ -1312,7 +1405,7 @@ const state = createState({
new Sessions.TransformSingleSession(
data,
Corner.BottomRight,
screenToWorld(payload.point, data),
tld.screenToWorld(payload.point, data),
true
)
)
@ -1320,14 +1413,14 @@ const state = createState({
keyUpdateTransformSession(data, payload: PointerInfo) {
session.update<Sessions.TransformSession>(
data,
screenToWorld(inputs.pointer.point, data),
tld.screenToWorld(inputs.pointer.point, data),
payload.shiftKey
)
},
updateTransformSession(data, payload: PointerInfo) {
session.update<Sessions.TransformSession>(
data,
screenToWorld(payload.point, data),
tld.screenToWorld(payload.point, data),
payload.shiftKey
)
},
@ -1337,32 +1430,32 @@ const state = createState({
session.begin(
new Sessions.DirectionSession(
data,
screenToWorld(inputs.pointer.origin, data)
tld.screenToWorld(inputs.pointer.origin, data)
)
)
},
updateDirectionSession(data, payload: PointerInfo) {
session.update<Sessions.DirectionSession>(
data,
screenToWorld(payload.point, data)
tld.screenToWorld(payload.point, data)
)
},
// Drawing
startDrawSession(data) {
const id = Array.from(getSelectedIds(data).values())[0]
const id = Array.from(tld.getSelectedIds(data).values())[0]
session.begin(
new Sessions.DrawSession(
data,
id,
screenToWorld(inputs.pointer.origin, data)
tld.screenToWorld(inputs.pointer.origin, data)
)
)
},
keyUpdateDrawSession(data, payload: PointerInfo) {
session.update<Sessions.DrawSession>(
data,
screenToWorld(inputs.pointer.point, data),
tld.screenToWorld(inputs.pointer.point, data),
payload.pressure,
payload.shiftKey
)
@ -1370,7 +1463,7 @@ const state = createState({
updateDrawSession(data, payload: PointerInfo) {
session.update<Sessions.DrawSession>(
data,
screenToWorld(payload.point, data),
tld.screenToWorld(payload.point, data),
payload.pressure,
payload.shiftKey
)
@ -1378,13 +1471,13 @@ const state = createState({
// Arrow
startArrowSession(data, payload: PointerInfo) {
const id = Array.from(getSelectedIds(data).values())[0]
const id = Array.from(tld.getSelectedIds(data).values())[0]
session.begin(
new Sessions.ArrowSession(
data,
id,
screenToWorld(inputs.pointer.origin, data),
tld.screenToWorld(inputs.pointer.origin, data),
payload.shiftKey
)
)
@ -1392,14 +1485,14 @@ const state = createState({
keyUpdateArrowSession(data, payload: PointerInfo) {
session.update<Sessions.ArrowSession>(
data,
screenToWorld(inputs.pointer.point, data),
tld.screenToWorld(inputs.pointer.point, data),
payload.shiftKey
)
},
updateArrowSession(data, payload: PointerInfo) {
session.update<Sessions.ArrowSession>(
data,
screenToWorld(payload.point, data),
tld.screenToWorld(payload.point, data),
payload.shiftKey
)
},
@ -1424,8 +1517,8 @@ const state = createState({
},
selectAll(data) {
const selectedIds = getSelectedIds(data)
const page = getPage(data)
const selectedIds = tld.getSelectedIds(data)
const page = tld.getPage(data)
selectedIds.clear()
for (const id in page.shapes) {
if (page.shapes[id].parentId === data.currentPageId) {
@ -1455,15 +1548,15 @@ const state = createState({
data.pointedId = undefined
},
clearSelectedIds(data) {
setSelectedIds(data, [])
tld.setSelectedIds(data, [])
},
pullPointedIdFromSelectedIds(data) {
const { pointedId } = data
const selectedIds = getSelectedIds(data)
const selectedIds = tld.getSelectedIds(data)
selectedIds.delete(pointedId)
},
pushPointedIdToSelectedIds(data) {
getSelectedIds(data).add(data.pointedId)
tld.getSelectedIds(data).add(data.pointedId)
},
moveSelection(data, payload: { type: MoveType }) {
commands.move(data, payload.type)
@ -1514,12 +1607,12 @@ const state = createState({
/* --------------------- Editing -------------------- */
setEditingId(data) {
const selectedShape = getSelectedShapes(data)[0]
const selectedShape = tld.getSelectedShapes(data)[0]
if (getShapeUtils(selectedShape).canEdit) {
data.editingId = selectedShape.id
}
getPageState(data).selectedIds = new Set([selectedShape.id])
tld.getPageState(data).selectedIds = new Set([selectedShape.id])
},
clearEditingId(data) {
data.editingId = null
@ -1564,44 +1657,43 @@ const state = createState({
/* --------------------- Camera --------------------- */
zoomIn(data) {
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
const i = Math.round((camera.zoom * 100) / 25)
const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data)
camera.zoom = getCameraZoom((i + 1) * 0.25)
const p1 = screenToWorld(center, data)
const p0 = tld.screenToWorld(center, data)
camera.zoom = tld.getCameraZoom((i + 1) * 0.25)
const p1 = tld.screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
zoomOut(data) {
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
const i = Math.round((camera.zoom * 100) / 25)
const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data)
camera.zoom = getCameraZoom((i - 1) * 0.25)
const p1 = screenToWorld(center, data)
const p0 = tld.screenToWorld(center, data)
camera.zoom = tld.getCameraZoom((i - 1) * 0.25)
const p1 = tld.screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
zoomCameraToActual(data) {
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data)
const p0 = tld.screenToWorld(center, data)
camera.zoom = 1
const p1 = screenToWorld(center, data)
const p1 = tld.screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
zoomCameraToSelectionActual(data) {
const camera = getCurrentCamera(data)
const bounds = getSelectedBounds(data)
const camera = tld.getCurrentCamera(data)
const bounds = tld.getSelectedBounds(data)
const mx = (window.innerWidth - bounds.width) / 2
const my = (window.innerHeight - bounds.height) / 2
@ -1609,13 +1701,13 @@ const state = createState({
camera.zoom = 1
camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
zoomCameraToSelection(data) {
const camera = getCurrentCamera(data)
const bounds = getSelectedBounds(data)
const camera = tld.getCurrentCamera(data)
const bounds = tld.getSelectedBounds(data)
const zoom = getCameraZoom(
const zoom = tld.getCameraZoom(
bounds.width > bounds.height
? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height
@ -1627,11 +1719,11 @@ const state = createState({
camera.zoom = zoom
camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
zoomCameraToFit(data) {
const camera = getCurrentCamera(data)
const page = getPage(data)
const camera = tld.getCurrentCamera(data)
const page = tld.getPage(data)
const shapes = Object.values(page.shapes)
@ -1645,7 +1737,7 @@ const state = createState({
)
)
const zoom = getCameraZoom(
const zoom = tld.getCameraZoom(
bounds.width > bounds.height
? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height
@ -1657,26 +1749,26 @@ const state = createState({
camera.zoom = zoom
camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
zoomCamera(data, payload: { delta: number; point: number[] }) {
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
const next = camera.zoom - (payload.delta / 100) * camera.zoom
const p0 = screenToWorld(payload.point, data)
camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(payload.point, data)
const p0 = tld.screenToWorld(payload.point, data)
camera.zoom = tld.getCameraZoom(next)
const p1 = tld.screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
panCamera(data, payload: { delta: number[] }) {
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
},
updateZoomCSS(data) {
const camera = getCurrentCamera(data)
setZoomCSS(camera.zoom)
const camera = tld.getCurrentCamera(data)
tld.setZoomCSS(camera.zoom)
},
pinchCamera(
data,
@ -1689,20 +1781,20 @@ const state = createState({
) {
// This is usually replaced with hacks.fastPinchCamera!
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
const p0 = screenToWorld(payload.point, data)
camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(payload.point, data)
const p0 = tld.screenToWorld(payload.point, data)
camera.zoom = tld.getCameraZoom(next)
const p1 = tld.screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
setZoomCSS(camera.zoom)
tld.setZoomCSS(camera.zoom)
},
resetCamera(data) {
const camera = getCurrentCamera(data)
const camera = tld.getCurrentCamera(data)
camera.zoom = 1
camera.point = [window.innerWidth / 2, window.innerHeight / 2]
document.documentElement.style.setProperty('--camera-zoom', '1')
@ -1763,7 +1855,7 @@ const state = createState({
commands.generate(data, payload.shapes)
},
updateGeneratedShapes(data, payload, result: { shapes: Shape[] }) {
setSelectedIds(data, [])
tld.setSelectedIds(data, [])
history.disable()
@ -1807,7 +1899,7 @@ const state = createState({
},
copyToClipboard(data) {
clipboard.copy(getSelectedShapes(data))
clipboard.copy(tld.getSelectedShapes(data))
},
copyStateToClipboard(data) {
@ -1880,26 +1972,26 @@ const state = createState({
},
values: {
selectedIds(data) {
return setToArray(getSelectedIds(data))
return setToArray(tld.getSelectedIds(data))
},
selectedBounds(data) {
return getSelectionBounds(data)
},
currentShapes(data) {
const page = getPage(data)
const page = tld.getPage(data)
return Object.values(page.shapes)
.filter((shape) => shape.parentId === page.id)
.sort((a, b) => a.childIndex - b.childIndex)
},
selectedStyle(data) {
const selectedIds = Array.from(getSelectedIds(data).values())
const selectedIds = setToArray(tld.getSelectedIds(data))
const { currentStyle } = data
if (selectedIds.length === 0) {
return currentStyle
}
const page = getPage(data)
const page = tld.getPage(data)
const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
@ -1930,12 +2022,12 @@ export default state
export const useSelector = createSelectorHook(state)
function getParentId(data: Data, id: string) {
const shape = getPage(data).shapes[id]
const shape = tld.getPage(data).shapes[id]
return shape.parentId
}
function getPointedId(data: Data, id: string) {
const shape = getPage(data).shapes[id]
const shape = tld.getPage(data).shapes[id]
if (!shape) return id
return shape.parentId === data.currentParentId ||
@ -1945,7 +2037,7 @@ function getPointedId(data: Data, id: string) {
}
function getDrilledPointedId(data: Data, id: string) {
const shape = getPage(data).shapes[id]
const shape = tld.getPage(data).shapes[id]
return shape.parentId === data.currentPageId ||
shape.parentId === data.pointedId ||
shape.parentId === data.currentParentId
@ -1970,18 +2062,18 @@ function getDrilledPointedId(data: Data, id: string) {
// }
function getSelectionBounds(data: Data) {
const selectedIds = getSelectedIds(data)
const selectedIds = tld.getSelectedIds(data)
const page = getPage(data)
const page = tld.getPage(data)
const shapes = getSelectedShapes(data)
const shapes = tld.getSelectedShapes(data)
if (selectedIds.size === 0) return null
if (selectedIds.size === 1) {
if (!shapes[0]) {
console.error('Could not find that shape! Clearing selected IDs.')
setSelectedIds(data, [])
console.warn('Could not find that shape! Clearing selected IDs.')
tld.setSelectedIds(data, [])
return null
}
@ -2014,7 +2106,7 @@ function getSelectionBounds(data: Data) {
const uniqueSelectedShapeIds: string[] = Array.from(
new Set(
Array.from(selectedIds.values()).flatMap((id) =>
getDocumentBranch(data, id)
tld.getDocumentBranch(data, id)
)
).values()
)

View file

@ -14,6 +14,11 @@ export interface Data {
isToolLocked: boolean
isPenLocked: boolean
}
room?: {
id: string
status: string
peers: Record<string, Peer>
}
currentStyle: ShapeStyles
activeTool: ShapeType | 'select'
brush?: Bounds
@ -33,6 +38,13 @@ export interface Data {
/* Document */
/* -------------------------------------------------- */
export interface Peer {
id: string
cursor: {
point: number[]
}
}
export interface TLDocument {
id: string
name: string
@ -190,43 +202,39 @@ export interface GroupShape extends BaseShape {
size: number[]
}
// type DeepPartial<T> = {
// [P in keyof T]?: DeepPartial<T[P]>
// }
export type ShapeProps<T extends Shape> = {
[P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
}
export type MutableShape =
| DotShape
| EllipseShape
| LineShape
| RayShape
| PolylineShape
| DrawShape
| RectangleShape
| ArrowShape
| TextShape
| GroupShape
export interface Shapes {
[ShapeType.Dot]: Readonly<DotShape>
[ShapeType.Ellipse]: Readonly<EllipseShape>
[ShapeType.Line]: Readonly<LineShape>
[ShapeType.Ray]: Readonly<RayShape>
[ShapeType.Polyline]: Readonly<PolylineShape>
[ShapeType.Draw]: Readonly<DrawShape>
[ShapeType.Rectangle]: Readonly<RectangleShape>
[ShapeType.Arrow]: Readonly<ArrowShape>
[ShapeType.Text]: Readonly<TextShape>
[ShapeType.Group]: Readonly<GroupShape>
export interface MutableShapes {
[ShapeType.Dot]: DotShape
[ShapeType.Ellipse]: EllipseShape
[ShapeType.Line]: LineShape
[ShapeType.Ray]: RayShape
[ShapeType.Polyline]: PolylineShape
[ShapeType.Draw]: DrawShape
[ShapeType.Rectangle]: RectangleShape
[ShapeType.Arrow]: ArrowShape
[ShapeType.Text]: TextShape
[ShapeType.Group]: GroupShape
}
export type MutableShape = MutableShapes[keyof MutableShapes]
export type Shapes = { [K in keyof MutableShapes]: Readonly<MutableShapes[K]> }
export type Shape = Readonly<MutableShape>
export type ShapeByType<T extends ShapeType> = Shapes[T]
export type IsParent<T> = 'children' extends RequiredKeys<T> ? T : never
export type ParentShape = {
[K in keyof MutableShapes]: IsParent<MutableShapes[K]>
}[keyof MutableShapes]
export type ParentTypes = ParentShape['type'] & 'page'
export enum Decoration {
Arrow = 'Arrow',
}
@ -321,8 +329,6 @@ export interface BoundsSnapshot extends PointSnapshot {
nh: number
}
export type Difference<A, B> = A extends B ? never : A
export type ShapeSpecificProps<T extends Shape> = Pick<
T,
Difference<keyof T, keyof BaseShape>
@ -605,3 +611,13 @@ export interface ShapeUtility<K extends Shape> {
// Get whether the shape should render
shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
}
/* -------------------------------------------------- */
/* Utilities */
/* -------------------------------------------------- */
export type Difference<A, B> = A extends B ? never : A
export type RequiredKeys<T> = {
[K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K
}[keyof T]

535
utils/tld.ts Normal file
View file

@ -0,0 +1,535 @@
import { clamp, deepClone, getCommonBounds, setToArray } from './utils'
import { getShapeUtils } from 'state/shape-utils'
import vec from './vec'
import {
Data,
Bounds,
Shape,
GroupShape,
ShapeType,
CodeFile,
Page,
PageState,
ShapeUtility,
ParentShape,
} from 'types'
import { AssertionError } from 'assert'
export default class ProjectUtils {
static getCameraZoom(zoom: number): number {
return clamp(zoom, 0.1, 5)
}
static screenToWorld(point: number[], data: Data): number[] {
const camera = this.getCurrentCamera(data)
return vec.sub(vec.div(point, camera.zoom), camera.point)
}
static getViewport(data: Data): Bounds {
const [minX, minY] = this.screenToWorld([0, 0], data)
const [maxX, maxY] = this.screenToWorld(
[window.innerWidth, window.innerHeight],
data
)
return {
minX,
minY,
maxX,
maxY,
height: maxX - minX,
width: maxY - minY,
}
}
static getCurrentCamera(data: Data): {
point: number[]
zoom: number
} {
return data.pageStates[data.currentPageId].camera
}
/**
* Get a shape from the project.
* @param data
* @param shapeId
*/
static getShape(data: Data, shapeId: string): Shape {
return data.document.pages[data.currentPageId].shapes[shapeId]
}
/**
* Get the current page.
* @param data
*/
static getPage(data: Data): Page {
return data.document.pages[data.currentPageId]
}
/**
* Get the current page's page state.
* @param data
*/
static getPageState(data: Data): PageState {
return data.pageStates[data.currentPageId]
}
/**
* Get the current page's code file.
* @param data
* @param fileId
*/
static getCurrentCode(data: Data, fileId: string): CodeFile {
return data.document.code[fileId]
}
/**
* Get the current page's shapes as an array.
* @param data
*/
static getShapes(data: Data): Shape[] {
const page = this.getPage(data)
return Object.values(page.shapes)
}
/**
* Get the current selected shapes as an array.
* @param data
*/
static getSelectedShapes(data: Data): Shape[] {
const page = this.getPage(data)
const ids = setToArray(this.getSelectedIds(data))
return ids.map((id) => page.shapes[id])
}
/**
* Get a shape's parent.
* @param data
* @param id
*/
static getParent(data: Data, id: string): Shape | Page {
const page = this.getPage(data)
const shape = page.shapes[id]
return page.shapes[shape.parentId] || data.document.pages[shape.parentId]
}
/**
* Get a shape's children.
* @param data
* @param id
*/
static getChildren(data: Data, id: string): Shape[] {
const page = this.getPage(data)
return Object.values(page.shapes)
.filter(({ parentId }) => parentId === id)
.sort((a, b) => a.childIndex - b.childIndex)
}
/**
* Get a shape's siblings.
* @param data
* @param id
*/
static getSiblings(data: Data, id: string): Shape[] {
const page = this.getPage(data)
const shape = page.shapes[id]
return Object.values(page.shapes)
.filter(({ parentId }) => parentId === shape.parentId)
.sort((a, b) => a.childIndex - b.childIndex)
}
/**
* Get the next child index above a shape.
* @param data
* @param id
*/
static getChildIndexAbove(data: Data, id: string): number {
const page = this.getPage(data)
const shape = page.shapes[id]
const siblings = Object.values(page.shapes)
.filter(({ parentId }) => parentId === shape.parentId)
.sort((a, b) => a.childIndex - b.childIndex)
const index = siblings.indexOf(shape)
const nextSibling = siblings[index + 1]
if (!nextSibling) {
return shape.childIndex + 1
}
let nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
if (nextIndex === nextSibling.childIndex) {
this.forceIntegerChildIndices(siblings)
nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
}
return nextIndex
}
/**
* Get the next child index below a shape.
* @param data
* @param id
* @param pageId
*/
static getChildIndexBelow(data: Data, id: string): number {
const page = this.getPage(data)
const shape = page.shapes[id]
const siblings = Object.values(page.shapes)
.filter(({ parentId }) => parentId === shape.parentId)
.sort((a, b) => a.childIndex - b.childIndex)
const index = siblings.indexOf(shape)
const prevSibling = siblings[index - 1]
if (!prevSibling) {
return shape.childIndex / 2
}
let nextIndex = (shape.childIndex + prevSibling.childIndex) / 2
if (nextIndex === prevSibling.childIndex) {
this.forceIntegerChildIndices(siblings)
nextIndex = (shape.childIndex + prevSibling.childIndex) / 2
}
return (shape.childIndex + prevSibling.childIndex) / 2
}
/**
* Assert whether a shape can have child shapes.
* @param shape
*/
static assertParentShape(shape: Shape): asserts shape is ParentShape {
if (!('children' in shape)) {
throw new AssertionError({
message: `That shape was not a parent (it was a ${shape.type}).`,
})
}
}
/**
* Get the top child index for a shape. This is potentially provisional:
* sorting all shapes on the page for each new created shape will become
* slower as the page grows. High indices aren't a problem, so consider
* tracking the highest index for the page when shapes are created / deleted.
*
* @param data
* @param id
*/
static getTopChildIndex(data: Data, parent: Shape | Page): number {
const page = this.getPage(data)
// If the parent is a shape, return either 1 (if no other shapes) or the
// highest sorted child index + 1.
if (parent.type === 'page') {
const children = Object.values(parent.shapes)
if (children.length === 0) return 1
return (
children.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
)
}
// If the shape is a regular shape that can accept children, return either
// 1 (if no other children) or the highest sorted child index + 1.
this.assertParentShape(parent)
if (parent.children.length === 0) return 1
return (
parent.children
.map((id) => page.shapes[id])
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
)
}
/**
* TODO: Make this recursive, so that it works for parented shapes.
* Force all shapes on the page to have integer child indices.
* @param shapes
*/
static forceIntegerChildIndices(shapes: Shape[]): void {
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i]
getShapeUtils(shape).setProperty(shape, 'childIndex', i + 1)
}
}
/**
* Update the zoom CSS variable.
* @param zoom ;
*/
static setZoomCSS(zoom: number): void {
document.documentElement.style.setProperty('--camera-zoom', zoom.toString())
}
/* --------------------- Groups --------------------- */
static getParentOffset(
data: Data,
shapeId: string,
offset = [0, 0]
): number[] {
const shape = this.getShape(data, shapeId)
return shape.parentId === data.currentPageId
? offset
: this.getParentOffset(data, shape.parentId, vec.add(offset, shape.point))
}
static getParentRotation(data: Data, shapeId: string, rotation = 0): number {
const shape = this.getShape(data, shapeId)
return shape.parentId === data.currentPageId
? rotation + shape.rotation
: this.getParentRotation(data, shape.parentId, rotation + shape.rotation)
}
static getDocumentBranch(data: Data, id: string): string[] {
const shape = this.getPage(data).shapes[id]
if (shape.type !== ShapeType.Group) return [id]
return [
id,
...shape.children.flatMap((childId) =>
this.getDocumentBranch(data, childId)
),
]
}
static getSelectedIds(data: Data): Set<string> {
return data.pageStates[data.currentPageId].selectedIds
}
static setSelectedIds(data: Data, ids: string[]): Set<string> {
data.pageStates[data.currentPageId].selectedIds = new Set(ids)
return data.pageStates[data.currentPageId].selectedIds
}
static getTopParentId(data: Data, id: string): string {
const shape = this.getPage(data).shapes[id]
return shape.parentId === data.currentPageId ||
shape.parentId === data.currentParentId
? id
: this.getTopParentId(data, shape.parentId)
}
/* ----------------- Shapes Related ----------------- */
/**
* Get a deep-cloned
* @param data
* @param fn
*/
static getSelectedBranchSnapshot<K>(
data: Data,
fn: <T extends Shape>(shape: T) => K
): ({ id: string } & K)[]
static getSelectedBranchSnapshot(data: Data): Shape[]
static getSelectedBranchSnapshot<
K,
F extends <T extends Shape>(shape: T) => K
>(data: Data, fn?: F): (Shape | K)[] {
const page = this.getPage(data)
const copies = setToArray(this.getSelectedIds(data))
.flatMap((id) =>
this.getDocumentBranch(data, id).map((id) => page.shapes[id])
)
.filter((shape) => !shape.isLocked)
.map(deepClone)
if (fn !== undefined) {
return copies.map((shape) => ({ id: shape.id, ...fn(shape) }))
}
return copies
}
/**
* Get a deep-cloned array of shapes
* @param data
*/
static getSelectedShapeSnapshot(data: Data): Shape[]
static getSelectedShapeSnapshot<K>(
data: Data,
fn: <T extends Shape>(shape: T) => K
): ({ id: string } & K)[]
static getSelectedShapeSnapshot<
K,
F extends <T extends Shape>(shape: T) => K
>(data: Data, fn?: F): (Shape | K)[] {
const copies = this.getSelectedShapes(data)
.filter((shape) => !shape.isLocked)
.map(deepClone)
if (fn !== undefined) {
return copies.map((shape) => ({ id: shape.id, ...fn(shape) }))
}
return copies
}
/**
* Get an array of all unique parentIds among a set of shapes.
* @param data
* @param shapes
*/
static getUniqueParentIds(data: Data, shapes: Shape[]): string[] {
return Array.from(new Set(shapes.map((s) => s.parentId)).values()).filter(
(id) => id !== data.currentPageId
)
}
/**
* Make an arbitrary change to shape.
* @param data
* @param ids
* @param fn
*/
static mutateShape<T extends Shape>(
data: Data,
id: string,
fn: (shapeUtils: ShapeUtility<T>, shape: T) => void,
updateParents = true
): T {
const page = this.getPage(data)
const shape = page.shapes[id] as T
fn(getShapeUtils(shape) as ShapeUtility<T>, shape)
if (updateParents) this.updateParents(data, [id])
return shape
}
/**
* Make an arbitrary change to a set of shapes.
* @param data
* @param ids
* @param fn
*/
static mutateShapes<T extends Shape>(
data: Data,
ids: string[],
fn: (shape: T, shapeUtils: ShapeUtility<T>, index: number) => T | void,
updateParents = true
): T[] {
const page = this.getPage(data)
const mutatedShapes = ids.map((id, i) => {
const shape = page.shapes[id] as T
// Define the new shape as either the (maybe new) shape returned by the
// function or the mutated shape.
page.shapes[id] =
fn(shape, getShapeUtils(shape) as ShapeUtility<T>, i) || shape
return page.shapes[id] as T
})
if (updateParents) this.updateParents(data, ids)
return mutatedShapes
}
/**
* Insert shapes into the current page.
* @param data
* @param shapes
*/
static insertShapes(data: Data, shapes: Shape[]): void {
const page = this.getPage(data)
shapes.forEach((shape) => {
page.shapes[shape.id] = shape
// Does the shape have a parent?
if (shape.parentId !== data.currentPageId) {
// The parent shape
const parent = page.shapes[shape.parentId]
// If the parent shape doesn't exist, assign the shape as a child
// of the page instead.
if (parent === undefined) {
getShapeUtils(shape).setProperty(
shape,
'childIndex',
this.getTopChildIndex(data, parent)
)
} else {
// Add the shape's id to the parent's children, then sort the
// new array just to be sure.
getShapeUtils(parent).setProperty(
parent,
'children',
[...parent.children, shape.id]
.map((id) => page.shapes[id])
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => shape.id)
)
}
}
})
// Update any new parents
this.updateParents(
data,
shapes.map((shape) => shape.id)
)
}
static getRotatedBounds(shape: Shape): Bounds {
return getShapeUtils(shape).getRotatedBounds(shape)
}
static getShapeBounds(shape: Shape): Bounds {
return getShapeUtils(shape).getBounds(shape)
}
static getSelectedBounds(data: Data): Bounds {
return getCommonBounds(
...this.getSelectedShapes(data).map((shape) =>
getShapeUtils(shape).getBounds(shape)
)
)
}
/**
* Recursively update shape parents.
* @param data
* @param changedShapeIds
*/
static updateParents(data: Data, changedShapeIds: string[]): void {
if (changedShapeIds.length === 0) return
const { shapes } = this.getPage(data)
const parentToUpdateIds = Array.from(
new Set(changedShapeIds.map((id) => shapes[id].parentId).values())
).filter((id) => id !== data.currentPageId)
for (const parentId of parentToUpdateIds) {
const parent = shapes[parentId] as GroupShape
getShapeUtils(parent).onChildrenChange(
parent,
parent.children.map((id) => shapes[id])
)
shapes[parentId] = { ...parent }
}
this.updateParents(data, parentToUpdateIds)
}
}

View file

@ -1,22 +1,9 @@
import React from 'react'
import {
Data,
Bounds,
Edge,
Corner,
Shape,
GroupShape,
ShapeType,
CodeFile,
Page,
PageState,
BezierCurveSegment,
} from 'types'
import { Bounds, Edge, Corner, BezierCurveSegment } from 'types'
import { v4 as uuid } from 'uuid'
import vec from './vec'
import _isMobile from 'ismobilejs'
import { intersectPolygonBounds } from './intersections'
import { getShapeUtils } from 'state/shape-utils'
/* ----------- Numbers and Data Structures ---------- */
@ -1789,304 +1776,3 @@ export function commandKey(): string {
// return delta
// }
/* -------------------------------------------------- */
/* State Utils */
/* -------------------------------------------------- */
export function getCameraZoom(zoom: number): number {
return clamp(zoom, 0.1, 5)
}
export function screenToWorld(point: number[], data: Data): number[] {
const camera = getCurrentCamera(data)
return vec.sub(vec.div(point, camera.zoom), camera.point)
}
export function getViewport(data: Data): Bounds {
const [minX, minY] = screenToWorld([0, 0], data)
const [maxX, maxY] = screenToWorld(
[window.innerWidth, window.innerHeight],
data
)
return {
minX,
minY,
maxX,
maxY,
height: maxX - minX,
width: maxY - minY,
}
}
export function getCurrentCamera(data: Data): {
point: number[]
zoom: number
} {
return data.pageStates[data.currentPageId].camera
}
/**
* Get a shape from the project.
* @param data
* @param shapeId
*/
export function getShape(data: Data, shapeId: string): Shape {
return data.document.pages[data.currentPageId].shapes[shapeId]
}
/**
* Get the current page.
* @param data
*/
export function getPage(data: Data): Page {
return data.document.pages[data.currentPageId]
}
/**
* Get the current page's page state.
* @param data
*/
export function getPageState(data: Data): PageState {
return data.pageStates[data.currentPageId]
}
/**
* Get the current page's code file.
* @param data
* @param fileId
*/
export function getCurrentCode(data: Data, fileId: string): CodeFile {
return data.document.code[fileId]
}
/**
* Get the current page's shapes as an array.
* @param data
*/
export function getShapes(data: Data): Shape[] {
const page = getPage(data)
return Object.values(page.shapes)
}
/**
* Get the current selected shapes as an array.
* @param data
*/
export function getSelectedShapes(data: Data): Shape[] {
const page = getPage(data)
const ids = setToArray(getSelectedIds(data))
return ids.map((id) => page.shapes[id])
}
/**
* Get a shape's parent.
* @param data
* @param id
*/
export function getParent(data: Data, id: string): Shape | Page {
const page = getPage(data)
const shape = page.shapes[id]
return page.shapes[shape.parentId] || data.document.pages[shape.parentId]
}
/**
* Get a shape's children.
* @param data
* @param id
*/
export function getChildren(data: Data, id: string): Shape[] {
const page = getPage(data)
return Object.values(page.shapes)
.filter(({ parentId }) => parentId === id)
.sort((a, b) => a.childIndex - b.childIndex)
}
/**
* Get a shape's siblings.
* @param data
* @param id
*/
export function getSiblings(data: Data, id: string): Shape[] {
const page = getPage(data)
const shape = page.shapes[id]
return Object.values(page.shapes)
.filter(({ parentId }) => parentId === shape.parentId)
.sort((a, b) => a.childIndex - b.childIndex)
}
/**
* Get the next child index above a shape.
* @param data
* @param id
*/
export function getChildIndexAbove(data: Data, id: string): number {
const page = getPage(data)
const shape = page.shapes[id]
const siblings = Object.values(page.shapes)
.filter(({ parentId }) => parentId === shape.parentId)
.sort((a, b) => a.childIndex - b.childIndex)
const index = siblings.indexOf(shape)
const nextSibling = siblings[index + 1]
if (!nextSibling) {
return shape.childIndex + 1
}
let nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
if (nextIndex === nextSibling.childIndex) {
forceIntegerChildIndices(siblings)
nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
}
return nextIndex
}
/**
* Get the next child index below a shape.
* @param data
* @param id
* @param pageId
*/
export function getChildIndexBelow(data: Data, id: string): number {
const page = getPage(data)
const shape = page.shapes[id]
const siblings = Object.values(page.shapes)
.filter(({ parentId }) => parentId === shape.parentId)
.sort((a, b) => a.childIndex - b.childIndex)
const index = siblings.indexOf(shape)
const prevSibling = siblings[index - 1]
if (!prevSibling) {
return shape.childIndex / 2
}
let nextIndex = (shape.childIndex + prevSibling.childIndex) / 2
if (nextIndex === prevSibling.childIndex) {
forceIntegerChildIndices(siblings)
nextIndex = (shape.childIndex + prevSibling.childIndex) / 2
}
return (shape.childIndex + prevSibling.childIndex) / 2
}
export function forceIntegerChildIndices(shapes: Shape[]): void {
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i]
getShapeUtils(shape).setProperty(shape, 'childIndex', i + 1)
}
}
/**
* Update the zoom CSS variable.
* @param zoom ;
*/
export function setZoomCSS(zoom: number): void {
document.documentElement.style.setProperty('--camera-zoom', zoom.toString())
}
/* --------------------- Groups --------------------- */
export function getParentOffset(
data: Data,
shapeId: string,
offset = [0, 0]
): number[] {
const shape = getShape(data, shapeId)
return shape.parentId === data.currentPageId
? offset
: getParentOffset(data, shape.parentId, vec.add(offset, shape.point))
}
export function getParentRotation(
data: Data,
shapeId: string,
rotation = 0
): number {
const shape = getShape(data, shapeId)
return shape.parentId === data.currentPageId
? rotation + shape.rotation
: getParentRotation(data, shape.parentId, rotation + shape.rotation)
}
export function getDocumentBranch(data: Data, id: string): string[] {
const shape = getPage(data).shapes[id]
if (shape.type !== ShapeType.Group) return [id]
return [
id,
...shape.children.flatMap((childId) => getDocumentBranch(data, childId)),
]
}
export function getSelectedIds(data: Data): Set<string> {
return data.pageStates[data.currentPageId].selectedIds
}
export function setSelectedIds(data: Data, ids: string[]): Set<string> {
data.pageStates[data.currentPageId].selectedIds = new Set(ids)
return data.pageStates[data.currentPageId].selectedIds
}
export function getTopParentId(data: Data, id: string): string {
const shape = getPage(data).shapes[id]
return shape.parentId === data.currentPageId ||
shape.parentId === data.currentParentId
? id
: getTopParentId(data, shape.parentId)
}
/* ----------------- Shapes Related ----------------- */
export function getRotatedBounds(shape: Shape): Bounds {
return getShapeUtils(shape).getRotatedBounds(shape)
}
export function getShapeBounds(shape: Shape): Bounds {
return getShapeUtils(shape).getBounds(shape)
}
export function getSelectedBounds(data: Data): Bounds {
return getCommonBounds(
...getSelectedShapes(data).map((shape) =>
getShapeUtils(shape).getBounds(shape)
)
)
}
export function updateParents(data: Data, changedShapeIds: string[]): void {
if (changedShapeIds.length === 0) return
const { shapes } = getPage(data)
const parentToUpdateIds = Array.from(
new Set(changedShapeIds.map((id) => shapes[id].parentId).values())
).filter((id) => id !== data.currentPageId)
for (const parentId of parentToUpdateIds) {
const parent = shapes[parentId] as GroupShape
getShapeUtils(parent).onChildrenChange(
parent,
parent.children.map((id) => shapes[id])
)
shapes[parentId] = { ...parent }
}
updateParents(data, parentToUpdateIds)
}

View file

@ -2243,6 +2243,13 @@ abab@^2.0.3, abab@^2.0.5:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
acorn-globals@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
@ -3831,6 +3838,11 @@ etag@1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
events@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@ -4546,6 +4558,11 @@ is-arrayish@^0.2.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
is-base64@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-base64/-/is-base64-1.1.0.tgz#8ce1d719895030a457c59a7dcaf39b66d99d56b4"
integrity sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==
is-bigint@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
@ -5971,7 +5988,7 @@ next@^11.0.1:
vm-browserify "1.1.2"
watchpack "2.1.1"
node-fetch@2.6.1, node-fetch@^2.6.0:
node-fetch@2.6.1, node-fetch@^2.6.0, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@ -6651,6 +6668,24 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
pusher-js@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-7.0.3.tgz#f81c78cdf2ad32f546caa7532ec7f9081ef00b8d"
integrity sha512-HIfCvt00CAqgO4W0BrdpPsDcAwy51rB6DN0VMC+JeVRRbo8mn3XTeUeIFjmmlRLZLX8rPhUtLRo7vPag6b8GCw==
dependencies:
tweetnacl "^1.0.3"
pusher@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pusher/-/pusher-5.0.0.tgz#3dc39ff527637a4b4597652357b0ec562514c8e6"
integrity sha512-YaSZHkukytHR9+lklJp4yefwfR4685kfS6pqrSDUxPj45Ga29lIgyN7Jcnsz+bN5WKwXaf2+4c/x/j3pzWIAkw==
dependencies:
abort-controller "^3.0.0"
is-base64 "^1.1.0"
node-fetch "^2.6.1"
tweetnacl "^1.0.0"
tweetnacl-util "^0.15.0"
querystring-es3@0.2.1, querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@ -7815,6 +7850,16 @@ tty-browserify@0.0.1:
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==
tweetnacl-util@^0.15.0:
version "0.15.1"
resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b"
integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==
tweetnacl@^1.0.0, tweetnacl@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"