Cleans up page / creates shape file
This commit is contained in:
parent
956c0717df
commit
97ae80baa0
2 changed files with 165 additions and 147 deletions
|
@ -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
112
components/canvas/shape.tsx
Normal 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,
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue