2021-11-18 13:09:18 +00:00
|
|
|
import * as React from 'react'
|
2021-12-25 17:06:33 +00:00
|
|
|
import type {
|
|
|
|
IShapeTreeNode,
|
|
|
|
TLPage,
|
|
|
|
TLPageState,
|
|
|
|
TLShape,
|
|
|
|
TLBinding,
|
|
|
|
TLBounds,
|
|
|
|
TLAssets,
|
|
|
|
} from '~types'
|
2021-11-18 13:09:18 +00:00
|
|
|
import { Utils } from '~utils'
|
|
|
|
import { Vec } from '@tldraw/vec'
|
|
|
|
import { useTLContext } from '~hooks'
|
|
|
|
|
|
|
|
function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
|
|
|
shape: T,
|
|
|
|
branch: IShapeTreeNode<T, M>[],
|
|
|
|
shapes: TLPage<T, TLBinding>['shapes'],
|
|
|
|
pageState: TLPageState & {
|
|
|
|
bindingTargetId?: string | null
|
|
|
|
},
|
2021-12-25 17:06:33 +00:00
|
|
|
assets: TLAssets,
|
2021-11-18 13:09:18 +00:00
|
|
|
isChildOfGhost = false,
|
|
|
|
isChildOfSelected = false,
|
|
|
|
meta?: M
|
|
|
|
) {
|
|
|
|
// Create a node for this shape
|
|
|
|
const node: IShapeTreeNode<T, M> = {
|
|
|
|
shape,
|
2021-12-25 17:06:33 +00:00
|
|
|
asset: shape.assetId ? assets[shape.assetId] : undefined,
|
2021-11-18 13:09:18 +00:00
|
|
|
meta: meta as any,
|
|
|
|
isChildOfSelected,
|
|
|
|
isGhost: shape.isGhost || isChildOfGhost,
|
|
|
|
isEditing: pageState.editingId === shape.id,
|
|
|
|
isBinding: pageState.bindingTargetId === shape.id,
|
|
|
|
isSelected: pageState.selectedIds.includes(shape.id),
|
|
|
|
isHovered:
|
|
|
|
// The shape is hovered..
|
|
|
|
pageState.hoveredId === shape.id ||
|
|
|
|
// Or the shape has children and...
|
|
|
|
(shape.children !== undefined &&
|
|
|
|
// One of the children is hovered
|
|
|
|
((pageState.hoveredId && shape.children.includes(pageState.hoveredId)) ||
|
|
|
|
// Or one of the children is selected
|
|
|
|
shape.children.some((childId) => pageState.selectedIds.includes(childId)))),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the node to the branch
|
|
|
|
branch.push(node)
|
|
|
|
|
|
|
|
// If the shape has children, add nodes for each child to the node's children array
|
|
|
|
if (shape.children) {
|
|
|
|
node.children = []
|
|
|
|
|
|
|
|
shape.children
|
|
|
|
.map((id) => shapes[id])
|
2022-03-02 14:21:14 +00:00
|
|
|
.filter(Boolean) // TODO: Find cases where shapes would be missing.
|
2021-11-18 13:09:18 +00:00
|
|
|
.sort((a, b) => a.childIndex - b.childIndex)
|
|
|
|
.forEach((childShape) =>
|
|
|
|
addToShapeTree(
|
|
|
|
childShape,
|
|
|
|
node.children!,
|
|
|
|
shapes,
|
|
|
|
pageState,
|
2021-12-25 17:06:33 +00:00
|
|
|
assets,
|
2021-11-18 13:09:18 +00:00
|
|
|
node.isGhost,
|
|
|
|
node.isSelected || node.isChildOfSelected,
|
|
|
|
meta
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) {
|
|
|
|
return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
|
|
|
page: TLPage<T, TLBinding>,
|
|
|
|
pageState: TLPageState,
|
2021-12-25 17:06:33 +00:00
|
|
|
assets: TLAssets,
|
2021-11-18 13:09:18 +00:00
|
|
|
meta?: M
|
|
|
|
) {
|
|
|
|
const { callbacks, shapeUtils, bounds } = useTLContext()
|
|
|
|
|
|
|
|
const rTimeout = React.useRef<unknown>()
|
2022-02-11 21:35:24 +00:00
|
|
|
const rPreviousCount = React.useRef(-1)
|
2021-11-18 13:09:18 +00:00
|
|
|
const rShapesIdsToRender = React.useRef(new Set<string>())
|
|
|
|
const rShapesToRender = React.useRef(new Set<TLShape>())
|
|
|
|
|
|
|
|
const { selectedIds, camera } = pageState
|
|
|
|
|
|
|
|
// Filter the page's shapes down to only those that:
|
|
|
|
// - are the direct child of the page
|
|
|
|
// - collide with or are contained by the viewport
|
|
|
|
// - OR are selected
|
|
|
|
|
|
|
|
const [minX, minY] = Vec.sub(Vec.div([0, 0], camera.zoom), camera.point)
|
|
|
|
const [maxX, maxY] = Vec.sub(Vec.div([bounds.width, bounds.height], camera.zoom), camera.point)
|
|
|
|
const viewport = {
|
|
|
|
minX,
|
|
|
|
minY,
|
|
|
|
maxX,
|
|
|
|
maxY,
|
|
|
|
height: maxX - minX,
|
|
|
|
width: maxY - minY,
|
|
|
|
}
|
|
|
|
|
|
|
|
const shapesToRender = rShapesToRender.current
|
|
|
|
const shapesIdsToRender = rShapesIdsToRender.current
|
|
|
|
|
|
|
|
shapesToRender.clear()
|
|
|
|
shapesIdsToRender.clear()
|
|
|
|
|
2022-02-11 21:35:24 +00:00
|
|
|
const allShapes = Object.values(page.shapes)
|
|
|
|
|
|
|
|
allShapes
|
2021-11-18 13:09:18 +00:00
|
|
|
.filter(
|
|
|
|
(shape) =>
|
|
|
|
// Always render shapes that are flagged as stateful
|
|
|
|
shapeUtils[shape.type as T['type']].isStateful ||
|
|
|
|
// Always render selected shapes (this preserves certain drag interactions)
|
|
|
|
selectedIds.includes(shape.id) ||
|
|
|
|
// Otherwise, only render shapes that are in view
|
|
|
|
shapeIsInViewport(shapeUtils[shape.type as T['type']].getBounds(shape as any), viewport)
|
|
|
|
)
|
|
|
|
.forEach((shape) => {
|
|
|
|
// If the shape's parent is the page, add it to our sets of shapes to render
|
|
|
|
if (shape.parentId === page.id) {
|
|
|
|
shapesIdsToRender.add(shape.id)
|
|
|
|
shapesToRender.add(shape)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the shape's parent is a different shape (e.g. a group),
|
|
|
|
// add the parent to the sets of shapes to render. The parent's
|
|
|
|
// children will all be rendered.
|
2022-05-18 11:45:04 +00:00
|
|
|
const parent = page.shapes[shape.parentId]
|
|
|
|
|
|
|
|
if (parent === undefined) {
|
|
|
|
throw Error(`A shape (${shape.id}) has a parent (${shape.parentId}) that does not exist!`)
|
|
|
|
} else {
|
|
|
|
shapesIdsToRender.add(parent.id)
|
|
|
|
shapesToRender.add(parent)
|
|
|
|
}
|
2021-11-18 13:09:18 +00:00
|
|
|
})
|
|
|
|
|
2022-02-11 21:35:24 +00:00
|
|
|
// Call onRenderCountChange callback when number of rendering shapes changes
|
2021-11-18 13:09:18 +00:00
|
|
|
|
|
|
|
if (shapesToRender.size !== rPreviousCount.current) {
|
|
|
|
// Use a timeout to clear call stack, in case the onChange handler
|
|
|
|
// produces a new state change, which could cause nested state
|
|
|
|
// changes, which is bad in React.
|
|
|
|
if (rTimeout.current) {
|
|
|
|
clearTimeout(rTimeout.current as number)
|
|
|
|
}
|
|
|
|
rTimeout.current = requestAnimationFrame(() => {
|
|
|
|
callbacks.onRenderCountChange?.(Array.from(shapesIdsToRender.values()))
|
|
|
|
})
|
|
|
|
rPreviousCount.current = shapesToRender.size
|
|
|
|
}
|
|
|
|
|
|
|
|
const bindingTargetId = pageState.bindingId ? page.bindings[pageState.bindingId].toId : undefined
|
|
|
|
|
|
|
|
// Populate the shape tree
|
|
|
|
|
|
|
|
const tree: IShapeTreeNode<T, M>[] = []
|
|
|
|
|
2022-05-18 11:45:04 +00:00
|
|
|
shapesToRender.forEach((shape) => {
|
|
|
|
if (shape === undefined) {
|
|
|
|
throw Error('Rendered shapes included a missing shape')
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:09:18 +00:00
|
|
|
addToShapeTree(
|
|
|
|
shape,
|
|
|
|
tree,
|
|
|
|
page.shapes,
|
|
|
|
{ ...pageState, bindingTargetId },
|
2021-12-25 17:06:33 +00:00
|
|
|
assets,
|
2021-11-18 13:09:18 +00:00
|
|
|
shape.isGhost,
|
|
|
|
false,
|
|
|
|
meta
|
|
|
|
)
|
2022-05-18 11:45:04 +00:00
|
|
|
})
|
2021-11-18 13:09:18 +00:00
|
|
|
|
|
|
|
tree.sort((a, b) => a.shape.childIndex - b.shape.childIndex)
|
|
|
|
|
|
|
|
return tree
|
|
|
|
}
|