2021-09-12 12:21:44 +00:00
|
|
|
import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core'
|
2021-10-19 11:19:56 +00:00
|
|
|
import {
|
2021-11-16 16:01:29 +00:00
|
|
|
TDSnapshot,
|
2021-08-12 13:39:41 +00:00
|
|
|
ShapeStyles,
|
|
|
|
ShapesWithProp,
|
2021-11-16 16:01:29 +00:00
|
|
|
TDShape,
|
|
|
|
TDBinding,
|
|
|
|
TDPage,
|
|
|
|
TldrawCommand,
|
|
|
|
TldrawPatch,
|
|
|
|
TDShapeType,
|
2021-10-19 11:19:56 +00:00
|
|
|
ArrowShape,
|
2021-08-13 09:28:09 +00:00
|
|
|
} from '~types'
|
2021-09-12 12:21:44 +00:00
|
|
|
import { Vec } from '@tldraw/vec'
|
2021-11-16 16:01:29 +00:00
|
|
|
import type { TDShapeUtil } from './shapes/TDShapeUtil'
|
|
|
|
import { getShapeUtil } from './shapes'
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
export class TLDR {
|
2021-09-13 15:38:42 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2021-11-16 16:01:29 +00:00
|
|
|
static getShapeUtil<T extends TDShape>(type: T['type']): TDShapeUtil<T>
|
2021-09-13 15:38:42 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2021-11-16 16:01:29 +00:00
|
|
|
static getShapeUtil<T extends TDShape>(shape: T): TDShapeUtil<T>
|
|
|
|
static getShapeUtil<T extends TDShape>(shape: T | T['type']) {
|
|
|
|
return getShapeUtil<T>(shape)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getSelectedShapes(data: TDSnapshot, pageId: string) {
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = TLDR.getPage(data, pageId)
|
|
|
|
const selectedIds = TLDR.getSelectedIds(data, pageId)
|
2021-08-16 21:52:03 +00:00
|
|
|
return selectedIds.map((id) => page.shapes[id])
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static screenToWorld(data: TDSnapshot, point: number[]) {
|
2021-09-13 15:38:42 +00:00
|
|
|
const camera = TLDR.getPageState(data, data.appState.currentPageId).camera
|
2021-08-10 16:12:55 +00:00
|
|
|
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
|
|
|
}
|
|
|
|
|
|
|
|
static getCameraZoom(zoom: number) {
|
|
|
|
return Utils.clamp(zoom, 0.1, 5)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getPage(data: TDSnapshot, pageId: string): TDPage {
|
2021-08-16 21:52:03 +00:00
|
|
|
return data.document.pages[pageId]
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getPageState(data: TDSnapshot, pageId: string): TLPageState {
|
2021-08-16 21:52:03 +00:00
|
|
|
return data.document.pageStates[pageId]
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getSelectedIds(data: TDSnapshot, pageId: string): string[] {
|
2021-09-13 15:38:42 +00:00
|
|
|
return TLDR.getPageState(data, pageId).selectedIds
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getShapes(data: TDSnapshot, pageId: string): TDShape[] {
|
2021-09-13 15:38:42 +00:00
|
|
|
return Object.values(TLDR.getPage(data, pageId).shapes)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getCamera(data: TDSnapshot, pageId: string): TLPageState['camera'] {
|
2021-09-13 15:38:42 +00:00
|
|
|
return TLDR.getPageState(data, pageId).camera
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getShape<T extends TDShape = TDShape>(
|
|
|
|
data: TDSnapshot,
|
2021-08-16 21:52:03 +00:00
|
|
|
shapeId: string,
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId: string
|
2021-08-16 21:52:03 +00:00
|
|
|
): T {
|
2021-09-13 15:38:42 +00:00
|
|
|
return TLDR.getPage(data, pageId).shapes[shapeId] as T
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getCenter<T extends TDShape>(shape: T) {
|
|
|
|
return TLDR.getShapeUtil(shape).getCenter(shape)
|
2021-09-19 13:53:52 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getBounds<T extends TDShape>(shape: T) {
|
|
|
|
return TLDR.getShapeUtil(shape).getBounds(shape)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getRotatedBounds<T extends TDShape>(shape: T) {
|
|
|
|
return TLDR.getShapeUtil(shape).getRotatedBounds(shape)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getSelectedBounds(data: TDSnapshot): TLBounds {
|
2021-08-10 16:12:55 +00:00
|
|
|
return Utils.getCommonBounds(
|
2021-09-13 15:38:42 +00:00
|
|
|
TLDR.getSelectedShapes(data, data.appState.currentPageId).map((shape) =>
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShapeUtil(shape).getBounds(shape)
|
2021-08-17 21:38:37 +00:00
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getParentId(data: TDSnapshot, id: string, pageId: string) {
|
2021-09-13 15:38:42 +00:00
|
|
|
return TLDR.getShape(data, id, pageId).parentId
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
// static getPointedId(data: TDSnapshot, id: string, pageId: string): string {
|
2021-10-28 16:50:58 +00:00
|
|
|
// const page = TLDR.getPage(data, pageId)
|
|
|
|
// const pageState = TLDR.getPageState(data, data.appState.currentPageId)
|
|
|
|
// const shape = TLDR.getShape(data, id, pageId)
|
|
|
|
// if (!shape) return id
|
|
|
|
|
|
|
|
// return shape.parentId === pageState.currentParentId || shape.parentId === page.id
|
|
|
|
// ? id
|
|
|
|
// : TLDR.getPointedId(data, shape.parentId, pageId)
|
|
|
|
// }
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
// static getDrilledPointedId(data: TDSnapshot, id: string, pageId: string): string {
|
2021-10-28 16:50:58 +00:00
|
|
|
// const shape = TLDR.getShape(data, id, pageId)
|
|
|
|
// const { currentPageId } = data.appState
|
|
|
|
// const { currentParentId, pointedId } = TLDR.getPageState(data, data.appState.currentPageId)
|
|
|
|
|
|
|
|
// return shape.parentId === currentPageId ||
|
|
|
|
// shape.parentId === pointedId ||
|
|
|
|
// shape.parentId === currentParentId
|
|
|
|
// ? id
|
|
|
|
// : TLDR.getDrilledPointedId(data, shape.parentId, pageId)
|
|
|
|
// }
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
// static getTopParentId(data: TDSnapshot, id: string, pageId: string): string {
|
2021-10-28 16:50:58 +00:00
|
|
|
// const page = TLDR.getPage(data, pageId)
|
|
|
|
// const pageState = TLDR.getPageState(data, pageId)
|
|
|
|
// const shape = TLDR.getShape(data, id, pageId)
|
|
|
|
|
|
|
|
// if (shape.parentId === shape.id) {
|
|
|
|
// throw Error(`Shape has the same id as its parent! ${shape.id}`)
|
|
|
|
// }
|
|
|
|
|
|
|
|
// return shape.parentId === page.id || shape.parentId === pageState.currentParentId
|
|
|
|
// ? id
|
|
|
|
// : TLDR.getTopParentId(data, shape.parentId, pageId)
|
|
|
|
// }
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
// Get an array of a shape id and its descendant shapes' ids
|
2021-11-16 16:01:29 +00:00
|
|
|
static getDocumentBranch(data: TDSnapshot, id: string, pageId: string): string[] {
|
2021-09-13 15:38:42 +00:00
|
|
|
const shape = TLDR.getShape(data, id, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
if (shape.children === undefined) return [id]
|
|
|
|
|
2021-08-17 21:38:37 +00:00
|
|
|
return [
|
|
|
|
id,
|
2021-09-13 15:38:42 +00:00
|
|
|
...shape.children.flatMap((childId) => TLDR.getDocumentBranch(data, childId, pageId)),
|
2021-08-17 21:38:37 +00:00
|
|
|
]
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get a deep array of unproxied shapes and their descendants
|
|
|
|
static getSelectedBranchSnapshot<K>(
|
2021-11-16 16:01:29 +00:00
|
|
|
data: TDSnapshot,
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId: string,
|
2021-11-16 16:01:29 +00:00
|
|
|
fn: (shape: TDShape) => K
|
2021-08-10 16:12:55 +00:00
|
|
|
): ({ id: string } & K)[]
|
2021-11-16 16:01:29 +00:00
|
|
|
static getSelectedBranchSnapshot(data: TDSnapshot, pageId: string): TDShape[]
|
2021-08-10 16:12:55 +00:00
|
|
|
static getSelectedBranchSnapshot<K>(
|
2021-11-16 16:01:29 +00:00
|
|
|
data: TDSnapshot,
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId: string,
|
2021-11-16 16:01:29 +00:00
|
|
|
fn?: (shape: TDShape) => K
|
|
|
|
): (TDShape | K)[] {
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = TLDR.getPage(data, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-13 15:38:42 +00:00
|
|
|
const copies = TLDR.getSelectedIds(data, pageId)
|
|
|
|
.flatMap((id) => TLDR.getDocumentBranch(data, id, pageId).map((id) => page.shapes[id]))
|
2021-08-10 16:12:55 +00:00
|
|
|
.filter((shape) => !shape.isLocked)
|
|
|
|
.map(Utils.deepClone)
|
|
|
|
|
|
|
|
if (fn !== undefined) {
|
|
|
|
return copies.map((shape) => ({ id: shape.id, ...fn(shape) }))
|
|
|
|
}
|
|
|
|
|
|
|
|
return copies
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a shallow array of unproxied shapes
|
2021-11-16 16:01:29 +00:00
|
|
|
static getSelectedShapeSnapshot(data: TDSnapshot, pageId: string): TDShape[]
|
2021-08-10 16:12:55 +00:00
|
|
|
static getSelectedShapeSnapshot<K>(
|
2021-11-16 16:01:29 +00:00
|
|
|
data: TDSnapshot,
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId: string,
|
2021-11-16 16:01:29 +00:00
|
|
|
fn?: (shape: TDShape) => K
|
2021-08-10 16:12:55 +00:00
|
|
|
): ({ id: string } & K)[]
|
|
|
|
static getSelectedShapeSnapshot<K>(
|
2021-11-16 16:01:29 +00:00
|
|
|
data: TDSnapshot,
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId: string,
|
2021-11-16 16:01:29 +00:00
|
|
|
fn?: (shape: TDShape) => K
|
|
|
|
): (TDShape | K)[] {
|
2021-09-13 15:38:42 +00:00
|
|
|
const copies = TLDR.getSelectedShapes(data, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
.filter((shape) => !shape.isLocked)
|
|
|
|
.map(Utils.deepClone)
|
|
|
|
|
|
|
|
if (fn !== undefined) {
|
|
|
|
return copies.map((shape) => ({ id: shape.id, ...fn(shape) }))
|
|
|
|
}
|
|
|
|
|
|
|
|
return copies
|
|
|
|
}
|
|
|
|
|
|
|
|
// For a given array of shape ids, an array of all other shapes that may be affected by a mutation to it.
|
|
|
|
// Use this to decide which shapes to clone as before / after for a command.
|
2021-11-16 16:01:29 +00:00
|
|
|
static getAllEffectedShapeIds(data: TDSnapshot, ids: string[], pageId: string): string[] {
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = TLDR.getPage(data, pageId)
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
const visited = new Set(ids)
|
|
|
|
|
|
|
|
ids.forEach((id) => {
|
2021-08-16 21:52:03 +00:00
|
|
|
const shape = page.shapes[id]
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
// Add descendant shapes
|
2021-11-16 16:01:29 +00:00
|
|
|
function collectDescendants(shape: TDShape): void {
|
2021-08-10 16:12:55 +00:00
|
|
|
if (shape.children === undefined) return
|
|
|
|
shape.children
|
|
|
|
.filter((childId) => !visited.has(childId))
|
|
|
|
.forEach((childId) => {
|
|
|
|
visited.add(childId)
|
2021-08-16 21:52:03 +00:00
|
|
|
collectDescendants(page.shapes[childId])
|
2021-08-10 16:12:55 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
collectDescendants(shape)
|
|
|
|
|
|
|
|
// Add asecendant shapes
|
2021-11-16 16:01:29 +00:00
|
|
|
function collectAscendants(shape: TDShape): void {
|
2021-08-10 16:12:55 +00:00
|
|
|
const parentId = shape.parentId
|
2021-08-16 21:52:03 +00:00
|
|
|
if (parentId === page.id) return
|
2021-08-10 16:12:55 +00:00
|
|
|
if (visited.has(parentId)) return
|
|
|
|
visited.add(parentId)
|
2021-08-16 21:52:03 +00:00
|
|
|
collectAscendants(page.shapes[parentId])
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
collectAscendants(shape)
|
|
|
|
|
|
|
|
// Add bindings that are to or from any of the visited shapes (this does not have to be recursive)
|
|
|
|
visited.forEach((id) => {
|
2021-08-16 21:52:03 +00:00
|
|
|
Object.values(page.bindings)
|
2021-08-10 16:12:55 +00:00
|
|
|
.filter((binding) => binding.fromId === id || binding.toId === id)
|
2021-08-10 20:36:29 +00:00
|
|
|
.forEach((binding) => visited.add(binding.fromId === id ? binding.toId : binding.fromId))
|
2021-08-10 16:12:55 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
// Return the unique array of visited shapes
|
|
|
|
return Array.from(visited.values())
|
|
|
|
}
|
|
|
|
|
|
|
|
static updateBindings(
|
2021-11-16 16:01:29 +00:00
|
|
|
data: TDSnapshot,
|
2021-08-10 16:12:55 +00:00
|
|
|
id: string,
|
2021-11-16 16:01:29 +00:00
|
|
|
beforeShapes: Record<string, Partial<TDShape>> = {},
|
|
|
|
afterShapes: Record<string, Partial<TDShape>> = {},
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId: string
|
2021-11-16 16:01:29 +00:00
|
|
|
): TDSnapshot {
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = { ...TLDR.getPage(data, pageId) }
|
2021-08-16 21:52:03 +00:00
|
|
|
return Object.values(page.bindings)
|
2021-08-10 16:12:55 +00:00
|
|
|
.filter((binding) => binding.fromId === id || binding.toId === id)
|
2021-11-16 16:01:29 +00:00
|
|
|
.reduce((cTDSnapshot, binding) => {
|
2021-09-01 12:00:51 +00:00
|
|
|
if (!beforeShapes[binding.fromId]) {
|
2021-08-17 21:38:37 +00:00
|
|
|
beforeShapes[binding.fromId] = Utils.deepClone(
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShape(cTDSnapshot, binding.fromId, pageId)
|
2021-08-17 21:38:37 +00:00
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!beforeShapes[binding.toId]) {
|
2021-11-08 14:21:37 +00:00
|
|
|
beforeShapes[binding.toId] = Utils.deepClone(
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
2021-11-08 14:21:37 +00:00
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-13 15:38:42 +00:00
|
|
|
TLDR.onBindingChange(
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShape(cTDSnapshot, binding.fromId, pageId),
|
2021-08-10 16:12:55 +00:00
|
|
|
binding,
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
)
|
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
afterShapes[binding.fromId] = Utils.deepClone(
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShape(cTDSnapshot, binding.fromId, pageId)
|
2021-11-08 14:21:37 +00:00
|
|
|
)
|
|
|
|
afterShapes[binding.toId] = Utils.deepClone(
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
2021-11-08 14:21:37 +00:00
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
return cTDSnapshot
|
2021-08-10 16:12:55 +00:00
|
|
|
}, data)
|
|
|
|
}
|
2021-10-19 11:19:56 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getLinkedShapeIds(
|
|
|
|
data: TDSnapshot,
|
2021-10-19 11:19:56 +00:00
|
|
|
pageId: string,
|
|
|
|
direction: 'center' | 'left' | 'right',
|
|
|
|
includeArrows = true
|
|
|
|
) {
|
|
|
|
const selectedIds = TLDR.getSelectedIds(data, pageId)
|
|
|
|
|
|
|
|
const page = TLDR.getPage(data, pageId)
|
|
|
|
|
|
|
|
const linkedIds = new Set<string>(selectedIds)
|
|
|
|
|
|
|
|
const checkedIds = new Set<string>()
|
|
|
|
|
|
|
|
const idsToCheck = [...selectedIds]
|
|
|
|
|
|
|
|
const arrows = new Set(
|
|
|
|
Object.values(page.shapes).filter((shape) => {
|
|
|
|
return (
|
2021-11-16 16:01:29 +00:00
|
|
|
shape.type === TDShapeType.Arrow &&
|
2021-10-19 11:19:56 +00:00
|
|
|
(shape.handles.start.bindingId || shape.handles?.end.bindingId)
|
|
|
|
)
|
|
|
|
}) as ArrowShape[]
|
|
|
|
)
|
|
|
|
|
|
|
|
while (idsToCheck.length) {
|
|
|
|
const id = idsToCheck.pop()
|
|
|
|
|
|
|
|
if (!(id && arrows.size)) break
|
|
|
|
|
|
|
|
if (checkedIds.has(id)) continue
|
|
|
|
|
|
|
|
checkedIds.add(id)
|
|
|
|
|
|
|
|
arrows.forEach((arrow) => {
|
|
|
|
const {
|
|
|
|
handles: {
|
|
|
|
start: { bindingId: startBindingId },
|
|
|
|
end: { bindingId: endBindingId },
|
|
|
|
},
|
|
|
|
} = arrow
|
|
|
|
|
|
|
|
const startBinding = startBindingId ? page.bindings[startBindingId] : null
|
|
|
|
const endBinding = endBindingId ? page.bindings[endBindingId] : null
|
|
|
|
|
|
|
|
let hit = false
|
|
|
|
|
|
|
|
if (startBinding && startBinding.toId === id) {
|
|
|
|
if (direction === 'center') {
|
|
|
|
hit = true
|
|
|
|
} else if (arrow.decorations?.start && endBinding) {
|
|
|
|
// The arrow is pointing to this shape at its start
|
|
|
|
hit = direction === 'left'
|
|
|
|
} else {
|
|
|
|
// The arrow is pointing away from this shape
|
|
|
|
hit = direction === 'right'
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hit) {
|
|
|
|
// This arrow is bound to this shape
|
|
|
|
if (includeArrows) linkedIds.add(arrow.id)
|
|
|
|
linkedIds.add(id)
|
|
|
|
|
|
|
|
if (endBinding) {
|
|
|
|
linkedIds.add(endBinding.toId)
|
|
|
|
idsToCheck.push(endBinding.toId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (endBinding && endBinding.toId === id) {
|
|
|
|
// This arrow is bound to this shape at its end
|
|
|
|
if (direction === 'center') {
|
|
|
|
hit = true
|
|
|
|
} else if (arrow.decorations?.end && startBinding) {
|
|
|
|
// The arrow is pointing to this shape
|
|
|
|
hit = direction === 'left'
|
|
|
|
} else {
|
|
|
|
// The arrow is pointing away from this shape
|
|
|
|
hit = direction === 'right'
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hit) {
|
|
|
|
if (includeArrows) linkedIds.add(arrow.id)
|
|
|
|
linkedIds.add(id)
|
|
|
|
|
|
|
|
if (startBinding) {
|
|
|
|
linkedIds.add(startBinding.toId)
|
|
|
|
idsToCheck.push(startBinding.toId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
(!startBinding || linkedIds.has(startBinding.toId)) &&
|
|
|
|
(!endBinding || linkedIds.has(endBinding.toId))
|
|
|
|
) {
|
|
|
|
arrows.delete(arrow)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return Array.from(linkedIds.values())
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getChildIndexAbove(data: TDSnapshot, id: string, pageId: string): number {
|
2021-09-05 09:51:21 +00:00
|
|
|
const page = data.document.pages[pageId]
|
2021-08-10 16:12:55 +00:00
|
|
|
const shape = page.shapes[id]
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
let siblings: TDShape[]
|
2021-08-12 13:39:41 +00:00
|
|
|
|
|
|
|
if (shape.parentId === page.id) {
|
|
|
|
siblings = Object.values(page.shapes)
|
|
|
|
.filter((shape) => shape.parentId === page.id)
|
|
|
|
.sort((a, b) => a.childIndex - b.childIndex)
|
|
|
|
} else {
|
|
|
|
const parent = page.shapes[shape.parentId]
|
|
|
|
if (!parent.children) throw Error('No children in parent!')
|
|
|
|
siblings = parent.children
|
|
|
|
.map((childId) => page.shapes[childId])
|
|
|
|
.sort((a, b) => a.childIndex - b.childIndex)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
const index = siblings.indexOf(shape)
|
|
|
|
|
|
|
|
const nextSibling = siblings[index + 1]
|
|
|
|
|
|
|
|
if (!nextSibling) return shape.childIndex + 1
|
|
|
|
|
2021-09-05 09:51:21 +00:00
|
|
|
return nextSibling.childIndex
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Mutations */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getBeforeShape<T extends TDShape>(shape: T, change: Partial<T>): Partial<T> {
|
2021-09-19 13:53:52 +00:00
|
|
|
return Object.fromEntries(
|
|
|
|
Object.keys(change).map((k) => [k, shape[k as keyof T]])
|
|
|
|
) as Partial<T>
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static mutateShapes<T extends TDShape>(
|
|
|
|
data: TDSnapshot,
|
2021-08-10 16:12:55 +00:00
|
|
|
ids: string[],
|
2021-09-06 11:07:15 +00:00
|
|
|
fn: (shape: T, i: number) => Partial<T> | void,
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId: string
|
2021-08-10 16:12:55 +00:00
|
|
|
): {
|
2021-08-11 13:34:17 +00:00
|
|
|
before: Record<string, Partial<T>>
|
|
|
|
after: Record<string, Partial<T>>
|
2021-11-16 16:01:29 +00:00
|
|
|
data: TDSnapshot
|
2021-08-10 16:12:55 +00:00
|
|
|
} {
|
2021-08-11 13:34:17 +00:00
|
|
|
const beforeShapes: Record<string, Partial<T>> = {}
|
|
|
|
const afterShapes: Record<string, Partial<T>> = {}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
ids.forEach((id, i) => {
|
2021-09-13 15:38:42 +00:00
|
|
|
const shape = TLDR.getShape<T>(data, id, pageId)
|
2021-11-16 16:01:29 +00:00
|
|
|
if (shape.isLocked) return
|
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
const change = fn(shape, i)
|
2021-09-06 11:07:15 +00:00
|
|
|
if (change) {
|
2021-09-19 13:53:52 +00:00
|
|
|
beforeShapes[id] = TLDR.getBeforeShape(shape, change)
|
2021-09-06 11:07:15 +00:00
|
|
|
afterShapes[id] = change
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
})
|
|
|
|
|
2021-09-01 11:18:50 +00:00
|
|
|
const dataWithMutations = Utils.deepMerge(data, {
|
|
|
|
document: {
|
|
|
|
pages: {
|
|
|
|
[data.appState.currentPageId]: {
|
|
|
|
shapes: afterShapes,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2021-11-16 16:01:29 +00:00
|
|
|
const dataWithBindingChanges = ids.reduce<TDSnapshot>((cTDSnapshot, id) => {
|
|
|
|
return TLDR.updateBindings(cTDSnapshot, id, beforeShapes, afterShapes, pageId)
|
2021-09-13 15:38:42 +00:00
|
|
|
}, dataWithMutations)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
before: beforeShapes,
|
|
|
|
after: afterShapes,
|
|
|
|
data: dataWithBindingChanges,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static createShapes(data: TDSnapshot, shapes: TDShape[], pageId: string): TldrawCommand {
|
|
|
|
const before: TldrawPatch = {
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
|
|
|
[pageId]: {
|
|
|
|
shapes: {
|
|
|
|
...Object.fromEntries(
|
|
|
|
shapes.flatMap((shape) => {
|
2021-11-16 16:01:29 +00:00
|
|
|
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, undefined]]
|
2021-08-16 21:52:03 +00:00
|
|
|
|
|
|
|
// If the shape is a child of another shape, also save that shape
|
|
|
|
if (shape.parentId !== pageId) {
|
2021-09-13 15:38:42 +00:00
|
|
|
const parent = TLDR.getShape(data, shape.parentId, pageId)
|
2021-08-16 21:52:03 +00:00
|
|
|
if (!parent.children) throw Error('No children in parent!')
|
|
|
|
results.push([parent.id, { children: parent.children }])
|
|
|
|
}
|
|
|
|
|
|
|
|
return results
|
|
|
|
})
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
2021-08-11 12:26:34 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const after: TldrawPatch = {
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
|
|
|
[pageId]: {
|
|
|
|
shapes: {
|
|
|
|
shapes: {
|
|
|
|
...Object.fromEntries(
|
|
|
|
shapes.flatMap((shape) => {
|
2021-11-16 16:01:29 +00:00
|
|
|
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, shape]]
|
2021-08-16 21:52:03 +00:00
|
|
|
|
|
|
|
// If the shape is a child of a different shape, update its parent
|
|
|
|
if (shape.parentId !== pageId) {
|
2021-09-13 15:38:42 +00:00
|
|
|
const parent = TLDR.getShape(data, shape.parentId, pageId)
|
2021-08-16 21:52:03 +00:00
|
|
|
if (!parent.children) throw Error('No children in parent!')
|
|
|
|
results.push([parent.id, { children: [...parent.children, shape.id] }])
|
|
|
|
}
|
|
|
|
|
|
|
|
return results
|
|
|
|
})
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2021-08-11 12:26:34 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-08-11 12:26:34 +00:00
|
|
|
return {
|
|
|
|
before,
|
|
|
|
after,
|
|
|
|
}
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-08-11 12:26:34 +00:00
|
|
|
static deleteShapes(
|
2021-11-16 16:01:29 +00:00
|
|
|
data: TDSnapshot,
|
|
|
|
shapes: TDShape[] | string[],
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId?: string
|
2021-11-16 16:01:29 +00:00
|
|
|
): TldrawCommand {
|
2021-08-17 21:38:37 +00:00
|
|
|
pageId = pageId ? pageId : data.appState.currentPageId
|
|
|
|
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = TLDR.getPage(data, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-08-11 12:26:34 +00:00
|
|
|
const shapeIds =
|
|
|
|
typeof shapes[0] === 'string'
|
|
|
|
? (shapes as string[])
|
2021-11-16 16:01:29 +00:00
|
|
|
: (shapes as TDShape[]).map((shape) => shape.id)
|
2021-08-11 12:26:34 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const before: TldrawPatch = {
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
|
|
|
[pageId]: {
|
|
|
|
shapes: {
|
|
|
|
// These are the shapes that we're going to delete
|
|
|
|
...Object.fromEntries(
|
|
|
|
shapeIds.flatMap((id) => {
|
|
|
|
const shape = page.shapes[id]
|
2021-11-16 16:01:29 +00:00
|
|
|
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, shape]]
|
2021-08-16 21:52:03 +00:00
|
|
|
|
|
|
|
// If the shape is a child of another shape, also add that shape
|
|
|
|
if (shape.parentId !== pageId) {
|
|
|
|
const parent = page.shapes[shape.parentId]
|
|
|
|
if (!parent.children) throw Error('No children in parent!')
|
|
|
|
results.push([parent.id, { children: parent.children }])
|
|
|
|
}
|
|
|
|
|
|
|
|
return results
|
|
|
|
})
|
|
|
|
),
|
|
|
|
},
|
|
|
|
bindings: {
|
|
|
|
// These are the bindings that we're going to delete
|
|
|
|
...Object.fromEntries(
|
|
|
|
Object.values(page.bindings)
|
|
|
|
.filter((binding) => {
|
|
|
|
return shapeIds.includes(binding.fromId) || shapeIds.includes(binding.toId)
|
|
|
|
})
|
|
|
|
.map((binding) => {
|
|
|
|
return [binding.id, binding]
|
|
|
|
})
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
2021-08-11 12:26:34 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const after: TldrawPatch = {
|
2021-08-16 21:52:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
|
|
|
[pageId]: {
|
|
|
|
shapes: {
|
|
|
|
...Object.fromEntries(
|
|
|
|
shapeIds.flatMap((id) => {
|
|
|
|
const shape = page.shapes[id]
|
2021-11-16 16:01:29 +00:00
|
|
|
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, undefined]]
|
2021-08-16 21:52:03 +00:00
|
|
|
|
|
|
|
// If the shape is a child of a different shape, update its parent
|
|
|
|
if (shape.parentId !== page.id) {
|
|
|
|
const parent = page.shapes[shape.parentId]
|
|
|
|
|
|
|
|
if (!parent.children) throw Error('No children in parent!')
|
|
|
|
|
|
|
|
results.push([
|
|
|
|
parent.id,
|
|
|
|
{ children: parent.children.filter((id) => id !== shape.id) },
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
return results
|
|
|
|
})
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
2021-08-11 12:26:34 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
before,
|
|
|
|
after,
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static onSessionComplete<T extends TDShape>(shape: T) {
|
|
|
|
const delta = TLDR.getShapeUtil(shape).onSessionComplete?.(shape)
|
2021-08-10 16:12:55 +00:00
|
|
|
if (!delta) return shape
|
2021-09-13 15:38:42 +00:00
|
|
|
return { ...shape, ...delta }
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static onChildrenChange<T extends TDShape>(data: TDSnapshot, shape: T, pageId: string) {
|
2021-08-12 13:39:41 +00:00
|
|
|
if (!shape.children) return
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const delta = TLDR.getShapeUtil(shape).onChildrenChange?.(
|
2021-08-10 16:12:55 +00:00
|
|
|
shape,
|
2021-09-13 15:38:42 +00:00
|
|
|
shape.children.map((id) => TLDR.getShape(data, id, pageId))
|
2021-08-10 16:12:55 +00:00
|
|
|
)
|
2021-09-13 15:38:42 +00:00
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
if (!delta) return shape
|
2021-09-13 15:38:42 +00:00
|
|
|
|
|
|
|
return { ...shape, ...delta }
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static onBindingChange<T extends TDShape>(shape: T, binding: TDBinding, otherShape: TDShape) {
|
|
|
|
const delta = TLDR.getShapeUtil(shape).onBindingChange?.(
|
2021-08-10 16:12:55 +00:00
|
|
|
shape,
|
|
|
|
binding,
|
|
|
|
otherShape,
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShapeUtil(otherShape).getBounds(otherShape),
|
|
|
|
TLDR.getShapeUtil(otherShape).getCenter(otherShape)
|
2021-08-10 16:12:55 +00:00
|
|
|
)
|
|
|
|
if (!delta) return shape
|
2021-09-13 15:38:42 +00:00
|
|
|
|
|
|
|
return { ...shape, ...delta }
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static transform<T extends TDShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
|
|
|
const delta = TLDR.getShapeUtil(shape).transform(shape, bounds, info)
|
2021-09-13 15:38:42 +00:00
|
|
|
if (!delta) return shape
|
|
|
|
return { ...shape, ...delta }
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static transformSingle<T extends TDShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
|
|
|
const delta = TLDR.getShapeUtil(shape).transformSingle(shape, bounds, info)
|
2021-09-13 15:38:42 +00:00
|
|
|
if (!delta) return shape
|
|
|
|
return { ...shape, ...delta }
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-19 13:53:52 +00:00
|
|
|
/**
|
|
|
|
* Rotate a shape around an origin point.
|
|
|
|
* @param shape a shape.
|
|
|
|
* @param center the shape's center in page space.
|
|
|
|
* @param origin the page point to rotate around.
|
|
|
|
* @param rotation the amount to rotate the shape.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
static getRotatedShapeMutation<T extends TDShape>(
|
2021-09-19 13:53:52 +00:00
|
|
|
shape: T, // in page space
|
|
|
|
center: number[], // in page space
|
|
|
|
origin: number[], // in page space (probably the center of common bounds)
|
|
|
|
delta: number // The shape's rotation delta
|
|
|
|
): Partial<T> | void {
|
|
|
|
// The shape's center relative to the shape's point
|
|
|
|
const relativeCenter = Vec.sub(center, shape.point)
|
|
|
|
|
|
|
|
// Rotate the center around the origin
|
|
|
|
const rotatedCenter = Vec.rotWith(center, origin, delta)
|
|
|
|
|
|
|
|
// Get the top left point relative to the rotated center
|
|
|
|
const nextPoint = Vec.round(Vec.sub(rotatedCenter, relativeCenter))
|
|
|
|
|
|
|
|
// If the shape has handles, we need to rotate the handles instead
|
|
|
|
// of rotating the shape. Shapes with handles should never be rotated,
|
|
|
|
// because that makes a lot of other things incredible difficult.
|
|
|
|
if (shape.handles !== undefined) {
|
2021-11-16 16:01:29 +00:00
|
|
|
const change = this.getShapeUtil(shape).onHandleChange?.(
|
2021-09-19 13:53:52 +00:00
|
|
|
// Base the change on a shape with the next point
|
|
|
|
{ ...shape, point: nextPoint },
|
|
|
|
Object.fromEntries(
|
|
|
|
Object.entries(shape.handles).map(([handleId, handle]) => {
|
|
|
|
// Rotate each handle's point around the shape's center
|
|
|
|
// (in relative shape space, as the handle's point will be).
|
|
|
|
const point = Vec.round(Vec.rotWith(handle.point, relativeCenter, delta))
|
|
|
|
return [handleId, { ...handle, point }]
|
|
|
|
})
|
|
|
|
) as T['handles'],
|
|
|
|
{ shiftKey: false }
|
|
|
|
)
|
|
|
|
|
|
|
|
return change
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the shape has no handles, move the shape to the new point
|
|
|
|
// and set the rotation.
|
|
|
|
|
|
|
|
// Clamp the next rotation between 0 and PI2
|
|
|
|
const nextRotation = Utils.clampRadians((shape.rotation || 0) + delta)
|
|
|
|
|
|
|
|
return {
|
|
|
|
point: nextPoint,
|
|
|
|
rotation: nextRotation,
|
|
|
|
} as Partial<T>
|
|
|
|
}
|
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Parents */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static updateParents(data: TDSnapshot, pageId: string, changedShapeIds: string[]): void {
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = TLDR.getPage(data, pageId)
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
if (changedShapeIds.length === 0) return
|
|
|
|
|
2021-09-13 15:38:42 +00:00
|
|
|
const { shapes } = TLDR.getPage(data, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
const parentToUpdateIds = Array.from(
|
|
|
|
new Set(changedShapeIds.map((id) => shapes[id].parentId).values())
|
2021-08-16 21:52:03 +00:00
|
|
|
).filter((id) => id !== page.id)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
for (const parentId of parentToUpdateIds) {
|
|
|
|
const parent = shapes[parentId]
|
|
|
|
|
|
|
|
if (!parent.children) {
|
|
|
|
throw Error('A shape is parented to a shape without a children array.')
|
|
|
|
}
|
|
|
|
|
2021-09-13 15:38:42 +00:00
|
|
|
TLDR.onChildrenChange(data, parent, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-13 15:38:42 +00:00
|
|
|
TLDR.updateParents(data, pageId, parentToUpdateIds)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getSelectedStyle(data: TDSnapshot, pageId: string): ShapeStyles | false {
|
2021-08-17 21:38:37 +00:00
|
|
|
const { currentStyle } = data.appState
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-08-17 21:38:37 +00:00
|
|
|
const page = data.document.pages[pageId]
|
|
|
|
const pageState = data.document.pageStates[pageId]
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
if (pageState.selectedIds.length === 0) {
|
|
|
|
return currentStyle
|
|
|
|
}
|
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
const shapeStyles = pageState.selectedIds.map((id) => page.shapes[id].style)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-08-12 13:39:41 +00:00
|
|
|
const commonStyle = {} as ShapeStyles
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
const overrides = new Set<string>([])
|
|
|
|
|
|
|
|
for (const shapeStyle of shapeStyles) {
|
2021-08-12 13:39:41 +00:00
|
|
|
const styles = Object.keys(currentStyle) as (keyof ShapeStyles)[]
|
|
|
|
styles.forEach((key) => {
|
2021-08-10 16:12:55 +00:00
|
|
|
if (overrides.has(key)) return
|
|
|
|
if (commonStyle[key] === undefined) {
|
2021-08-12 13:39:41 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
2021-08-10 16:12:55 +00:00
|
|
|
// @ts-ignore
|
|
|
|
commonStyle[key] = shapeStyle[key]
|
|
|
|
} else {
|
|
|
|
if (commonStyle[key] === shapeStyle[key]) return
|
2021-08-12 13:39:41 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
2021-08-10 16:12:55 +00:00
|
|
|
// @ts-ignore
|
|
|
|
commonStyle[key] = currentStyle[key]
|
|
|
|
overrides.add(key)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return commonStyle
|
|
|
|
}
|
|
|
|
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Bindings */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getBinding(data: TDSnapshot, id: string, pageId: string): TDBinding {
|
2021-09-13 15:38:42 +00:00
|
|
|
return TLDR.getPage(data, pageId).bindings[id]
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getBindings(data: TDSnapshot, pageId: string): TDBinding[] {
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = TLDR.getPage(data, pageId)
|
2021-08-10 16:12:55 +00:00
|
|
|
return Object.values(page.bindings)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getBindableShapeIds(data: TDSnapshot) {
|
2021-09-13 15:38:42 +00:00
|
|
|
return TLDR.getShapes(data, data.appState.currentPageId)
|
2021-11-16 16:01:29 +00:00
|
|
|
.filter((shape) => TLDR.getShapeUtil(shape).canBind)
|
2021-08-11 12:26:34 +00:00
|
|
|
.sort((a, b) => b.childIndex - a.childIndex)
|
|
|
|
.map((shape) => shape.id)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getBindingsWithShapeIds(data: TDSnapshot, ids: string[], pageId: string): TDBinding[] {
|
2021-08-10 16:12:55 +00:00
|
|
|
return Array.from(
|
|
|
|
new Set(
|
2021-09-13 15:38:42 +00:00
|
|
|
TLDR.getBindings(data, pageId).filter((binding) => {
|
2021-08-10 16:12:55 +00:00
|
|
|
return ids.includes(binding.toId) || ids.includes(binding.fromId)
|
|
|
|
})
|
|
|
|
).values()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getRelatedBindings(data: TDSnapshot, ids: string[], pageId: string): TDBinding[] {
|
2021-08-12 13:39:41 +00:00
|
|
|
const changedShapeIds = new Set(ids)
|
|
|
|
|
2021-09-13 15:38:42 +00:00
|
|
|
const page = TLDR.getPage(data, pageId)
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-08-12 13:39:41 +00:00
|
|
|
// Find all bindings that we need to update
|
2021-08-16 21:52:03 +00:00
|
|
|
const bindingsArr = Object.values(page.bindings)
|
2021-08-12 13:39:41 +00:00
|
|
|
|
|
|
|
// Start with bindings that are directly bound to our changed shapes
|
|
|
|
const bindingsToUpdate = new Set(
|
|
|
|
bindingsArr.filter(
|
|
|
|
(binding) => changedShapeIds.has(binding.toId) || changedShapeIds.has(binding.fromId)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
// Next, look for other bindings that effect the same shapes
|
|
|
|
let prevSize = bindingsToUpdate.size
|
|
|
|
let delta = -1
|
|
|
|
|
|
|
|
while (delta !== 0) {
|
|
|
|
bindingsToUpdate.forEach((binding) => {
|
|
|
|
const fromId = binding.fromId
|
|
|
|
|
|
|
|
for (const otherBinding of bindingsArr) {
|
|
|
|
if (otherBinding.fromId === fromId) {
|
|
|
|
bindingsToUpdate.add(otherBinding)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (otherBinding.toId === fromId) {
|
|
|
|
bindingsToUpdate.add(otherBinding)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Continue until we stop finding new bindings to update
|
|
|
|
delta = bindingsToUpdate.size - prevSize
|
|
|
|
|
|
|
|
prevSize = bindingsToUpdate.size
|
|
|
|
}
|
|
|
|
|
|
|
|
return Array.from(bindingsToUpdate.values())
|
|
|
|
}
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
static copyStringToClipboard = (string: string) => {
|
|
|
|
try {
|
|
|
|
navigator.clipboard.writeText(string)
|
|
|
|
} catch (e) {
|
|
|
|
const textarea = document.createElement('textarea')
|
|
|
|
textarea.setAttribute('position', 'fixed')
|
|
|
|
textarea.setAttribute('top', '0')
|
|
|
|
textarea.setAttribute('readonly', 'true')
|
|
|
|
textarea.setAttribute('contenteditable', 'true')
|
|
|
|
textarea.style.position = 'fixed'
|
|
|
|
textarea.value = string
|
|
|
|
document.body.appendChild(textarea)
|
|
|
|
textarea.focus()
|
|
|
|
textarea.select()
|
|
|
|
|
|
|
|
try {
|
|
|
|
const range = document.createRange()
|
|
|
|
range.selectNodeContents(textarea)
|
|
|
|
const sel = window.getSelection()
|
|
|
|
if (sel) {
|
|
|
|
sel.removeAllRanges()
|
|
|
|
sel.addRange(range)
|
|
|
|
textarea.setSelectionRange(0, textarea.value.length)
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
null // Could not copy to clipboard
|
|
|
|
} finally {
|
|
|
|
document.body.removeChild(textarea)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Groups */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static flattenShape = (data: TDSnapshot, shape: TDShape): TDShape[] => {
|
2021-09-02 12:51:39 +00:00
|
|
|
return [
|
|
|
|
shape,
|
|
|
|
...(shape.children ?? [])
|
|
|
|
.map((childId) => TLDR.getShape(data, childId, data.appState.currentPageId))
|
|
|
|
.sort((a, b) => a.childIndex - b.childIndex)
|
|
|
|
.flatMap((shape) => TLDR.flattenShape(data, shape)),
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static flattenPage = (data: TDSnapshot, pageId: string): TDShape[] => {
|
2021-09-02 12:51:39 +00:00
|
|
|
return Object.values(data.document.pages[pageId].shapes)
|
|
|
|
.sort((a, b) => a.childIndex - b.childIndex)
|
2021-11-16 16:01:29 +00:00
|
|
|
.reduce<TDShape[]>((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], [])
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static getTopChildIndex = (data: TDSnapshot, pageId: string): number => {
|
2021-09-04 15:00:13 +00:00
|
|
|
const shapes = TLDR.getShapes(data, pageId)
|
|
|
|
return shapes.length === 0
|
|
|
|
? 1
|
|
|
|
: shapes
|
|
|
|
.filter((shape) => shape.parentId === pageId)
|
|
|
|
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Text */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
|
|
|
static fixNewLines = /\r?\n|\r/g
|
|
|
|
static fixSpaces = / /g
|
|
|
|
|
|
|
|
static normalizeText(text: string) {
|
|
|
|
return text.replace(TLDR.fixNewLines, '\n').replace(TLDR.fixSpaces, '\u00a0')
|
|
|
|
}
|
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Assertions */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static assertShapeHasProperty<P extends keyof TDShape>(
|
|
|
|
shape: TDShape,
|
2021-08-10 16:12:55 +00:00
|
|
|
prop: P
|
|
|
|
): asserts shape is ShapesWithProp<P> {
|
|
|
|
if (shape[prop] === undefined) {
|
|
|
|
throw new Error()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|