Tldraw/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts

204 wiersze
5.8 KiB
TypeScript

import { Vec } from '@tldraw/vec'
import {
SessionType,
TDStatus,
TDShape,
PagePartial,
TDBinding,
TldrawPatch,
TldrawCommand,
} from '~types'
import type { TldrawApp } from '../../internal'
import { BaseSession } from '../BaseSession'
export class EraseSession extends BaseSession {
type = SessionType.Draw
status = TDStatus.Creating
isLocked?: boolean
lockedDirection?: 'horizontal' | 'vertical'
erasedShapes = new Set<TDShape>()
erasedBindings = new Set<TDBinding>()
initialSelectedShapes: TDShape[]
erasableShapes: TDShape[]
prevPoint: number[]
constructor(app: TldrawApp) {
super(app)
this.prevPoint = [...app.originPoint]
this.initialSelectedShapes = this.app.selectedIds.map((id) => this.app.getShape(id))
this.erasableShapes = this.app.shapes.filter((shape) => !shape.isLocked)
}
start = (): TldrawPatch | undefined => void null
update = (): TldrawPatch | undefined => {
const { page, shiftKey, originPoint, currentPoint } = this.app
if (shiftKey) {
if (!this.isLocked && Vec.dist(originPoint, currentPoint) > 4) {
// If we're locking before knowing what direction we're in, set it
// early based on the bigger dimension.
if (!this.lockedDirection) {
const delta = Vec.sub(currentPoint, originPoint)
this.lockedDirection = delta[0] > delta[1] ? 'horizontal' : 'vertical'
}
this.isLocked = true
}
} else if (this.isLocked) {
this.isLocked = false
}
if (this.isLocked) {
if (this.lockedDirection === 'vertical') {
currentPoint[0] = originPoint[0]
} else {
currentPoint[1] = originPoint[1]
}
}
const newPoint = Vec.toFixed(Vec.add(originPoint, Vec.sub(currentPoint, originPoint)))
const deletedShapeIds = new Set<string>([])
for (const shape of this.erasableShapes) {
if (this.erasedShapes.has(shape)) continue
if (this.app.getShapeUtil(shape).hitTestLineSegment(shape, this.prevPoint, newPoint)) {
this.erasedShapes.add(shape)
deletedShapeIds.add(shape.id)
if (shape.children !== undefined) {
for (const childId of shape.children) {
this.erasedShapes.add(this.app.getShape(childId))
deletedShapeIds.add(childId)
}
}
}
}
// Erase bindings that reference deleted shapes
Object.values(page.bindings).forEach((binding) => {
for (const id of [binding.toId, binding.fromId]) {
if (deletedShapeIds.has(id)) {
this.erasedBindings.add(binding)
}
}
})
const erasedShapes = Array.from(this.erasedShapes.values())
this.prevPoint = newPoint
return {
document: {
pages: {
[page.id]: {
shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, { isGhost: true }])),
},
},
},
}
}
cancel = (): TldrawPatch | undefined => {
const { page } = this.app
const erasedShapes = Array.from(this.erasedShapes.values())
return {
document: {
pages: {
[page.id]: {
shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, { isGhost: false }])),
},
},
pageStates: {
[page.id]: {
selectedIds: this.initialSelectedShapes.map((shape) => shape.id),
},
},
},
}
}
complete = (): TldrawPatch | TldrawCommand | undefined => {
const { page } = this.app
const erasedShapes = Array.from(this.erasedShapes.values())
const erasedBindings = Array.from(this.erasedBindings.values())
const erasedShapeIds = erasedShapes.map((shape) => shape.id)
const erasedBindingIds = erasedBindings.map((binding) => binding.id)
const before: PagePartial = {
shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, shape])),
bindings: Object.fromEntries(erasedBindings.map((binding) => [binding.id, binding])),
}
const after: PagePartial = {
shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, undefined])),
bindings: Object.fromEntries(erasedBindings.map((binding) => [binding.id, undefined])),
}
// Remove references on any shape's handles to any deleted bindings
this.app.shapes.forEach((shape) => {
if (shape.handles && !after.shapes[shape.id]) {
Object.values(shape.handles).forEach((handle) => {
if (handle.bindingId && erasedBindingIds.includes(handle.bindingId)) {
// Save the binding reference in the before patch
before.shapes[shape.id] = {
...before.shapes[shape.id],
handles: {
...before.shapes[shape.id]?.handles,
[handle.id]: handle,
},
}
// Save the binding reference in the before patch
if (!erasedShapeIds.includes(shape.id)) {
after.shapes[shape.id] = {
...after.shapes[shape.id],
handles: {
...after.shapes[shape.id]?.handles,
[handle.id]: undefined,
},
}
}
}
})
}
})
return {
id: 'erase',
before: {
document: {
pages: {
[page.id]: before,
},
pageStates: {
[page.id]: {
selectedIds: this.initialSelectedShapes.map((shape) => shape.id),
},
},
},
},
after: {
document: {
pages: {
[page.id]: after,
},
pageStates: {
[page.id]: {
selectedIds: this.initialSelectedShapes
.filter((shape) => !erasedShapeIds.includes(shape.id))
.map((shape) => shape.id),
},
},
},
},
}
}
}