import { useSelector } from 'state' 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' /* 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. */ export default function Page(): JSX.Element { // 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) return ( boundsContain(viewport, shapeBounds) || boundsCollide(viewport, shapeBounds) ) }) const tree: Node[] = [] shapesToShow.forEach((shape) => addToTree(s.data, s.values.selectedIds, allowHovers, tree, shape) ) return tree }) return ( <> {shapeTree.map((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 ( <> {children.map((childNode) => ( ))} ) } interface TranslatedShapeProps { shape: Shape isEditing: boolean isHovered: boolean isSelected: boolean isCurrentParent: boolean } const TranslatedShape = memo( ({ shape, isEditing, isHovered, isSelected, isCurrentParent, }: TranslatedShapeProps) => { const rGroup = useRef(null) const events = useShapeEvents(shape.id, isCurrentParent, rGroup) const utils = getShapeUtils(shape) const center = utils.getCenter(shape) const rotation = shape.rotation * (180 / Math.PI) const transform = ` rotate(${rotation}, ${center}) translate(${shape.point}) ` return ( {isEditing && shape.type === ShapeType.Text ? ( ) : ( )} ) }, 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(null) return getShapeUtils(shape).render(shape, { ref, isEditing: true, isHovered: false, isSelected: false, isCurrentParent: false, }) }