[fix] copy bindings (#124)

* v0.0.103

* Copies bindings together with shapes that are bound

* Remove old shape bindings from copied shape handles
This commit is contained in:
Steve Ruiz 2021-09-24 13:47:11 +01:00 committed by GitHub
parent 84a283828d
commit ea66362135
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 53 deletions

View file

@ -1,5 +1,5 @@
{
"version": "0.0.102",
"version": "0.0.103",
"registry": "https://registry.npmjs.org/",
"publishConfig": {
"access": "public",

View file

@ -1,6 +1,6 @@
{
"name": "@tldraw/core",
"version": "0.0.102",
"version": "0.0.103",
"private": false,
"description": "A tiny little drawing app (core)",
"author": "@steveruizok",
@ -57,8 +57,8 @@
"react-dom": "^16.8 || ^17.0"
},
"dependencies": {
"@tldraw/intersect": "^0.0.102",
"@tldraw/vec": "^0.0.102",
"@tldraw/intersect": "^0.0.103",
"@tldraw/vec": "^0.0.103",
"@use-gesture/react": "^10.0.0-beta.26"
},
"gitHead": "5cb031ddc264846ec6732d7179511cddea8ef034"

View file

@ -1,6 +1,6 @@
{
"name": "@tldraw/dev",
"version": "0.0.102",
"version": "0.0.103",
"private": true,
"description": "A tiny little drawing app (dev)",
"author": "@steveruizok",
@ -19,7 +19,7 @@
],
"sideEffects": false,
"dependencies": {
"@tldraw/tldraw": "^0.0.102",
"@tldraw/tldraw": "^0.0.103",
"idb": "^6.1.2",
"react": ">=16.8",
"react-dom": "^16.8 || ^17.0",

View file

@ -1,6 +1,6 @@
{
"name": "@tldraw/intersect",
"version": "0.0.102",
"version": "0.0.103",
"private": false,
"description": "A tiny little drawing app (intersect)",
"author": "@steveruizok",
@ -48,7 +48,7 @@
"typescript": "^4.4.2"
},
"dependencies": {
"@tldraw/vec": "^0.0.102"
"@tldraw/vec": "^0.0.103"
},
"gitHead": "5cb031ddc264846ec6732d7179511cddea8ef034"
}

View file

@ -1,6 +1,6 @@
{
"name": "@tldraw/tldraw",
"version": "0.0.102",
"version": "0.0.103",
"private": false,
"description": "A tiny little drawing app (editor)",
"author": "@steveruizok",
@ -64,12 +64,12 @@
"@radix-ui/react-tooltip": "^0.0.20",
"@stitches/core": "^1.2.0",
"@stitches/react": "^1.0.0",
"@tldraw/core": "^0.0.102",
"@tldraw/intersect": "^0.0.102",
"@tldraw/vec": "^0.0.102",
"@tldraw/core": "^0.0.103",
"@tldraw/intersect": "^0.0.103",
"@tldraw/vec": "^0.0.103",
"perfect-freehand": "^1.0.12",
"react-hotkeys-hook": "^3.4.0",
"rko": "^0.5.25"
},
"gitHead": "5cb031ddc264846ec6732d7179511cddea8ef034"
}
}

View file

@ -16,6 +16,7 @@ Array [
},
"pages": Object {
"page1": Object {
"bindings": Object {},
"shapes": Object {
"rect1": Object {
"childIndex": 1,
@ -53,6 +54,7 @@ Array [
},
"pages": Object {
"page1": Object {
"bindings": Object {},
"shapes": Object {
"rect1": undefined,
},
@ -74,6 +76,7 @@ Array [
},
"pages": Object {
"page1": Object {
"bindings": Object {},
"shapes": Object {
"rect2": Object {
"childIndex": 1,
@ -113,6 +116,7 @@ Array [
},
"pages": Object {
"page1": Object {
"bindings": Object {},
"shapes": Object {
"rect2": undefined,
},

View file

@ -21,7 +21,7 @@ describe('Create command', () => {
it('does, undoes and redoes command', () => {
const shape = { ...tlstate.getShape('rect1'), id: 'rect4' }
tlstate.create(shape)
tlstate.create([shape])
expect(tlstate.getShape('rect4')).toBeTruthy()
@ -33,4 +33,6 @@ describe('Create command', () => {
expect(tlstate.getShape('rect4')).toBeTruthy()
})
it.todo('Creates bindings')
})

View file

@ -1,8 +1,12 @@
import type { Patch } from 'rko'
import { TLDR } from '~state/tldr'
import type { TLDrawShape, Data, TLDrawCommand } from '~types'
import type { TLDrawShape, Data, TLDrawCommand, TLDrawBinding } from '~types'
export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
export function create(
data: Data,
shapes: TLDrawShape[],
bindings: TLDrawBinding[] = []
): TLDrawCommand {
const { currentPageId } = data.appState
const beforeShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
@ -13,6 +17,14 @@ export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
afterShapes[shape.id] = shape
})
const beforeBindings: Record<string, Patch<TLDrawBinding> | undefined> = {}
const afterBindings: Record<string, Patch<TLDrawBinding> | undefined> = {}
bindings.forEach((binding) => {
beforeBindings[binding.id] = undefined
afterBindings[binding.id] = binding
})
return {
id: 'create',
before: {
@ -20,6 +32,7 @@ export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
pages: {
[currentPageId]: {
shapes: beforeShapes,
bindings: beforeBindings,
},
},
pageStates: {
@ -34,6 +47,7 @@ export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
pages: {
[currentPageId]: {
shapes: afterShapes,
bindings: afterBindings,
},
},
pageStates: {

View file

@ -11,7 +11,7 @@ describe('Draw session', () => {
expect(tlstate.getShape('draw1')).toBe(undefined)
tlstate
.create({
.createShapes({
id: 'draw1',
parentId: 'page1',
name: 'Draw',

View file

@ -1,13 +1,13 @@
import { TLDrawState } from './tlstate'
import { mockDocument, TLStateUtils } from '~test'
import { ColorStyle, TLDrawShapeType } from '~types'
import { ArrowShape, ColorStyle, TLDrawShapeType } from '~types'
describe('TLDrawState', () => {
const tlstate = new TLDrawState()
const tlu = new TLStateUtils(tlstate)
describe('Copy and Paste', () => {
describe('When copying and pasting...', () => {
it('copies a shape', () => {
tlstate.loadDocument(mockDocument).deselectAll().copy(['rect1'])
})
@ -45,6 +45,62 @@ describe('TLDrawState', () => {
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
})
it.todo("Pastes in to the top child index of the page's children.")
it.todo('Pastes in the correct child index order.')
})
describe('When copying and pasting a shape with bindings', () => {
it('copies two bound shapes and their binding', () => {
const tlstate = new TLDrawState()
tlstate
.createShapes(
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
)
.select('arrow1')
.startHandleSession([200, 200], 'start')
.updateHandleSession([55, 55])
.completeSession()
expect(tlstate.bindings.length).toBe(1)
tlstate.selectAll().copy().paste()
const newArrow = tlstate.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
expect(newArrow.handles.start.bindingId).not.toBe(
tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId
)
expect(tlstate.bindings.length).toBe(2)
})
it('removes bindings from copied shape handles', () => {
const tlstate = new TLDrawState()
tlstate
.createShapes(
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
)
.select('arrow1')
.startHandleSession([200, 200], 'start')
.updateHandleSession([55, 55])
.completeSession()
expect(tlstate.bindings.length).toBe(1)
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeDefined()
tlstate.select('arrow1').copy().paste()
const newArrow = tlstate.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
expect(newArrow.handles.start.bindingId).toBeUndefined()
})
})
describe('Selection', () => {

View file

@ -107,7 +107,10 @@ export class TLDrawState extends StateManager<Data> {
pointer: 0,
}
clipboard?: TLDrawShape[]
clipboard?: {
shapes: TLDrawShape[]
bindings: TLDrawBinding[]
}
session?: Session
@ -845,16 +848,32 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to copy.
*/
copy = (ids = this.selectedIds): this => {
const clones = ids
.flatMap((id) => TLDR.getDocumentBranch(this.state, id, this.currentPageId))
.map((id) => this.getShape(id, this.currentPageId))
const copyingShapeIds = ids.flatMap((id) =>
TLDR.getDocumentBranch(this.state, id, this.currentPageId)
)
if (clones.length === 0) return this
const copyingShapes = copyingShapeIds.map((id) =>
Utils.deepClone(this.getShape(id, this.currentPageId))
)
this.clipboard = clones
if (copyingShapes.length === 0) return this
const copyingBindings: TLDrawBinding[] = Object.values(this.page.bindings).filter(
(binding) =>
copyingShapeIds.includes(binding.fromId) && copyingShapeIds.includes(binding.toId)
)
this.clipboard = {
shapes: copyingShapes,
bindings: copyingBindings,
}
try {
const text = JSON.stringify({ type: 'tldr/clipboard', shapes: clones })
const text = JSON.stringify({
type: 'tldr/clipboard',
shapes: copyingShapes,
bindings: copyingBindings,
})
navigator.clipboard.writeText(text).then(
() => {
@ -879,15 +898,47 @@ export class TLDrawState extends StateManager<Data> {
* @param point
*/
paste = (point?: number[]) => {
const pasteInCurrentPage = (shapes: TLDrawShape[]) => {
const idsMap = Object.fromEntries(
shapes.map((shape: TLDrawShape) => [shape.id, Utils.uniqueId()])
)
const pasteInCurrentPage = (shapes: TLDrawShape[], bindings: TLDrawBinding[]) => {
const idsMap: Record<string, string> = {}
const shapesToPaste = shapes.map((shape: TLDrawShape) => ({
...shape,
id: idsMap[shape.id],
parentId: idsMap[shape.parentId] || this.currentPageId,
shapes.forEach((shape) => (idsMap[shape.id] = Utils.uniqueId()))
bindings.forEach((binding) => (idsMap[binding.id] = Utils.uniqueId()))
let startIndex = TLDR.getTopChildIndex(this.state, this.currentPageId)
const shapesToPaste = shapes
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => {
const parentShapeId = idsMap[shape.parentId]
const copy = {
...shape,
id: idsMap[shape.id],
parentId: parentShapeId || this.currentPageId,
}
if (!parentShapeId) {
copy.childIndex = startIndex
startIndex++
}
if (copy.handles) {
Object.values(copy.handles).forEach((handle) => {
if (handle.bindingId) {
handle.bindingId = idsMap[handle.bindingId]
}
})
}
return copy
})
const bindingsToPaste = bindings.map((binding) => ({
...binding,
id: idsMap[binding.id],
toId: idsMap[binding.toId],
fromId: idsMap[binding.fromId],
}))
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
@ -915,11 +966,15 @@ export class TLDrawState extends StateManager<Data> {
Utils.getBoundsCenter(commonBounds)
)
this.createShapes(
...shapesToPaste.map((shape) => ({
...shape,
point: Vec.round(Vec.add(shape.point, delta)),
}))
this.create(
shapesToPaste.map((shape) =>
TLDR.getShapeUtils(shape.type).create({
...shape,
point: Vec.round(Vec.add(shape.point, delta)),
parentId: shape.parentId || this.currentPageId,
})
),
bindingsToPaste
)
}
try {
@ -929,15 +984,16 @@ export class TLDrawState extends StateManager<Data> {
navigator.clipboard.readText().then((result) => {
try {
const data: { type: string; shapes: TLDrawShape[] } = JSON.parse(result)
const data: { type: string; shapes: TLDrawShape[]; bindings: TLDrawBinding[] } =
JSON.parse(result)
if (data.type !== 'tldr/clipboard') {
throw Error('The pasted string was not from the tldraw clipboard.')
}
pasteInCurrentPage(data.shapes)
pasteInCurrentPage(data.shapes, data.bindings)
} catch (e) {
console.warn(e)
console.log(e)
const shapeId = Utils.uniqueId()
@ -953,12 +1009,11 @@ export class TLDrawState extends StateManager<Data> {
this.select(shapeId)
}
})
} catch (e: any) {
console.warn(e.message)
} catch (e) {
// Navigator does not support clipboard. Note that this fallback will
// not support pasting from one document to another.
if (this.clipboard) {
pasteInCurrentPage(this.clipboard)
pasteInCurrentPage(this.clipboard.shapes, this.clipboard.bindings)
}
}
@ -1761,12 +1816,12 @@ export class TLDrawState extends StateManager<Data> {
): this => {
if (shapes.length === 0) return this
return this.create(
...shapes.map((shape) => {
return TLDR.getShapeUtils(shape.type).create({
shapes.map((shape) =>
TLDR.getShapeUtils(shape.type).create({
...shape,
parentId: shape.parentId || this.currentPageId,
})
})
)
)
}
@ -1805,9 +1860,9 @@ export class TLDrawState extends StateManager<Data> {
* @param shapes An array of shapes.
* @command
*/
create = (...shapes: TLDrawShape[]): this => {
create = (shapes: TLDrawShape[] = [], bindings: TLDrawBinding[] = []): this => {
if (shapes.length === 0) return this
return this.setState(Commands.create(this.state, shapes))
return this.setState(Commands.create(this.state, shapes, bindings))
}
/**

View file

@ -1,6 +1,6 @@
{
"name": "@tldraw/vec",
"version": "0.0.102",
"version": "0.0.103",
"private": false,
"description": "A tiny little drawing app (vec)",
"author": "@steveruizok",

View file

@ -1,6 +1,6 @@
{
"name": "www",
"version": "0.0.102",
"version": "0.0.103",
"private": true,
"description": "A tiny little drawing app (site).",
"repository": {
@ -21,7 +21,7 @@
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
"@stitches/react": "^1.0.0",
"@tldraw/tldraw": "^0.0.102",
"@tldraw/tldraw": "^0.0.103",
"next": "11.1.2",
"next-auth": "3.29.0",
"next-pwa": "^5.2.23",