diff --git a/packages/tldraw/src/state/session/sessions/translate/translate.session.spec.ts b/packages/tldraw/src/state/session/sessions/translate/translate.session.spec.ts index f3c4d614f..7fbaec96e 100644 --- a/packages/tldraw/src/state/session/sessions/translate/translate.session.spec.ts +++ b/packages/tldraw/src/state/session/sessions/translate/translate.session.spec.ts @@ -1,6 +1,6 @@ import { TLDrawState } from '~state' import { mockDocument } from '~test' -import { TLDrawShapeType, TLDrawStatus } from '~types' +import { GroupShape, TLDrawShapeType, TLDrawStatus } from '~types' describe('Translate session', () => { const tlstate = new TLDrawState() @@ -184,15 +184,98 @@ describe('Translate session', () => { // it.todo('clones a shape with a parent shape') + describe('when translating a child of a group', () => { + it('translates the shape and updates the groups size / point', () => { + tlstate + .loadDocument(mockDocument) + .select('rect1', 'rect2') + .group(['rect1', 'rect2'], 'groupA') + .select('rect1') + .startTranslateSession([10, 10]) + .updateTranslateSession([20, 20], false, false) + .completeSession() + + expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100]) + + tlstate.undo() + + expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100]) + + tlstate.redo() + + expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100]) + }) + + it('clones the shape and updates the parent', () => { + tlstate + .loadDocument(mockDocument) + .select('rect1', 'rect2') + .group(['rect1', 'rect2'], 'groupA') + .select('rect1') + .startTranslateSession([10, 10]) + .updateTranslateSession([20, 20], false, true) + .completeSession() + + const children = tlstate.getShape('groupA').children + const newShapeId = children[children.length - 1] + + expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('groupA').children.length).toBe(3) + expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100]) + expect(tlstate.getShape(newShapeId).point).toStrictEqual([20, 20]) + expect(tlstate.getShape(newShapeId).parentId).toBe('groupA') + + tlstate.undo() + + expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('groupA').children.length).toBe(2) + expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100]) + expect(tlstate.getShape(newShapeId)).toBeUndefined() + + tlstate.redo() + + expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('groupA').children.length).toBe(3) + expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100]) + expect(tlstate.getShape(newShapeId).point).toStrictEqual([20, 20]) + expect(tlstate.getShape(newShapeId).parentId).toBe('groupA') + }) + }) + describe('when translating a shape with children', () => { it('translates the shapes children', () => { tlstate .loadDocument(mockDocument) .select('rect1', 'rect2') - .group() + .group(['rect1', 'rect2'], 'groupA') .startTranslateSession([10, 10]) .updateTranslateSession([20, 20], false, false) .completeSession() + + expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect2').point).toStrictEqual([110, 110]) + + tlstate.undo() + + expect(tlstate.getShape('groupA').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0]) + expect(tlstate.getShape('rect2').point).toStrictEqual([100, 100]) + + tlstate.redo() + + expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10]) + expect(tlstate.getShape('rect2').point).toStrictEqual([110, 110]) }) it('clones the shapes and children', () => { diff --git a/packages/tldraw/src/state/session/sessions/translate/translate.session.ts b/packages/tldraw/src/state/session/sessions/translate/translate.session.ts index 33072125d..dbafaf07e 100644 --- a/packages/tldraw/src/state/session/sessions/translate/translate.session.ts +++ b/packages/tldraw/src/state/session/sessions/translate/translate.session.ts @@ -8,6 +8,7 @@ import { TLDrawCommand, TLDrawStatus, ArrowShape, + GroupShape, } from '~types' import { TLDR } from '~state/tldr' import type { Patch } from 'rko' @@ -230,7 +231,8 @@ export class TranslateSession implements Session { complete(data: Data): TLDrawCommand { const pageId = data.appState.currentPageId - const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot + const { initialShapes, initialParentChildren, bindingsToDelete, clones, clonedBindings } = + this.snapshot const beforeBindings: Patch> = {} const beforeShapes: Patch> = {} @@ -238,21 +240,47 @@ export class TranslateSession implements Session { const afterBindings: Patch> = {} const afterShapes: Patch> = {} - clones.forEach((clone) => { - beforeShapes[clone.id] = undefined - afterShapes[clone.id] = this.isCloning ? TLDR.getShape(data, clone.id, pageId) : undefined - }) + if (this.isCloning) { + // Update the clones + clones.forEach((clone) => { + beforeShapes[clone.id] = undefined - initialShapes.forEach((shape) => { - beforeShapes[shape.id] = { point: shape.point } - afterShapes[shape.id] = { point: TLDR.getShape(data, shape.id, pageId).point } - }) + afterShapes[clone.id] = TLDR.getShape(data, clone.id, pageId) - clonedBindings.forEach((binding) => { - beforeBindings[binding.id] = undefined - afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId) - }) + if (clone.parentId !== pageId) { + beforeShapes[clone.parentId] = { + ...beforeShapes[clone.parentId], + children: initialParentChildren[clone.parentId], + } + afterShapes[clone.parentId] = { + ...afterShapes[clone.parentId], + children: TLDR.getShape(data, clone.parentId, pageId).children, + } + } + }) + + // Update the cloned bindings + clonedBindings.forEach((binding) => { + beforeBindings[binding.id] = undefined + afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId) + }) + } else { + // If we aren't cloning, then update the initial shapes + initialShapes.forEach((shape) => { + beforeShapes[shape.id] = { + ...beforeShapes[shape.id], + point: shape.point, + } + + afterShapes[shape.id] = { + ...afterShapes[shape.id], + point: TLDR.getShape(data, shape.id, pageId).point, + } + }) + } + + // Update the deleted bindings and any associated shapes bindingsToDelete.forEach((binding) => { beforeBindings[binding.id] = binding @@ -270,6 +298,8 @@ export class TranslateSession implements Session { afterShapes[id] = { ...afterShapes[id], handles: {} } + // There should be before and after shapes + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion beforeShapes[id]!.handles![handle.id as keyof ArrowShape['handles']] = { bindingId: binding.id, diff --git a/packages/www/pages/index.tsx b/packages/www/pages/index.tsx index e56cb9709..6e78baf15 100644 --- a/packages/www/pages/index.tsx +++ b/packages/www/pages/index.tsx @@ -1,11 +1,19 @@ import dynamic from 'next/dynamic' import { GetServerSideProps } from 'next' import { getSession } from 'next-auth/client' +import Head from 'next/head' const Editor = dynamic(() => import('components/editor'), { ssr: false }) export default function Shhh(): JSX.Element { - return + return ( + <> + + tldraw + + + + ) } export const getServerSideProps: GetServerSideProps = async (context) => { diff --git a/packages/www/pages/r/[id].tsx b/packages/www/pages/r/[id].tsx index a4b0258a4..ea80cba38 100644 --- a/packages/www/pages/r/[id].tsx +++ b/packages/www/pages/r/[id].tsx @@ -1,5 +1,6 @@ import * as React from 'react' import type { GetServerSideProps } from 'next' +import Head from 'next/head' import { getSession } from 'next-auth/client' import dynamic from 'next/dynamic' const Editor = dynamic(() => import('components/editor'), { ssr: false }) @@ -9,7 +10,14 @@ interface RoomProps { } export default function Room({ id }: RoomProps): JSX.Element { - return + return ( + <> + + tldraw + + + + ) } export const getServerSideProps: GetServerSideProps = async (context) => { diff --git a/packages/www/pages/r/index.tsx b/packages/www/pages/r/index.tsx index 5de750195..73d12ee86 100644 --- a/packages/www/pages/r/index.tsx +++ b/packages/www/pages/r/index.tsx @@ -1,12 +1,20 @@ import * as React from 'react' import type { GetServerSideProps } from 'next' +import Head from 'next/head' interface RoomProps { id?: string } export default function RandomRoomPage({ id }: RoomProps): JSX.Element { - return
Should have routed to room: {id}
+ return ( + <> + + tldraw + +
Should have routed to room: {id}
+ + ) } export const getServerSideProps: GetServerSideProps = async (context) => { diff --git a/packages/www/pages/shhh.tsx b/packages/www/pages/shhh.tsx index 5b3304060..4a50cbb4c 100644 --- a/packages/www/pages/shhh.tsx +++ b/packages/www/pages/shhh.tsx @@ -1,6 +1,14 @@ import dynamic from 'next/dynamic' const Editor = dynamic(() => import('components/editor'), { ssr: false }) +import Head from 'next/head' export default function Shhh(): JSX.Element { - return + return ( + <> + + tldraw + + + + ) } diff --git a/packages/www/pages/sponsorware.tsx b/packages/www/pages/sponsorware.tsx index 7ac8333a9..f65b5aa2f 100644 --- a/packages/www/pages/sponsorware.tsx +++ b/packages/www/pages/sponsorware.tsx @@ -3,80 +3,86 @@ import { getSession, signin, signout, useSession } from 'next-auth/client' import { GetServerSideProps } from 'next' import Link from 'next/link' import React from 'react' +import Head from 'next/head' export default function Sponsorware(): JSX.Element { const [session, loading] = useSession() return ( - - -

tldraw (is sponsorware)

-

- Hey, thanks for visiting tldraw, a tiny little drawing app by{' '} - - steveruizok - {' '} - and friends . -

- -

This project is currently:

-
    -
  • in development
  • -
  • only available for my sponsors
  • -
-

- If you'd like to try it out,{' '} - - sponsor me on Github - {' '} - (at any level) and sign in below. -

- - {session ? ( - <> - - - Signed in as {session?.user?.name} ({session?.user?.email}), but it looks like - you're not yet a sponsor. -
- Something wrong? Try reloading the page or DM me on{' '} - - Twitter - - . -
- - ) : ( - <> - - Already a sponsor? Just sign in to visit the app. - - )} -
-
-
+ <> + + tldraw + + + +

tldraw (is sponsorware)

+

+ Hey, thanks for visiting tldraw, a tiny little drawing app by{' '} + + steveruizok + {' '} + and friends . +

+ +

This project is currently:

+
    +
  • in development
  • +
  • only available for my sponsors
  • +
+

+ If you'd like to try it out,{' '} + + sponsor me on Github + {' '} + (at any level) and sign in below. +

+ + {session ? ( + <> + + + Signed in as {session?.user?.name} ({session?.user?.email}), but it looks like + you're not yet a sponsor. +
+ Something wrong? Try reloading the page or DM me on{' '} + + Twitter + + . +
+ + ) : ( + <> + + Already a sponsor? Just sign in to visit the app. + + )} +
+
+
+ ) } diff --git a/packages/www/pages/u/[id].tsx b/packages/www/pages/u/[id].tsx index a9f08ed1f..23379a74e 100644 --- a/packages/www/pages/u/[id].tsx +++ b/packages/www/pages/u/[id].tsx @@ -1,13 +1,21 @@ import * as React from 'react' import type { GetServerSideProps } from 'next' import { getSession } from 'next-auth/client' +import Head from 'next/head' interface RoomProps { id?: string } export default function OtherUserPage({ id }: RoomProps): JSX.Element { - return
Todo, other user: {id}
+ return ( + <> + + tldraw + +
Todo, other user: {id}
+ + ) } export const getServerSideProps: GetServerSideProps = async (context) => { diff --git a/packages/www/pages/u/index.tsx b/packages/www/pages/u/index.tsx index 216a1d7d5..d623c27c2 100644 --- a/packages/www/pages/u/index.tsx +++ b/packages/www/pages/u/index.tsx @@ -3,6 +3,7 @@ import type { GetServerSideProps } from 'next' import { getSession } from 'next-auth/client' import type { Session } from 'next-auth' import { signOut } from 'next-auth/client' +import Head from 'next/head' interface UserPageProps { session: Session @@ -10,12 +11,17 @@ interface UserPageProps { export default function UserPage({ session }: UserPageProps): JSX.Element { return ( -
-
-        {JSON.stringify(session.user, null, 2)}
-      
- -
+ <> + + tldraw + +
+
+          {JSON.stringify(session.user, null, 2)}
+        
+ +
+ ) } diff --git a/packages/www/styles/globals.css b/packages/www/styles/globals.css index e5e2dcc23..450e01fe3 100644 --- a/packages/www/styles/globals.css +++ b/packages/www/styles/globals.css @@ -1,9 +1,42 @@ +@font-face { + font-family: 'Recursive'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, + U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Recursive'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, + U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Recursive Mono'; + font-style: normal; + font-weight: 420; + font-display: swap; + src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, + U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + html, body { padding: 0; margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, + Fira Sans, Droid Sans, Helvetica Neue, sans-serif; } a {