tldraw/components/canvas/page.tsx

216 lines
4.9 KiB
TypeScript
Raw Normal View History

2021-06-21 21:35:28 +00:00
import { useSelector } from 'state'
2021-07-09 16:15:27 +00:00
import tld from 'utils/tld'
import useShapeEvents from 'hooks/useShapeEvents'
import { Data, Shape, ShapeType, TextShape } from 'types'
import { getShapeUtils } from 'state/shape-utils'
import { boundsCollide, boundsContain, shallowEqual } from 'utils'
import { memo, useRef } from 'react'
2021-05-09 21:22:25 +00:00
/*
On each state change, compare node ids of all shapes
on the current page. Kind of expensive but only happens
here; and still cheaper than any other pattern I've found.
*/
2021-06-21 21:35:28 +00:00
export default function Page(): JSX.Element {
2021-07-09 16:15:27 +00:00
// Get the shapes that fit into the current window
const shapeTree = useSelector((s) => {
const allowHovers = s.isInAny('selecting', 'text', 'editingShape')
const viewport = tld.getViewport(s.data)
const shapesToShow = s.values.currentShapes.filter((shape) => {
if (shape.type === ShapeType.Ray || shape.type === ShapeType.Line) {
return true
}
const shapeBounds = getShapeUtils(shape).getBounds(shape)
2021-05-28 14:37:23 +00:00
2021-07-09 16:15:27 +00:00
return (
boundsContain(viewport, shapeBounds) ||
boundsCollide(viewport, shapeBounds)
)
})
2021-07-09 16:15:27 +00:00
const tree: Node[] = []
shapesToShow.forEach((shape) =>
addToTree(s.data, s.values.selectedIds, allowHovers, tree, shape)
)
return tree
})
2021-05-09 21:22:25 +00:00
return (
2021-07-09 16:15:27 +00:00
<>
{shapeTree.map((node) => (
<ShapeNode key={node.shape.id} node={node} />
))}
</>
)
}
type Node = {
shape: Shape
children: Node[]
isEditing: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
}
function addToTree(
data: Data,
selectedIds: string[],
allowHovers: boolean,
branch: Node[],
shape: Shape
): void {
const node = {
shape,
children: [],
isHovered: data.hoveredId === shape.id,
isCurrentParent: data.currentParentId === shape.id,
isEditing: data.editingId === shape.id,
isSelected: selectedIds.includes(shape.id),
}
branch.push(node)
if (shape.children) {
shape.children
.map((id) => tld.getShape(data, id))
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((shape) => {
addToTree(data, selectedIds, allowHovers, node.children, shape)
})
}
}
const ShapeNode = ({
node: { shape, children, isEditing, isHovered, isSelected, isCurrentParent },
}: {
node: Node
parentPoint?: number[]
}) => {
return (
<>
<TranslatedShape
shape={shape}
isEditing={isEditing}
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
/>
{children.map((childNode) => (
<ShapeNode key={childNode.shape.id} node={childNode} />
2021-05-09 21:22:25 +00:00
))}
2021-07-09 16:15:27 +00:00
</>
2021-05-09 21:22:25 +00:00
)
}
2021-07-09 16:15:27 +00:00
interface TranslatedShapeProps {
shape: Shape
isEditing: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
}
const TranslatedShape = memo(
({
shape,
isEditing,
isHovered,
isSelected,
isCurrentParent,
}: TranslatedShapeProps) => {
const rGroup = useRef<SVGGElement>(null)
const events = useShapeEvents(shape.id, isCurrentParent, rGroup)
2021-07-09 19:43:18 +00:00
const utils = getShapeUtils(shape)
2021-07-09 16:15:27 +00:00
2021-07-09 19:43:18 +00:00
const center = utils.getCenter(shape)
2021-07-09 16:15:27 +00:00
const rotation = shape.rotation * (180 / Math.PI)
const transform = `
rotate(${rotation}, ${center})
translate(${shape.point})
`
return (
2021-07-09 19:43:18 +00:00
<g
ref={rGroup}
id={shape.id}
transform={transform}
filter={isHovered ? 'url(#expand)' : 'none'}
{...events}
>
2021-07-09 16:15:27 +00:00
{isEditing && shape.type === ShapeType.Text ? (
<EditingTextShape shape={shape} />
) : (
<RenderedShape
shape={shape}
isEditing={isEditing}
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
/>
)}
</g>
)
},
shallowEqual
)
interface RenderedShapeProps {
shape: Shape
isEditing: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
}
const RenderedShape = memo(
function RenderedShape({
shape,
isEditing,
isHovered,
isSelected,
isCurrentParent,
}: RenderedShapeProps) {
return getShapeUtils(shape).render(shape, {
isEditing,
isHovered,
isSelected,
isCurrentParent,
})
},
(prev, next) => {
if (
prev.isEditing !== next.isEditing ||
prev.isHovered !== next.isHovered ||
prev.isSelected !== next.isSelected ||
prev.isCurrentParent !== next.isCurrentParent
) {
return false
}
if (next.shape !== prev.shape) {
return !getShapeUtils(next.shape).shouldRender(next.shape, prev.shape)
}
return true
}
)
function EditingTextShape({ shape }: { shape: TextShape }) {
const ref = useRef<HTMLTextAreaElement>(null)
return getShapeUtils(shape).render(shape, {
ref,
isEditing: true,
isHovered: false,
isSelected: false,
isCurrentParent: false,
})
}