Cleans up page / creates shape file

This commit is contained in:
Steve Ruiz 2021-07-09 21:04:41 +01:00
parent 956c0717df
commit 97ae80baa0
2 changed files with 165 additions and 147 deletions

View file

@ -1,37 +1,46 @@
import { useSelector } from 'state'
import tld from 'utils/tld'
import useShapeEvents from 'hooks/useShapeEvents'
import { Data, Shape, ShapeType, TextShape } from 'types'
import { Data, Shape, ShapeType } from 'types'
import { getShapeUtils } from 'state/shape-utils'
import { boundsCollide, boundsContain, shallowEqual } from 'utils'
import { memo, useRef } from 'react'
import { boundsCollide, boundsContain } from 'utils'
import ShapeComponent from './shape'
/*
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.
On each state change, populate a tree structure with all of
the shapes that we need to render..
*/
interface Node {
shape: Shape
children: Node[]
isEditing: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
}
export default function Page(): JSX.Element {
// Get the shapes that fit into the current window
// Get a tree of shapes to render
const shapeTree = useSelector((s) => {
const allowHovers = s.isInAny('selecting', 'text', 'editingShape')
// Get the shapes that fit into the current viewport
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 (
shape.type === ShapeType.Ray ||
shape.type === ShapeType.Line ||
boundsContain(viewport, shapeBounds) ||
boundsCollide(viewport, shapeBounds)
)
})
// Should we allow shapes to be hovered?
const allowHovers = s.isInAny('selecting', 'text', 'editingShape')
// Populate the shape tree
const tree: Node[] = []
shapesToShow.forEach((shape) =>
@ -50,15 +59,39 @@ export default function Page(): JSX.Element {
)
}
type Node = {
shape: Shape
children: Node[]
isEditing: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
interface ShapeNodeProps {
node: Node
parentPoint?: number[]
}
const ShapeNode = ({
node: { shape, children, isEditing, isHovered, isSelected, isCurrentParent },
}: ShapeNodeProps) => {
return (
<>
<ShapeComponent
shape={shape}
isEditing={isEditing}
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
/>
{children.map((childNode) => (
<ShapeNode key={childNode.shape.id} node={childNode} />
))}
</>
)
}
/**
* Populate the shape tree. This helper is recursive and only one call is needed.
*
* ### Example
*
*```ts
* addDataToTree(data, selectedIds, allowHovers, branch, shape)
*```
*/
function addToTree(
data: Data,
selectedIds: string[],
@ -86,130 +119,3 @@ function addToTree(
})
}
}
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} />
))}
</>
)
}
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)
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 (
<g
ref={rGroup}
id={shape.id}
transform={transform}
filter={isHovered ? 'url(#expand)' : 'none'}
{...events}
>
{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,
})
}

112
components/canvas/shape.tsx Normal file
View file

@ -0,0 +1,112 @@
import useShapeEvents from 'hooks/useShapeEvents'
import { Shape as _Shape, ShapeType, TextShape } from 'types'
import { getShapeUtils } from 'state/shape-utils'
import { shallowEqual } from 'utils'
import { memo, useRef } from 'react'
interface ShapeProps {
shape: _Shape
isEditing: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
}
const Shape = memo(
({
shape,
isEditing,
isHovered,
isSelected,
isCurrentParent,
}: ShapeProps) => {
const rGroup = useRef<SVGGElement>(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 (
<g
ref={rGroup}
id={shape.id}
transform={transform}
filter={isHovered ? 'url(#expand)' : 'none'}
{...events}
>
{isEditing && shape.type === ShapeType.Text ? (
<EditingTextShape shape={shape} />
) : (
<RenderedShape
shape={shape}
isEditing={isEditing}
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
/>
)}
</g>
)
},
shallowEqual
)
export default Shape
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,
})
}