2021-09-02 20:13:54 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
2022-06-09 14:33:35 +00:00
|
|
|
import { TLPageState, Utils, TLBoundsWithCenter, TLSnapLine, TLBounds } from '@tldraw/core'
|
2021-09-12 12:21:44 +00:00
|
|
|
import { Vec } from '@tldraw/vec'
|
2021-09-01 14:39:01 +00:00
|
|
|
import {
|
2021-11-16 16:01:29 +00:00
|
|
|
TDShape,
|
|
|
|
TDBinding,
|
|
|
|
TldrawCommand,
|
|
|
|
TDStatus,
|
2021-09-01 14:39:01 +00:00
|
|
|
ArrowShape,
|
2021-11-22 14:00:24 +00:00
|
|
|
Patch,
|
2021-09-06 13:30:59 +00:00
|
|
|
GroupShape,
|
2021-10-13 13:55:31 +00:00
|
|
|
SessionType,
|
2021-10-18 13:30:42 +00:00
|
|
|
ArrowBinding,
|
2021-11-16 16:01:29 +00:00
|
|
|
TldrawPatch,
|
2021-12-25 17:06:33 +00:00
|
|
|
TDShapeType,
|
2021-09-01 14:39:01 +00:00
|
|
|
} from '~types'
|
2021-10-27 15:15:01 +00:00
|
|
|
import { SLOW_SPEED, SNAP_DISTANCE } from '~constants'
|
2021-11-06 11:16:30 +00:00
|
|
|
import { TLDR } from '~state/TLDR'
|
2021-11-16 16:01:29 +00:00
|
|
|
import { BaseSession } from '../BaseSession'
|
|
|
|
import type { TldrawApp } from '../../internal'
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-10-18 13:30:42 +00:00
|
|
|
type CloneInfo =
|
|
|
|
| {
|
|
|
|
state: 'empty'
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
state: 'ready'
|
2021-11-26 15:14:10 +00:00
|
|
|
cloneMap: Record<string, string>
|
2021-11-16 16:01:29 +00:00
|
|
|
clones: TDShape[]
|
2021-10-18 13:30:42 +00:00
|
|
|
clonedBindings: ArrowBinding[]
|
|
|
|
}
|
|
|
|
|
|
|
|
type SnapInfo =
|
|
|
|
| {
|
|
|
|
state: 'empty'
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
state: 'ready'
|
|
|
|
others: TLBoundsWithCenter[]
|
|
|
|
bounds: TLBoundsWithCenter[]
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
export class TranslateSession extends BaseSession {
|
2021-12-27 12:38:30 +00:00
|
|
|
performanceMode = undefined
|
2021-10-13 13:55:31 +00:00
|
|
|
type = SessionType.Translate
|
2021-11-16 16:01:29 +00:00
|
|
|
status = TDStatus.Translating
|
2021-08-10 16:12:55 +00:00
|
|
|
delta = [0, 0]
|
|
|
|
prev = [0, 0]
|
2021-10-18 13:30:42 +00:00
|
|
|
prevPoint = [0, 0]
|
|
|
|
speed = 1
|
|
|
|
cloneInfo: CloneInfo = {
|
|
|
|
state: 'empty',
|
|
|
|
}
|
|
|
|
snapInfo: SnapInfo = {
|
|
|
|
state: 'empty',
|
|
|
|
}
|
|
|
|
snapLines: TLSnapLine[] = []
|
2021-10-18 15:59:12 +00:00
|
|
|
isCloning = false
|
|
|
|
isCreate: boolean
|
2021-11-16 16:01:29 +00:00
|
|
|
link: 'left' | 'right' | 'center' | false
|
|
|
|
|
|
|
|
initialIds: Set<string>
|
|
|
|
hasUnlockedShapes: boolean
|
|
|
|
initialSelectedIds: string[]
|
|
|
|
initialCommonBounds: TLBounds
|
|
|
|
initialShapes: TDShape[]
|
|
|
|
initialParentChildren: Record<string, string[]>
|
|
|
|
bindingsToDelete: ArrowBinding[]
|
|
|
|
|
|
|
|
constructor(app: TldrawApp, isCreate = false, link: 'left' | 'right' | 'center' | false = false) {
|
|
|
|
super(app)
|
2021-10-15 09:33:48 +00:00
|
|
|
this.isCreate = isCreate
|
2021-11-16 16:01:29 +00:00
|
|
|
this.link = link
|
|
|
|
|
|
|
|
const { currentPageId, selectedIds, page } = this.app
|
|
|
|
|
|
|
|
this.initialSelectedIds = [...selectedIds]
|
|
|
|
|
|
|
|
const selectedShapes = (
|
|
|
|
link ? TLDR.getLinkedShapeIds(this.app.state, currentPageId, link, false) : selectedIds
|
|
|
|
)
|
|
|
|
.map((id) => this.app.getShape(id))
|
|
|
|
.filter((shape) => !shape.isLocked)
|
|
|
|
|
|
|
|
const selectedShapeIds = new Set(selectedShapes.map((shape) => shape.id))
|
|
|
|
|
|
|
|
this.hasUnlockedShapes = selectedShapes.length > 0
|
|
|
|
|
|
|
|
this.initialShapes = Array.from(
|
|
|
|
new Set(
|
|
|
|
selectedShapes
|
|
|
|
.filter((shape) => !selectedShapeIds.has(shape.parentId))
|
|
|
|
.flatMap((shape) => {
|
|
|
|
return shape.children
|
|
|
|
? [shape, ...shape.children.map((childId) => this.app.getShape(childId))]
|
|
|
|
: [shape]
|
|
|
|
})
|
|
|
|
).values()
|
|
|
|
)
|
|
|
|
|
|
|
|
this.initialIds = new Set(this.initialShapes.map((shape) => shape.id))
|
|
|
|
|
|
|
|
this.bindingsToDelete = []
|
|
|
|
|
|
|
|
Object.values(page.bindings)
|
|
|
|
.filter((binding) => this.initialIds.has(binding.fromId) || this.initialIds.has(binding.toId))
|
|
|
|
.forEach((binding) => {
|
|
|
|
if (this.initialIds.has(binding.fromId)) {
|
|
|
|
if (!this.initialIds.has(binding.toId)) {
|
|
|
|
this.bindingsToDelete.push(binding)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.initialParentChildren = {}
|
|
|
|
|
|
|
|
this.initialShapes
|
|
|
|
.map((s) => s.parentId)
|
|
|
|
.filter((id) => id !== page.id)
|
|
|
|
.forEach((id) => {
|
|
|
|
this.initialParentChildren[id] = this.app.getShape(id).children!
|
|
|
|
})
|
|
|
|
|
|
|
|
this.initialCommonBounds = Utils.getCommonBounds(this.initialShapes.map(TLDR.getRotatedBounds))
|
|
|
|
|
|
|
|
this.app.rotationInfo.selectedIds = [...this.app.selectedIds]
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
start = (): TldrawPatch | undefined => {
|
|
|
|
const {
|
|
|
|
bindingsToDelete,
|
|
|
|
initialIds,
|
|
|
|
app: { currentPageId, page },
|
|
|
|
} = this
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const allBounds: TLBoundsWithCenter[] = []
|
|
|
|
const otherBounds: TLBoundsWithCenter[] = []
|
|
|
|
|
|
|
|
Object.values(page.shapes).forEach((shape) => {
|
|
|
|
const bounds = Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))
|
|
|
|
allBounds.push(bounds)
|
|
|
|
if (!initialIds.has(shape.id)) {
|
|
|
|
otherBounds.push(bounds)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.snapInfo = {
|
|
|
|
state: 'ready',
|
|
|
|
bounds: allBounds,
|
|
|
|
others: otherBounds,
|
|
|
|
}
|
2021-10-18 13:30:42 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
if (bindingsToDelete.length === 0) return
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const nextBindings: Patch<Record<string, TDBinding>> = {}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
|
2021-08-12 13:39:41 +00:00
|
|
|
|
|
|
|
return {
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: {
|
2021-08-16 21:52:03 +00:00
|
|
|
bindings: nextBindings,
|
|
|
|
},
|
|
|
|
},
|
2021-08-12 13:39:41 +00:00
|
|
|
},
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
update = (): TldrawPatch | undefined => {
|
|
|
|
const {
|
|
|
|
initialParentChildren,
|
|
|
|
initialShapes,
|
|
|
|
initialCommonBounds,
|
|
|
|
bindingsToDelete,
|
|
|
|
app: {
|
|
|
|
pageState: { camera },
|
2021-11-26 15:14:10 +00:00
|
|
|
settings: { isSnapping, showGrid },
|
2021-11-16 16:01:29 +00:00
|
|
|
currentPageId,
|
|
|
|
viewport,
|
|
|
|
selectedIds,
|
|
|
|
currentPoint,
|
|
|
|
previousPoint,
|
|
|
|
originPoint,
|
|
|
|
altKey,
|
|
|
|
shiftKey,
|
|
|
|
metaKey,
|
2021-11-26 15:14:10 +00:00
|
|
|
currentGrid,
|
2021-11-16 16:01:29 +00:00
|
|
|
},
|
|
|
|
} = this
|
2021-10-18 13:30:42 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const nextBindings: Patch<Record<string, TDBinding>> = {}
|
|
|
|
const nextShapes: Patch<Record<string, TDShape>> = {}
|
2021-09-01 14:39:01 +00:00
|
|
|
const nextPageState: Patch<TLPageState> = {}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
let delta = Vec.sub(currentPoint, originPoint)
|
2021-10-18 13:30:42 +00:00
|
|
|
|
|
|
|
let didChangeCloning = false
|
|
|
|
|
|
|
|
if (!this.isCreate) {
|
|
|
|
if (altKey && !this.isCloning) {
|
|
|
|
this.isCloning = true
|
|
|
|
didChangeCloning = true
|
|
|
|
} else if (!altKey && this.isCloning) {
|
|
|
|
this.isCloning = false
|
|
|
|
didChangeCloning = true
|
|
|
|
}
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
if (shiftKey) {
|
2021-08-10 16:12:55 +00:00
|
|
|
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
|
|
|
delta[0] = 0
|
|
|
|
} else {
|
|
|
|
delta[1] = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-18 13:30:42 +00:00
|
|
|
// Should we snap?
|
|
|
|
|
|
|
|
// Speed is used to decide which snap points to use. At a high
|
|
|
|
// speed, we don't use any snap points. At a low speed, we only
|
|
|
|
// allow center-to-center snap points. At very low speed, we
|
|
|
|
// enable all snap points (still preferring middle snaps). We're
|
|
|
|
// using an acceleration function here to smooth the changes in
|
|
|
|
// speed, but we also want the speed to accelerate faster than
|
|
|
|
// it decelerates.
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const speed = Vec.dist(currentPoint, previousPoint)
|
2021-10-18 13:30:42 +00:00
|
|
|
|
|
|
|
const change = speed - this.speed
|
|
|
|
|
|
|
|
this.speed = this.speed + change * (change > 1 ? 0.5 : 0.15)
|
|
|
|
|
|
|
|
this.snapLines = []
|
|
|
|
|
2021-10-19 13:29:55 +00:00
|
|
|
if (
|
2021-11-16 16:01:29 +00:00
|
|
|
((isSnapping && !metaKey) || (!isSnapping && metaKey)) &&
|
|
|
|
this.speed * camera.zoom < SLOW_SPEED &&
|
2021-10-19 13:29:55 +00:00
|
|
|
this.snapInfo.state === 'ready'
|
|
|
|
) {
|
2021-10-18 13:30:42 +00:00
|
|
|
const snapResult = Utils.getSnapPoints(
|
2021-11-26 15:14:10 +00:00
|
|
|
Utils.getBoundsWithCenter(
|
|
|
|
showGrid
|
|
|
|
? Utils.snapBoundsToGrid(Utils.translateBounds(initialCommonBounds, delta), currentGrid)
|
|
|
|
: Utils.translateBounds(initialCommonBounds, delta)
|
|
|
|
),
|
2022-01-14 20:57:54 +00:00
|
|
|
(this.isCloning ? this.snapInfo.bounds : this.snapInfo.others).filter((bounds) => {
|
|
|
|
return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
|
|
|
|
}),
|
2021-11-16 16:01:29 +00:00
|
|
|
SNAP_DISTANCE / camera.zoom
|
2021-10-18 13:30:42 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if (snapResult) {
|
|
|
|
this.snapLines = snapResult.snapLines
|
2021-10-18 14:26:34 +00:00
|
|
|
delta = Vec.sub(delta, snapResult.offset)
|
2021-10-18 13:30:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We've now calculated the "delta", or difference between the
|
|
|
|
// cursor's position (real or adjusted by snaps or axis locking)
|
|
|
|
// and the cursor's original position ("origin").
|
|
|
|
|
|
|
|
// The "movement" is the actual change of position between this
|
|
|
|
// computed position and the previous computed position.
|
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
this.prev = delta
|
|
|
|
|
|
|
|
// If cloning...
|
2021-10-18 13:30:42 +00:00
|
|
|
if (this.isCloning) {
|
2021-08-10 16:12:55 +00:00
|
|
|
// Not Cloning -> Cloning
|
2021-10-18 13:30:42 +00:00
|
|
|
if (didChangeCloning) {
|
|
|
|
if (this.cloneInfo.state === 'empty') {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.createCloneInfo()
|
2021-10-18 13:30:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.cloneInfo.state === 'empty') {
|
|
|
|
throw Error
|
|
|
|
}
|
|
|
|
|
|
|
|
const { clones, clonedBindings } = this.cloneInfo
|
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
this.isCloning = true
|
|
|
|
|
2021-09-01 14:39:01 +00:00
|
|
|
// Put back any bindings we deleted
|
|
|
|
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = binding))
|
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
// Move original shapes back to start
|
2021-08-16 21:52:03 +00:00
|
|
|
initialShapes.forEach((shape) => (nextShapes[shape.id] = { point: shape.point }))
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-02 20:13:54 +00:00
|
|
|
// Add the clones to the page
|
2021-09-01 14:39:01 +00:00
|
|
|
clones.forEach((clone) => {
|
2021-11-26 15:14:10 +00:00
|
|
|
nextShapes[clone.id] = { ...clone }
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-02 20:13:54 +00:00
|
|
|
// Add clones to non-selected parents
|
2021-11-16 16:01:29 +00:00
|
|
|
if (clone.parentId !== currentPageId && !selectedIds.includes(clone.parentId)) {
|
2021-09-02 20:13:54 +00:00
|
|
|
const children =
|
|
|
|
nextShapes[clone.parentId]?.children || initialParentChildren[clone.parentId]
|
|
|
|
|
2021-09-02 20:29:45 +00:00
|
|
|
if (!children.includes(clone.id)) {
|
|
|
|
nextShapes[clone.parentId] = {
|
|
|
|
...nextShapes[clone.parentId],
|
|
|
|
children: [...children, clone.id],
|
|
|
|
}
|
2021-09-02 20:13:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2021-08-11 16:03:08 +00:00
|
|
|
|
2021-09-02 20:13:54 +00:00
|
|
|
// Add the cloned bindings
|
2021-10-18 13:30:42 +00:00
|
|
|
for (const binding of clonedBindings) {
|
2021-08-16 21:52:03 +00:00
|
|
|
nextBindings[binding.id] = binding
|
2021-08-11 16:03:08 +00:00
|
|
|
}
|
2021-09-02 20:13:54 +00:00
|
|
|
|
|
|
|
// Set the selected ids to the clones
|
|
|
|
nextPageState.selectedIds = clones.map((clone) => clone.id)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
// Either way, move the clones
|
|
|
|
clones.forEach((clone) => {
|
|
|
|
nextShapes[clone.id] = {
|
|
|
|
...clone,
|
2021-11-26 15:14:10 +00:00
|
|
|
point: showGrid
|
|
|
|
? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
|
|
|
|
: Vec.toFixed(Vec.add(clone.point, delta)),
|
2021-11-18 18:18:30 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
if (this.cloneInfo.state === 'empty') throw Error
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
const { clones } = this.cloneInfo
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
clones.forEach((clone) => {
|
|
|
|
nextShapes[clone.id] = {
|
2021-11-26 15:14:10 +00:00
|
|
|
point: showGrid
|
|
|
|
? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
|
|
|
|
: Vec.toFixed(Vec.add(clone.point, delta)),
|
2021-11-18 18:18:30 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2021-08-16 21:52:03 +00:00
|
|
|
} else {
|
|
|
|
// If not cloning...
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
// Cloning -> Not Cloning
|
2021-10-18 13:30:42 +00:00
|
|
|
if (didChangeCloning) {
|
|
|
|
if (this.cloneInfo.state === 'empty') throw Error
|
|
|
|
|
|
|
|
const { clones, clonedBindings } = this.cloneInfo
|
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
this.isCloning = false
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-01 14:39:01 +00:00
|
|
|
// Delete the bindings
|
|
|
|
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
|
|
|
|
|
2021-10-22 12:23:36 +00:00
|
|
|
// Remove the clones from parents
|
2021-09-02 20:13:54 +00:00
|
|
|
clones.forEach((clone) => {
|
|
|
|
if (clone.parentId !== currentPageId) {
|
|
|
|
nextShapes[clone.parentId] = {
|
|
|
|
...nextShapes[clone.parentId],
|
|
|
|
children: initialParentChildren[clone.parentId],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2021-08-11 16:03:08 +00:00
|
|
|
|
2021-10-22 12:23:36 +00:00
|
|
|
// Delete the clones (including any parent clones)
|
|
|
|
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
|
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
// Move the original shapes back to the cursor position
|
|
|
|
initialShapes.forEach((shape) => {
|
|
|
|
nextShapes[shape.id] = {
|
2021-11-26 15:14:10 +00:00
|
|
|
point: showGrid
|
|
|
|
? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
|
|
|
|
: Vec.toFixed(Vec.add(shape.point, delta)),
|
2021-08-16 21:52:03 +00:00
|
|
|
}
|
|
|
|
})
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
// Delete the cloned bindings
|
2021-10-18 13:30:42 +00:00
|
|
|
for (const binding of clonedBindings) {
|
2021-08-16 21:52:03 +00:00
|
|
|
nextBindings[binding.id] = undefined
|
2021-08-11 16:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
// Set selected ids
|
|
|
|
nextPageState.selectedIds = initialShapes.map((shape) => shape.id)
|
2021-11-26 15:14:10 +00:00
|
|
|
} else {
|
|
|
|
// Move the shapes by the delta
|
|
|
|
initialShapes.forEach((shape) => {
|
|
|
|
// const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-11-26 15:14:10 +00:00
|
|
|
nextShapes[shape.id] = {
|
|
|
|
point: showGrid
|
|
|
|
? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
|
|
|
|
: Vec.toFixed(Vec.add(shape.point, delta)),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
return {
|
2021-10-18 13:30:42 +00:00
|
|
|
appState: {
|
|
|
|
snapLines: this.snapLines,
|
|
|
|
},
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: {
|
2021-08-16 21:52:03 +00:00
|
|
|
shapes: nextShapes,
|
|
|
|
bindings: nextBindings,
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
|
|
|
pageStates: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: nextPageState,
|
2021-08-16 21:52:03 +00:00
|
|
|
},
|
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
cancel = (): TldrawPatch | undefined => {
|
|
|
|
const {
|
|
|
|
initialShapes,
|
|
|
|
initialSelectedIds,
|
|
|
|
bindingsToDelete,
|
|
|
|
app: { currentPageId },
|
|
|
|
} = this
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const nextBindings: Record<string, Partial<TDBinding> | undefined> = {}
|
|
|
|
const nextShapes: Record<string, Partial<TDShape> | undefined> = {}
|
2021-10-15 09:33:48 +00:00
|
|
|
const nextPageState: Partial<TLPageState> = {
|
|
|
|
editingId: undefined,
|
|
|
|
hoveredId: undefined,
|
|
|
|
}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
|
|
|
// Put back any deleted bindings
|
|
|
|
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = binding))
|
|
|
|
|
2021-10-15 09:33:48 +00:00
|
|
|
if (this.isCreate) {
|
|
|
|
initialShapes.forEach(({ id }) => (nextShapes[id] = undefined))
|
|
|
|
nextPageState.selectedIds = []
|
|
|
|
} else {
|
|
|
|
// Put initial shapes back to where they started
|
|
|
|
initialShapes.forEach(({ id, point }) => (nextShapes[id] = { ...nextShapes[id], point }))
|
2021-11-16 16:01:29 +00:00
|
|
|
nextPageState.selectedIds = initialSelectedIds
|
2021-10-15 09:33:48 +00:00
|
|
|
}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-10-18 13:30:42 +00:00
|
|
|
if (this.cloneInfo.state === 'ready') {
|
|
|
|
const { clones, clonedBindings } = this.cloneInfo
|
|
|
|
// Delete clones
|
|
|
|
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-10-18 13:30:42 +00:00
|
|
|
// Delete cloned bindings
|
|
|
|
clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
|
|
|
|
}
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
return {
|
2021-10-18 13:30:42 +00:00
|
|
|
appState: {
|
|
|
|
snapLines: [],
|
|
|
|
},
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: {
|
2021-08-16 21:52:03 +00:00
|
|
|
shapes: nextShapes,
|
|
|
|
bindings: nextBindings,
|
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
|
|
|
pageStates: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: nextPageState,
|
2021-08-16 21:52:03 +00:00
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
|
|
|
const {
|
|
|
|
initialShapes,
|
|
|
|
initialParentChildren,
|
|
|
|
bindingsToDelete,
|
|
|
|
app: { currentPageId },
|
|
|
|
} = this
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const beforeBindings: Patch<Record<string, TDBinding>> = {}
|
|
|
|
const beforeShapes: Patch<Record<string, TDShape>> = {}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const afterBindings: Patch<Record<string, TDBinding>> = {}
|
|
|
|
const afterShapes: Patch<Record<string, TDShape>> = {}
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-09-06 13:30:59 +00:00
|
|
|
if (this.isCloning) {
|
2021-10-18 13:30:42 +00:00
|
|
|
if (this.cloneInfo.state === 'empty') {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.createCloneInfo()
|
2021-10-18 13:30:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.cloneInfo.state !== 'ready') throw Error
|
|
|
|
const { clones, clonedBindings } = this.cloneInfo
|
|
|
|
|
2021-09-06 13:30:59 +00:00
|
|
|
// Update the clones
|
|
|
|
clones.forEach((clone) => {
|
|
|
|
beforeShapes[clone.id] = undefined
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
afterShapes[clone.id] = this.app.getShape(clone.id)
|
2021-09-06 13:30:59 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
if (clone.parentId !== currentPageId) {
|
2021-09-06 13:30:59 +00:00
|
|
|
beforeShapes[clone.parentId] = {
|
|
|
|
...beforeShapes[clone.parentId],
|
|
|
|
children: initialParentChildren[clone.parentId],
|
|
|
|
}
|
|
|
|
|
|
|
|
afterShapes[clone.parentId] = {
|
|
|
|
...afterShapes[clone.parentId],
|
2021-11-16 16:01:29 +00:00
|
|
|
children: this.app.getShape<GroupShape>(clone.parentId).children,
|
2021-09-06 13:30:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-09-06 13:30:59 +00:00
|
|
|
// Update the cloned bindings
|
|
|
|
clonedBindings.forEach((binding) => {
|
|
|
|
beforeBindings[binding.id] = undefined
|
2021-11-16 16:01:29 +00:00
|
|
|
afterBindings[binding.id] = this.app.getBinding(binding.id)
|
2021-09-06 13:30:59 +00:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// If we aren't cloning, then update the initial shapes
|
|
|
|
initialShapes.forEach((shape) => {
|
2021-10-15 09:33:48 +00:00
|
|
|
beforeShapes[shape.id] = this.isCreate
|
|
|
|
? undefined
|
|
|
|
: {
|
|
|
|
...beforeShapes[shape.id],
|
|
|
|
point: shape.point,
|
|
|
|
}
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-09-06 13:30:59 +00:00
|
|
|
afterShapes[shape.id] = {
|
|
|
|
...afterShapes[shape.id],
|
2021-10-16 20:06:29 +00:00
|
|
|
...(this.isCreate
|
2021-11-16 16:01:29 +00:00
|
|
|
? this.app.getShape(shape.id)
|
|
|
|
: { point: this.app.getShape(shape.id).point }),
|
2021-09-06 13:30:59 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-09-06 13:30:59 +00:00
|
|
|
// Update the deleted bindings and any associated shapes
|
2021-08-12 13:39:41 +00:00
|
|
|
bindingsToDelete.forEach((binding) => {
|
2021-08-16 21:52:03 +00:00
|
|
|
beforeBindings[binding.id] = binding
|
|
|
|
|
2021-08-12 13:39:41 +00:00
|
|
|
for (const id of [binding.toId, binding.fromId]) {
|
|
|
|
// Let's also look at the bound shape...
|
2021-11-16 16:01:29 +00:00
|
|
|
const shape = this.app.getShape(id)
|
2021-08-12 13:39:41 +00:00
|
|
|
|
|
|
|
// If the bound shape has a handle that references the deleted binding, delete that reference
|
|
|
|
if (!shape.handles) continue
|
|
|
|
|
|
|
|
Object.values(shape.handles)
|
|
|
|
.filter((handle) => handle.bindingId === binding.id)
|
|
|
|
.forEach((handle) => {
|
2021-09-01 14:39:01 +00:00
|
|
|
beforeShapes[id] = { ...beforeShapes[id], handles: {} }
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-09-01 14:39:01 +00:00
|
|
|
afterShapes[id] = { ...afterShapes[id], handles: {} }
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-09-06 13:30:59 +00:00
|
|
|
// There should be before and after shapes
|
|
|
|
|
2021-09-01 14:39:01 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
beforeShapes[id]!.handles![handle.id as keyof ArrowShape['handles']] = {
|
|
|
|
bindingId: binding.id,
|
2021-08-12 13:39:41 +00:00
|
|
|
}
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-09-01 14:39:01 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
afterShapes[id]!.handles![handle.id as keyof ArrowShape['handles']] = {
|
|
|
|
bindingId: undefined,
|
2021-08-12 13:39:41 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
return {
|
|
|
|
id: 'translate',
|
|
|
|
before: {
|
2021-10-18 13:30:42 +00:00
|
|
|
appState: {
|
|
|
|
snapLines: [],
|
|
|
|
},
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: {
|
2021-08-16 21:52:03 +00:00
|
|
|
shapes: beforeShapes,
|
|
|
|
bindings: beforeBindings,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
pageStates: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: {
|
|
|
|
selectedIds: this.isCreate ? [] : [...this.initialSelectedIds],
|
2021-08-16 21:52:03 +00:00
|
|
|
},
|
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
after: {
|
2021-10-18 13:30:42 +00:00
|
|
|
appState: {
|
|
|
|
snapLines: [],
|
|
|
|
},
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: {
|
2021-08-16 21:52:03 +00:00
|
|
|
shapes: afterShapes,
|
|
|
|
bindings: afterBindings,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
pageStates: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[currentPageId]: {
|
|
|
|
selectedIds: [...this.app.selectedIds],
|
2021-08-16 21:52:03 +00:00
|
|
|
},
|
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2021-10-18 13:30:42 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
private createCloneInfo = () => {
|
2021-10-18 13:30:42 +00:00
|
|
|
// Create clones when as they're needed.
|
|
|
|
// Consider doing this work in a worker.
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const {
|
|
|
|
initialShapes,
|
|
|
|
initialParentChildren,
|
|
|
|
app: { selectedIds, currentPageId, page },
|
|
|
|
} = this
|
2021-10-18 13:30:42 +00:00
|
|
|
|
|
|
|
const cloneMap: Record<string, string> = {}
|
|
|
|
const clonedBindingsMap: Record<string, string> = {}
|
2021-11-16 16:01:29 +00:00
|
|
|
const clonedBindings: TDBinding[] = []
|
2021-10-18 13:30:42 +00:00
|
|
|
|
|
|
|
// Create clones of selected shapes
|
2021-11-16 16:01:29 +00:00
|
|
|
const clones: TDShape[] = []
|
2021-10-18 13:30:42 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
initialShapes.forEach((shape) => {
|
2021-10-18 13:30:42 +00:00
|
|
|
const newId = Utils.uniqueId()
|
|
|
|
|
|
|
|
initialParentChildren[newId] = initialParentChildren[shape.id]
|
|
|
|
|
|
|
|
cloneMap[shape.id] = newId
|
|
|
|
|
|
|
|
const clone = {
|
|
|
|
...Utils.deepClone(shape),
|
|
|
|
id: newId,
|
|
|
|
parentId: shape.parentId,
|
2021-11-16 16:01:29 +00:00
|
|
|
childIndex: TLDR.getChildIndexAbove(this.app.state, shape.id, currentPageId),
|
2021-10-18 13:30:42 +00:00
|
|
|
}
|
|
|
|
|
2021-12-25 17:06:33 +00:00
|
|
|
if (clone.type === TDShapeType.Video) {
|
|
|
|
const element = document.getElementById(shape.id + '_video') as HTMLVideoElement
|
|
|
|
if (element) clone.currentTime = (element.currentTime + 16) % element.duration
|
|
|
|
}
|
|
|
|
|
2021-10-18 13:30:42 +00:00
|
|
|
clones.push(clone)
|
|
|
|
})
|
|
|
|
|
|
|
|
clones.forEach((clone) => {
|
|
|
|
if (clone.children !== undefined) {
|
|
|
|
clone.children = clone.children.map((childId) => cloneMap[childId])
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
clones.forEach((clone) => {
|
|
|
|
if (selectedIds.includes(clone.parentId)) {
|
|
|
|
clone.parentId = cloneMap[clone.parentId]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Potentially confusing name here: these are the ids of the
|
|
|
|
// original shapes that were cloned, not their clones' ids.
|
|
|
|
const clonedShapeIds = new Set(Object.keys(cloneMap))
|
|
|
|
|
|
|
|
// Create cloned bindings for shapes where both to and from shapes are selected
|
|
|
|
// (if the user clones, then we will create a new binding for the clones)
|
|
|
|
Object.values(page.bindings)
|
|
|
|
.filter((binding) => clonedShapeIds.has(binding.fromId) || clonedShapeIds.has(binding.toId))
|
|
|
|
.forEach((binding) => {
|
|
|
|
if (clonedShapeIds.has(binding.fromId)) {
|
|
|
|
if (clonedShapeIds.has(binding.toId)) {
|
|
|
|
const cloneId = Utils.uniqueId()
|
|
|
|
const cloneBinding = {
|
|
|
|
...Utils.deepClone(binding),
|
|
|
|
id: cloneId,
|
|
|
|
fromId: cloneMap[binding.fromId] || binding.fromId,
|
|
|
|
toId: cloneMap[binding.toId] || binding.toId,
|
|
|
|
}
|
|
|
|
clonedBindingsMap[binding.id] = cloneId
|
|
|
|
clonedBindings.push(cloneBinding)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Assign new binding ids to clones (or delete them!)
|
|
|
|
clones.forEach((clone) => {
|
|
|
|
if (clone.handles) {
|
|
|
|
if (clone.handles) {
|
|
|
|
for (const id in clone.handles) {
|
|
|
|
const handle = clone.handles[id as keyof ArrowShape['handles']]
|
|
|
|
handle.bindingId = handle.bindingId ? clonedBindingsMap[handle.bindingId] : undefined
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
clones.forEach((clone) => {
|
|
|
|
if (page.shapes[clone.id]) {
|
|
|
|
throw Error("uh oh, we didn't clone correctly")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.cloneInfo = {
|
|
|
|
state: 'ready',
|
|
|
|
clones,
|
2021-11-26 15:14:10 +00:00
|
|
|
cloneMap,
|
2021-10-18 13:30:42 +00:00
|
|
|
clonedBindings,
|
|
|
|
}
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|