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:
parent
c1f896b042
commit
ac593b2ac2
3 changed files with 83 additions and 19 deletions
|
@ -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 }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue