[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:
parent
84a283828d
commit
ea66362135
13 changed files with 184 additions and 53 deletions
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.0.102",
|
||||
"version": "0.0.103",
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('Draw session', () => {
|
|||
expect(tlstate.getShape('draw1')).toBe(undefined)
|
||||
|
||||
tlstate
|
||||
.create({
|
||||
.createShapes({
|
||||
id: 'draw1',
|
||||
parentId: 'page1',
|
||||
name: 'Draw',
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue