From ac593b2ac2a3e78aeecb8419da3a687cfa59de48 Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Fri, 8 Sep 2023 09:26:55 +0100 Subject: [PATCH] Fix paste transform (#1859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes a bug where pasted content would be placed incorrectly if pasted into a parent frame. Closes #1857 ### Change Type - [x] `patch` — Bug fix - [ ] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Fixes a bug affecting the position of pasted content inside frames. --- packages/editor/src/lib/editor/Editor.ts | 24 +++++--- .../src/lib/defaultExternalContentHandlers.ts | 22 ++++---- packages/tldraw/src/test/paste.test.ts | 56 ++++++++++++++++++- 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index df41d8f4a..2b35afb63 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -7939,7 +7939,10 @@ export class Editor extends EventEmitter { if (!isPageId(pasteParentId)) { // Put the shapes in the middle of the (on screen) parent const shape = this.getShape(pasteParentId)! - point = this.getShapeGeometry(shape).bounds.center + point = Matrix2d.applyToPoint( + this.getShapePageTransform(shape), + this.getShapeGeometry(shape).bounds.center + ) } else { const { viewportPageBounds } = this if (preservePosition || viewportPageBounds.includes(Box2d.From(bounds))) { @@ -7970,14 +7973,19 @@ export class Editor extends EventEmitter { } } - this.updateShapes( - rootShapes.map((s) => { - const delta = { - x: (s.x ?? 0) - (bounds.x + bounds.w / 2), - y: (s.y ?? 0) - (bounds.y + bounds.h / 2), - } + const pageCenter = Box2d.Common( + compact(rootShapes.map(({ id }) => this.getShapePageBounds(id))) + ).center - return { id: s.id, type: s.type, x: point!.x + delta.x, y: point!.y + delta.y } + const offset = Vec2d.Sub(point, pageCenter) + + this.updateShapes( + rootShapes.map(({ id }) => { + const s = this.getShape(id)! + const localRotation = this.getShapeParentTransform(id).decompose().rotation + const localDelta = Vec2d.Rot(offset, -localRotation) + + return { id: s.id, type: s.type, x: s.x + localDelta.x, y: s.y + localDelta.y } }) ) }) diff --git a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts index 2f5ddf3c6..d10c51afd 100644 --- a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts +++ b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts @@ -379,12 +379,12 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p if (!assets.length) return const currentPoint = Vec2d.From(position) - const paritals: TLShapePartial[] = [] + const partials: TLShapePartial[] = [] for (const asset of assets) { switch (asset.type) { case 'bookmark': { - paritals.push({ + partials.push({ id: createShapeId(), type: 'bookmark', x: currentPoint.x - 150, @@ -400,7 +400,7 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p break } case 'image': { - paritals.push({ + partials.push({ id: createShapeId(), type: 'image', x: currentPoint.x - asset.props.w / 2, @@ -417,7 +417,7 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p break } case 'video': { - paritals.push({ + partials.push({ id: createShapeId(), type: 'video', x: currentPoint.x - asset.props.w / 2, @@ -443,7 +443,7 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p } // Create the shapes - editor.createShapes(paritals).select(...paritals.map((p) => p.id)) + editor.createShapes(partials).select(...partials.map((p) => p.id)) // Re-position shapes so that the center of the group is at the provided point const { viewportPageBounds } = editor @@ -453,12 +453,14 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p const offset = selectionPageBounds!.center.sub(position) editor.updateShapes( - paritals.map((partial) => { + editor.selectedShapes.map((shape) => { + const localRotation = editor.getShapeParentTransform(shape).decompose().rotation + const localDelta = Vec2d.Rot(offset, -localRotation) return { - id: partial.id, - type: partial.type, - x: partial.x! - offset.x, - y: partial.y! - offset.y, + id: shape.id, + type: shape.type, + x: shape.x! - localDelta.x, + y: shape.y! - localDelta.y, } }) ) diff --git a/packages/tldraw/src/test/paste.test.ts b/packages/tldraw/src/test/paste.test.ts index e1fd9733b..1d3dfaca5 100644 --- a/packages/tldraw/src/test/paste.test.ts +++ b/packages/tldraw/src/test/paste.test.ts @@ -1,4 +1,4 @@ -import { TLFrameShape, TLGeoShape, createShapeId } from '@tldraw/editor' +import { TLFrameShape, TLGeoShape, approximately, createShapeId } from '@tldraw/editor' import { TestEditor } from './TestEditor' let editor: TestEditor @@ -433,4 +433,58 @@ describe('When pasting into frames...', () => { // it should be on the canvas, NOT a child of frame2 expect(newShape.parentId).not.toBe(ids.frame2) }) + + it('keeps things in the right place', () => { + // clear the page + editor.selectAll().deleteShapes(editor.selectedShapeIds) + // create a small box and copy it + editor.createShapes([ + { + type: 'geo', + x: 0, + y: 0, + props: { + geo: 'rectangle', + w: 10, + h: 10, + }, + }, + ]) + editor.selectAll().copy() + // now delete it + editor.deleteShapes(editor.selectedShapeIds) + + // create a big frame away from the origin, the size of the viewport + editor + .createShapes([ + { + id: ids.frame1, + type: 'frame', + x: editor.viewportScreenBounds.w, + y: editor.viewportScreenBounds.h, + props: { + w: editor.viewportScreenBounds.w, + h: editor.viewportScreenBounds.h, + }, + }, + ]) + .selectAll() + // rotate the frame for hard mode + editor.rotateSelection(45) + // center on the center of the frame + editor.setCamera({ x: -editor.viewportScreenBounds.w, y: -editor.viewportScreenBounds.h, z: 1 }) + // paste the box + editor.paste() + const boxId = editor.onlySelectedShape!.id + // it should be a child of the frame + expect(editor.onlySelectedShape?.parentId).toBe(ids.frame1) + // it should have pageBounds of 10x10 because it is not rotated relative to the viewport + expect(editor.getShapePageBounds(boxId)).toMatchObject({ w: 10, h: 10 }) + // it should be in the middle of the frame + const framePageCenter = editor.getPageCenter(editor.getShape(ids.frame1)!)! + const boxPageCenter = editor.getPageCenter(editor.getShape(boxId)!)! + + expect(approximately(framePageCenter.x, boxPageCenter.x)).toBe(true) + expect(approximately(framePageCenter.y, boxPageCenter.y)).toBe(true) + }) })