Fix paste transform (#1859)

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.
This commit is contained in:
David Sheldrick 2023-09-08 09:26:55 +01:00 committed by GitHub
parent c1f896b042
commit ac593b2ac2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 83 additions and 19 deletions

View file

@ -7939,7 +7939,10 @@ export class Editor extends EventEmitter<TLEventMap> {
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<TLEventMap> {
}
}
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 }
})
)
})

View file

@ -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,
}
})
)

View file

@ -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)
})
})