Updates cursor logic
This commit is contained in:
parent
467424d93e
commit
49baa47a0e
17 changed files with 377 additions and 100 deletions
|
@ -1,19 +1,18 @@
|
||||||
import styled from 'styles'
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary'
|
import { ErrorBoundary } from 'react-error-boundary'
|
||||||
import state, { useSelector } from 'state'
|
|
||||||
import React, { useRef } from 'react'
|
|
||||||
import useZoomEvents from 'hooks/useZoomEvents'
|
|
||||||
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 Bounds from './bounds/bounding-box'
|
||||||
import BoundsBg from './bounds/bounds-bg'
|
import BoundsBg from './bounds/bounds-bg'
|
||||||
import Handles from './bounds/handles'
|
import Brush from './brush'
|
||||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
|
||||||
import ContextMenu from './context-menu/context-menu'
|
import ContextMenu from './context-menu/context-menu'
|
||||||
import { deepCompareArrays } from 'utils'
|
import Coop from './coop/coop'
|
||||||
|
import Defs from './defs'
|
||||||
|
import Handles from './bounds/handles'
|
||||||
|
import Page from './page'
|
||||||
|
import React, { useRef } from 'react'
|
||||||
|
import state, { useSelector } from 'state'
|
||||||
|
import styled from 'styles'
|
||||||
|
import useCamera from 'hooks/useCamera'
|
||||||
|
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||||
|
import useZoomEvents from 'hooks/useZoomEvents'
|
||||||
|
|
||||||
function resetError() {
|
function resetError() {
|
||||||
null
|
null
|
||||||
|
@ -40,10 +39,10 @@ export default function Canvas(): JSX.Element {
|
||||||
<g ref={rGroup} id="shapes">
|
<g ref={rGroup} id="shapes">
|
||||||
<BoundsBg />
|
<BoundsBg />
|
||||||
<Page />
|
<Page />
|
||||||
|
<Coop />
|
||||||
<Bounds />
|
<Bounds />
|
||||||
<Handles />
|
<Handles />
|
||||||
<Brush />
|
<Brush />
|
||||||
<Peers />
|
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
@ -52,32 +51,6 @@ 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', {
|
const MainSVG = styled('svg', {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
|
27
components/canvas/coop/coop.tsx
Normal file
27
components/canvas/coop/coop.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import Cursor from './cursor'
|
||||||
|
import { useCoopSelector } from 'state/coop/coop-state'
|
||||||
|
|
||||||
|
export default function Presence(): JSX.Element {
|
||||||
|
const others = useCoopSelector((s) => s.data.others)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.values(others).map(({ connectionId, presence }) => {
|
||||||
|
if (presence == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Cursor
|
||||||
|
key={`cursor-${connectionId}`}
|
||||||
|
color={'red'}
|
||||||
|
duration={presence.duration}
|
||||||
|
times={presence.times}
|
||||||
|
bufferedXs={presence.bufferedXs}
|
||||||
|
bufferedYs={presence.bufferedYs}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,25 +1,45 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
// const transition = {
|
||||||
|
// type: 'spring',
|
||||||
|
// mass: 2,
|
||||||
|
// damping: 20,
|
||||||
|
// }
|
||||||
|
|
||||||
export default function Cursor({
|
export default function Cursor({
|
||||||
color = 'dodgerblue',
|
color = 'dodgerblue',
|
||||||
point = [0, 0],
|
duration = 0,
|
||||||
|
bufferedXs = [],
|
||||||
|
bufferedYs = [],
|
||||||
|
times = [],
|
||||||
}: {
|
}: {
|
||||||
color?: string
|
color: string
|
||||||
point: number[]
|
duration: number
|
||||||
|
bufferedXs: number[]
|
||||||
|
bufferedYs: number[]
|
||||||
|
times: number[]
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const transform = `translate(${point[0] - 12} ${point[1] - 10})`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledCursor
|
<StyledCursor
|
||||||
color={color}
|
color={color}
|
||||||
transform={transform}
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
x: bufferedXs,
|
||||||
|
y: bufferedYs,
|
||||||
|
transition: {
|
||||||
|
type: 'tween',
|
||||||
|
ease: 'linear',
|
||||||
|
duration,
|
||||||
|
times,
|
||||||
|
},
|
||||||
|
}}
|
||||||
width="35px"
|
width="35px"
|
||||||
height="35px"
|
height="35px"
|
||||||
viewBox="0 0 35 35"
|
viewBox="0 0 35 35"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
opacity="0"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
>
|
>
|
||||||
|
@ -43,7 +63,7 @@ export default function Cursor({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledCursor = styled('g', {
|
const StyledCursor = styled(motion.g, {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
top: 0,
|
top: 0,
|
|
@ -1,11 +1,14 @@
|
||||||
import { useStateDesigner } from '@state-designer/react'
|
import { useStateDesigner } from '@state-designer/react'
|
||||||
import state from 'state'
|
import state from 'state'
|
||||||
|
import { useCoopSelector } from 'state/coop/coop-state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
|
|
||||||
const size: any = { '@sm': 'small' }
|
const size: any = { '@sm': 'small' }
|
||||||
|
|
||||||
export default function StatusBar(): JSX.Element {
|
export default function StatusBar(): JSX.Element {
|
||||||
const local = useStateDesigner(state)
|
const local = useStateDesigner(state)
|
||||||
|
const status = useCoopSelector((s) => s.data.status)
|
||||||
|
const others = useCoopSelector((s) => s.data.others)
|
||||||
|
|
||||||
const active = local.active.slice(1).map((s) => {
|
const active = local.active.slice(1).map((s) => {
|
||||||
const states = s.split('.')
|
const states = s.split('.')
|
||||||
|
@ -17,7 +20,8 @@ export default function StatusBar(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<StatusBarContainer size={size}>
|
<StatusBarContainer size={size}>
|
||||||
<Section>
|
<Section>
|
||||||
{active.join(' | ')} | {log} | {local.data.room?.status}
|
{active.join(' | ')} | {log} | {status} (
|
||||||
|
{Object.values(others).length || 0})
|
||||||
</Section>
|
</Section>
|
||||||
</StatusBarContainer>
|
</StatusBarContainer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,22 +31,12 @@ export default function useCanvasEvents(
|
||||||
|
|
||||||
if (state.isIn('draw.editing')) {
|
if (state.isIn('draw.editing')) {
|
||||||
fastDrawUpdate(info)
|
fastDrawUpdate(info)
|
||||||
return
|
} else if (state.isIn('brushSelecting')) {
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isIn('brushSelecting')) {
|
|
||||||
fastBrushSelect(info.point)
|
fastBrushSelect(info.point)
|
||||||
return
|
} else if (state.isIn('translatingSelection')) {
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isIn('translatingSelection')) {
|
|
||||||
fastTranslate(info)
|
fastTranslate(info)
|
||||||
return
|
} else if (state.isIn('transformingSelection')) {
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isIn('transformingSelection')) {
|
|
||||||
fastTransform(info)
|
fastTransform(info)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.send('MOVED_POINTER', info)
|
state.send('MOVED_POINTER', info)
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@liveblocks/client": "^0.8.1",
|
||||||
|
"@liveblocks/node": "^0.3.0",
|
||||||
|
"@liveblocks/react": "^0.8.0",
|
||||||
"@monaco-editor/react": "^4.2.1",
|
"@monaco-editor/react": "^4.2.1",
|
||||||
"@radix-ui/react-checkbox": "^0.0.16",
|
"@radix-ui/react-checkbox": "^0.0.16",
|
||||||
"@radix-ui/react-context-menu": "^0.0.22",
|
"@radix-ui/react-context-menu": "^0.0.22",
|
||||||
|
|
48
pages/api/auth-liveblocks.ts
Normal file
48
pages/api/auth-liveblocks.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { authorize } from '@liveblocks/node'
|
||||||
|
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY
|
||||||
|
|
||||||
|
const Auth: NextApiHandler = async (
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) => {
|
||||||
|
if (!API_KEY) {
|
||||||
|
return res.status(403).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = req.body.room
|
||||||
|
|
||||||
|
if (room === 'example-live-cursors-avatars') {
|
||||||
|
const response = await authorize({
|
||||||
|
room,
|
||||||
|
secret: API_KEY,
|
||||||
|
userInfo: {
|
||||||
|
name: NAMES[Math.floor(Math.random() * NAMES.length)],
|
||||||
|
picture: `/assets/avatars/${Math.floor(Math.random() * 10)}.png`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(response.status).end(response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authorize({
|
||||||
|
room,
|
||||||
|
secret: API_KEY,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(response.status).end(response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Auth
|
||||||
|
|
||||||
|
const NAMES = [
|
||||||
|
'Charlie Layne',
|
||||||
|
'Mislav Abha',
|
||||||
|
'Tatum Paolo',
|
||||||
|
'Anjali Wanda',
|
||||||
|
'Jody Hekla',
|
||||||
|
'Emil Joyce',
|
||||||
|
'Jory Quispe',
|
||||||
|
'Quinn Elton',
|
||||||
|
]
|
0
pages/api/nodes.md
Normal file
0
pages/api/nodes.md
Normal file
|
@ -1,10 +1,18 @@
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { GetServerSideProps } from 'next'
|
import { GetServerSideProps } from 'next'
|
||||||
import { getSession } from 'next-auth/client'
|
import { getSession } from 'next-auth/client'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import coopState from 'state/coop/coop-state'
|
||||||
|
|
||||||
const Editor = dynamic(() => import('components/editor'), { ssr: false })
|
const Editor = dynamic(() => import('components/editor'), { ssr: false })
|
||||||
|
|
||||||
export default function Room({ id }: { id: string }): JSX.Element {
|
export default function Room({ id }: { id: string }): JSX.Element {
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
coopState.send('LEFT_ROOM')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <Editor roomId={id} />
|
return <Editor roomId={id} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,4 +32,4 @@
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait"
|
"orientation": "portrait"
|
||||||
}
|
}
|
||||||
|
|
131
state/coop/client-liveblocks.ts
Normal file
131
state/coop/client-liveblocks.ts
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
/* eslint-disable prefer-const */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { Client, Room, createClient } from '@liveblocks/client'
|
||||||
|
import coopState from './coop-state'
|
||||||
|
import { CoopPresence } from 'types'
|
||||||
|
import {
|
||||||
|
ConnectionCallback,
|
||||||
|
MyPresenceCallback,
|
||||||
|
OthersEventCallback,
|
||||||
|
} from '@liveblocks/client/lib/cjs/types'
|
||||||
|
import { uniqueId } from 'utils'
|
||||||
|
|
||||||
|
class CoopClient {
|
||||||
|
id = uniqueId()
|
||||||
|
roomId: string
|
||||||
|
lastCursorEventTime = 0
|
||||||
|
client: Client
|
||||||
|
room: Room
|
||||||
|
bufferedXs: number[] = []
|
||||||
|
bufferedYs: number[] = []
|
||||||
|
bufferedTs: number[] = []
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.client = createClient({
|
||||||
|
authEndpoint: '/api/auth-liveblocks',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleConnectionEvent: ConnectionCallback = (status) => {
|
||||||
|
coopState.send('CHANGED_CONNECTION_STATUS', { status })
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMyPresenceEvent: MyPresenceCallback<CoopPresence> = () => {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOthersEvent: OthersEventCallback<CoopPresence> = (_, event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'enter': {
|
||||||
|
coopState.send('OTHER_USER_ENTERED', event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'leave': {
|
||||||
|
coopState.send('OTHER_USER_LEFT', event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
coopState.send('OTHER_USER_UPDATED', event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'reset': {
|
||||||
|
coopState.send('RESET_OTHER_USERS', event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(roomId: string) {
|
||||||
|
if (this.roomId) {
|
||||||
|
this.client.leave(this.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.roomId = roomId
|
||||||
|
|
||||||
|
this.room = this.client.enter(roomId, { cursor: null })
|
||||||
|
this.room.subscribe('connection', this.handleConnectionEvent)
|
||||||
|
this.room.subscribe('my-presence', this.handleMyPresenceEvent)
|
||||||
|
this.room.subscribe('others', this.handleOthersEvent)
|
||||||
|
|
||||||
|
coopState.send('JOINED_ROOM', { others: this.room.getOthers().toArray() })
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.room.unsubscribe('connection', this.handleConnectionEvent)
|
||||||
|
this.room.unsubscribe('my-presence', this.handleMyPresenceEvent)
|
||||||
|
this.room.unsubscribe('others', this.handleOthersEvent)
|
||||||
|
|
||||||
|
this.client.leave(this.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnect() {
|
||||||
|
this.connect(this.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
moveCursor(pageId: string, point: number[]) {
|
||||||
|
if (!this.room) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
let elapsed = now - this.lastCursorEventTime
|
||||||
|
|
||||||
|
if (elapsed > 200) {
|
||||||
|
// The animation's total duration (in seconds)
|
||||||
|
const duration = this.bufferedTs[this.bufferedTs.length - 1]
|
||||||
|
|
||||||
|
// Normalized times (0 - 1)
|
||||||
|
const times = this.bufferedTs.map((t) => t / duration)
|
||||||
|
|
||||||
|
// Make sure the array includes both a 0 and a 1
|
||||||
|
if (times.length === 1) {
|
||||||
|
this.bufferedXs.unshift(this.bufferedXs[0])
|
||||||
|
this.bufferedYs.unshift(this.bufferedYs[0])
|
||||||
|
times.unshift(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the event to the service
|
||||||
|
this.room.updatePresence<CoopPresence>({
|
||||||
|
bufferedXs: this.bufferedXs,
|
||||||
|
bufferedYs: this.bufferedYs,
|
||||||
|
times,
|
||||||
|
duration,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset data for next update
|
||||||
|
this.lastCursorEventTime = now
|
||||||
|
this.bufferedXs = []
|
||||||
|
this.bufferedYs = []
|
||||||
|
this.bufferedTs = []
|
||||||
|
elapsed = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bufferedXs.push(point[0])
|
||||||
|
this.bufferedYs.push(point[1])
|
||||||
|
this.bufferedTs.push(elapsed / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCursor() {
|
||||||
|
this.room.updatePresence({ cursor: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CoopClient()
|
64
state/coop/coop-state.ts
Normal file
64
state/coop/coop-state.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { createSelectorHook, createState } from '@state-designer/react'
|
||||||
|
import { CoopPresence } from 'types'
|
||||||
|
import { User } from '@liveblocks/client'
|
||||||
|
import client from 'state/coop/client-liveblocks'
|
||||||
|
|
||||||
|
type ConnectionState =
|
||||||
|
| 'closed'
|
||||||
|
| 'authenticating'
|
||||||
|
| 'unavailable'
|
||||||
|
| 'failed'
|
||||||
|
| 'open'
|
||||||
|
| 'connecting'
|
||||||
|
|
||||||
|
const coopState = createState({
|
||||||
|
data: {
|
||||||
|
status: 'closed' as ConnectionState,
|
||||||
|
others: {} as Record<string, User<CoopPresence>>,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
JOINED_ROOM: 'setOthers',
|
||||||
|
LEFT_ROOM: 'disconnectFromRoom',
|
||||||
|
CHANGED_CONNECTION_STATUS: 'setStatus',
|
||||||
|
OTHER_USER_ENTERED: 'addOtherUser',
|
||||||
|
OTHER_USER_LEFT: 'removeOtherUser',
|
||||||
|
OTHER_USER_UPDATED: 'updateOtherUser',
|
||||||
|
RESET_OTHER_USERS: 'resetOtherUsers',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
connectToRoom(data, payload: { id: string }) {
|
||||||
|
client.connect(payload.id)
|
||||||
|
},
|
||||||
|
disconnectFromRoom() {
|
||||||
|
client.disconnect()
|
||||||
|
},
|
||||||
|
setStatus(data, payload: { status: ConnectionState }) {
|
||||||
|
data.status = payload.status
|
||||||
|
},
|
||||||
|
setOthers(data, payload: { others: User<CoopPresence>[] }) {
|
||||||
|
const { others } = payload
|
||||||
|
data.others = Object.fromEntries(
|
||||||
|
others.map((user) => [user.connectionId, user])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
addOtherUser(data, payload: { user: User<CoopPresence> }) {
|
||||||
|
const { user } = payload
|
||||||
|
data.others[user.connectionId] = user
|
||||||
|
},
|
||||||
|
removeOtherUser(data, payload: { user: User<CoopPresence> }) {
|
||||||
|
const { user } = payload
|
||||||
|
delete data.others[user.connectionId]
|
||||||
|
},
|
||||||
|
updateOtherUser(data, payload: { user: User<CoopPresence>; changes: any }) {
|
||||||
|
const { user } = payload
|
||||||
|
data.others[user.connectionId] = user
|
||||||
|
},
|
||||||
|
resetOtherUsers(data) {
|
||||||
|
data.others = {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useCoopSelector = createSelectorHook(coopState)
|
||||||
|
|
||||||
|
export default coopState
|
|
@ -3,6 +3,7 @@ import { deepClone, setToArray } from 'utils'
|
||||||
import tld from 'utils/tld'
|
import tld from 'utils/tld'
|
||||||
import { freeze } from 'immer'
|
import { freeze } from 'immer'
|
||||||
import session from './session'
|
import session from './session'
|
||||||
|
import coopClient from 'state/coop/client-liveblocks'
|
||||||
import state from './state'
|
import state from './state'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import * as Session from './sessions'
|
import * as Session from './sessions'
|
||||||
|
@ -17,6 +18,8 @@ import * as Session from './sessions'
|
||||||
export function fastDrawUpdate(info: PointerInfo): void {
|
export function fastDrawUpdate(info: PointerInfo): void {
|
||||||
const data = { ...state.data }
|
const data = { ...state.data }
|
||||||
|
|
||||||
|
coopClient.moveCursor(data.currentPageId, info.point)
|
||||||
|
|
||||||
session.update<Session.DrawSession>(
|
session.update<Session.DrawSession>(
|
||||||
data,
|
data,
|
||||||
tld.screenToWorld(info.point, data),
|
tld.screenToWorld(info.point, data),
|
||||||
|
|
|
@ -7,7 +7,7 @@ import history from './history'
|
||||||
import storage from './storage'
|
import storage from './storage'
|
||||||
import clipboard from './clipboard'
|
import clipboard from './clipboard'
|
||||||
import * as Sessions from './sessions'
|
import * as Sessions from './sessions'
|
||||||
import coopClient from './coop/client-pusher'
|
import coopClient from './coop/client-liveblocks'
|
||||||
import commands from './commands'
|
import commands from './commands'
|
||||||
import {
|
import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
|
@ -163,18 +163,17 @@ const state = createState({
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
// Network-Related
|
// Network-Related
|
||||||
// RT_LOADED_ROOM: [
|
RT_LOADED_ROOM: [
|
||||||
// 'clearRoom',
|
'clearRoom',
|
||||||
// { if: 'hasRoom', do: ['clearDocument', 'connectToRoom'] },
|
{ if: 'hasRoom', do: ['clearDocument', 'connectToRoom'] },
|
||||||
// ],
|
],
|
||||||
|
RT_CHANGED_STATUS: 'setRtStatus',
|
||||||
|
MOVED_POINTER: { secretlyDo: 'sendRtCursorMove' },
|
||||||
// RT_UNLOADED_ROOM: ['clearRoom', 'clearDocument'],
|
// RT_UNLOADED_ROOM: ['clearRoom', 'clearDocument'],
|
||||||
// RT_DISCONNECTED_ROOM: ['clearRoom', 'clearDocument'],
|
// RT_DISCONNECTED_ROOM: ['clearRoom', 'clearDocument'],
|
||||||
// RT_CREATED_SHAPE: 'addRtShape',
|
// RT_CREATED_SHAPE: 'addRtShape',
|
||||||
// RT_CHANGED_STATUS: 'setRtStatus',
|
|
||||||
// RT_DELETED_SHAPE: 'deleteRtShape',
|
// RT_DELETED_SHAPE: 'deleteRtShape',
|
||||||
// RT_EDITED_SHAPE: 'editRtShape',
|
// RT_EDITED_SHAPE: 'editRtShape',
|
||||||
// RT_MOVED_CURSOR: 'moveRtCursor',
|
|
||||||
// MOVED_POINTER: { secretlyDo: 'sendRtCursorMove' },
|
|
||||||
// Client
|
// Client
|
||||||
RESIZED_WINDOW: 'resetPageState',
|
RESIZED_WINDOW: 'resetPageState',
|
||||||
RESET_PAGE: 'resetPage',
|
RESET_PAGE: 'resetPage',
|
||||||
|
@ -554,7 +553,7 @@ const state = createState({
|
||||||
onEnter: 'startTransformSession',
|
onEnter: 'startTransformSession',
|
||||||
onExit: 'completeSession',
|
onExit: 'completeSession',
|
||||||
on: {
|
on: {
|
||||||
// MOVED_POINTER: 'updateTransformSession', using hacks.fastTransform
|
// MOVED_POINTER: 'updateTransformSession', (see hacks)
|
||||||
PANNED_CAMERA: 'updateTransformSession',
|
PANNED_CAMERA: 'updateTransformSession',
|
||||||
PRESSED_SHIFT_KEY: 'keyUpdateTransformSession',
|
PRESSED_SHIFT_KEY: 'keyUpdateTransformSession',
|
||||||
RELEASED_SHIFT_KEY: 'keyUpdateTransformSession',
|
RELEASED_SHIFT_KEY: 'keyUpdateTransformSession',
|
||||||
|
@ -567,7 +566,7 @@ const state = createState({
|
||||||
onExit: 'completeSession',
|
onExit: 'completeSession',
|
||||||
on: {
|
on: {
|
||||||
STARTED_PINCHING: { to: 'pinching' },
|
STARTED_PINCHING: { to: 'pinching' },
|
||||||
MOVED_POINTER: 'updateTranslateSession',
|
// MOVED_POINTER: 'updateTranslateSession', (see hacks)
|
||||||
PANNED_CAMERA: 'updateTranslateSession',
|
PANNED_CAMERA: 'updateTranslateSession',
|
||||||
PRESSED_SHIFT_KEY: 'keyUpdateTranslateSession',
|
PRESSED_SHIFT_KEY: 'keyUpdateTranslateSession',
|
||||||
RELEASED_SHIFT_KEY: 'keyUpdateTranslateSession',
|
RELEASED_SHIFT_KEY: 'keyUpdateTranslateSession',
|
||||||
|
@ -604,7 +603,7 @@ const state = createState({
|
||||||
'startBrushSession',
|
'startBrushSession',
|
||||||
],
|
],
|
||||||
on: {
|
on: {
|
||||||
// MOVED_POINTER: 'updateBrushSession', using hacks.fastBrushSelect
|
// MOVED_POINTER: 'updateBrushSession', (see hacks)
|
||||||
PANNED_CAMERA: 'updateBrushSession',
|
PANNED_CAMERA: 'updateBrushSession',
|
||||||
STOPPED_POINTING: { to: 'selecting' },
|
STOPPED_POINTING: { to: 'selecting' },
|
||||||
STARTED_PINCHING: { to: 'pinching' },
|
STARTED_PINCHING: { to: 'pinching' },
|
||||||
|
@ -640,7 +639,7 @@ const state = createState({
|
||||||
},
|
},
|
||||||
pinching: {
|
pinching: {
|
||||||
on: {
|
on: {
|
||||||
// PINCHED: { do: 'pinchCamera' }, using hacks.fastPinchCamera
|
// PINCHED: { do: 'pinchCamera' }, (see hacks)
|
||||||
},
|
},
|
||||||
initial: 'selectPinching',
|
initial: 'selectPinching',
|
||||||
onExit: { secretlyDo: 'updateZoomCSS' },
|
onExit: { secretlyDo: 'updateZoomCSS' },
|
||||||
|
@ -701,7 +700,7 @@ const state = createState({
|
||||||
},
|
},
|
||||||
PRESSED_SHIFT: 'keyUpdateDrawSession',
|
PRESSED_SHIFT: 'keyUpdateDrawSession',
|
||||||
RELEASED_SHIFT: 'keyUpdateDrawSession',
|
RELEASED_SHIFT: 'keyUpdateDrawSession',
|
||||||
// MOVED_POINTER: 'updateDrawSession',
|
// MOVED_POINTER: 'updateDrawSession', (see hacks)
|
||||||
PANNED_CAMERA: 'updateDrawSession',
|
PANNED_CAMERA: 'updateDrawSession',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1164,23 +1163,6 @@ const state = createState({
|
||||||
const point = tld.screenToWorld(payload.point, data)
|
const point = tld.screenToWorld(payload.point, data)
|
||||||
coopClient.moveCursor(data.currentPageId, point)
|
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) {
|
clearRoom(data) {
|
||||||
data.room = undefined
|
data.room = undefined
|
||||||
},
|
},
|
||||||
|
|
11
types.ts
11
types.ts
|
@ -17,7 +17,7 @@ export interface Data {
|
||||||
room?: {
|
room?: {
|
||||||
id: string
|
id: string
|
||||||
status: string
|
status: string
|
||||||
peers: Record<string, Peer>
|
peers: Record<string, CoopPresence>
|
||||||
}
|
}
|
||||||
currentStyle: ShapeStyles
|
currentStyle: ShapeStyles
|
||||||
activeTool: ShapeType | 'select'
|
activeTool: ShapeType | 'select'
|
||||||
|
@ -38,11 +38,12 @@ export interface Data {
|
||||||
/* Document */
|
/* Document */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
export interface Peer {
|
export type CoopPresence = {
|
||||||
id: string
|
id: string
|
||||||
cursor: {
|
bufferedXs: number[]
|
||||||
point: number[]
|
bufferedYs: number[]
|
||||||
}
|
times: number[]
|
||||||
|
duration: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLDocument {
|
export interface TLDocument {
|
||||||
|
|
|
@ -142,6 +142,7 @@ export default class ProjectUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next child index above a shape.
|
* Get the next child index above a shape.
|
||||||
|
* TODO: Make work for grouped shapes, make faster.
|
||||||
* @param data
|
* @param data
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
|
@ -158,9 +159,7 @@ export default class ProjectUtils {
|
||||||
|
|
||||||
const nextSibling = siblings[index + 1]
|
const nextSibling = siblings[index + 1]
|
||||||
|
|
||||||
if (!nextSibling) {
|
if (!nextSibling) return shape.childIndex + 1
|
||||||
return shape.childIndex + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
|
let nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
|
||||||
|
|
||||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -1206,6 +1206,30 @@
|
||||||
"@types/yargs" "^16.0.0"
|
"@types/yargs" "^16.0.0"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
|
|
||||||
|
"@liveblocks/client@0.8.0":
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.8.0.tgz#b2cd1cc197d1ada76f4083d3a9065ee9f8fa1dc4"
|
||||||
|
integrity sha512-p7h7ZZpkyNjC/asdzjcZOzyTjINpQkgI5zrZGT7323VLXbn9ge/a+YL83N5sUosMtbjycfGLGHNN8fpSRgl7pA==
|
||||||
|
|
||||||
|
"@liveblocks/client@^0.8.1":
|
||||||
|
version "0.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.8.1.tgz#4220542c84473d71fb4032442e6a9861a5983ba3"
|
||||||
|
integrity sha512-+5LNtyOUA7RyxsK2uRupEZ6SzNhi1p9119fuWFrbrgP0dMabV40U7SVuvMnMxIsGzFqC+RoDQWEQ+iJFcuBVaQ==
|
||||||
|
|
||||||
|
"@liveblocks/node@^0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@liveblocks/node/-/node-0.3.0.tgz#f22ff0c3415502af2baf22250431852e198b12e0"
|
||||||
|
integrity sha512-3IJ6uN3QU71z6WXiDM97wW17fVVvrG9zMy4G4PY3zYzmeRfMnA+KBSBT1uPvlfgWm2D3d6/HNIXWxhwyv7bkfw==
|
||||||
|
dependencies:
|
||||||
|
node-fetch "^2.6.1"
|
||||||
|
|
||||||
|
"@liveblocks/react@^0.8.0":
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@liveblocks/react/-/react-0.8.0.tgz#e538be3e0e3bcb5fe47e2f8ad5fcd2e9c379fc5d"
|
||||||
|
integrity sha512-eCCVOz15ldmeDIT0AB08ExQrBROeAig6EBI0hY9tUe5iehADDDSAJmrkujg0K/lwPvtvzjjrzGq1qW79x2ggkg==
|
||||||
|
dependencies:
|
||||||
|
"@liveblocks/client" "0.8.0"
|
||||||
|
|
||||||
"@monaco-editor/loader@^1.1.1":
|
"@monaco-editor/loader@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.1.1.tgz#37db648c81a86946d0febd391de00df9c28a0a3d"
|
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.1.1.tgz#37db648c81a86946d0febd391de00df9c28a0a3d"
|
||||||
|
|
Loading…
Reference in a new issue