123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- import React, { useRef, memo, useEffect, useState } from 'react'
- import state, { useSelector } from 'state'
- import styled from 'styles'
- import { getShapeUtils } from 'state/shape-utils'
- import { deepCompareArrays, getPage, getShape } from 'utils'
- import useShapeEvents from 'hooks/useShapeEvents'
- import vec from 'utils/vec'
- import { getShapeStyle } from 'state/shape-styles'
- import useShapeDef from 'hooks/useShape'
-
- interface ShapeProps {
- id: string
- isSelecting: boolean
- }
-
- function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
- const rGroup = useRef<SVGGElement>(null)
-
- const isHidden = useSelector((s) => {
- const shape = getShape(s.data, id)
- return shape?.isHidden || false
- })
-
- const children = useSelector((s) => {
- const shape = getShape(s.data, id)
- return shape?.children || []
- }, deepCompareArrays)
-
- const strokeWidth = useSelector((s) => {
- const shape = getShape(s.data, id)
- const style = getShapeStyle(shape?.style)
- return +style.strokeWidth
- })
-
- const shapeUtils = useSelector((s) => {
- const shape = getShape(s.data, id)
- return getShapeUtils(shape)
- })
-
- const transform = useSelector((s) => {
- const shape = getShape(s.data, id)
- const center = getShapeUtils(shape).getCenter(shape)
- const rotation = shape.rotation * (180 / Math.PI)
- const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0]
-
- return `
- translate(${vec.neg(parentPoint)})
- rotate(${rotation}, ${center})
- translate(${shape.point})
- `
- })
-
- const events = useShapeEvents(id, shapeUtils?.isParent, rGroup)
-
- const hasShape = useMissingShapeTest(id)
-
- if (!hasShape) return null
-
- const isParent = shapeUtils.isParent
-
- const isForeignObject = shapeUtils.isForeignObject
-
- return (
- <StyledGroup
- id={id + '-group'}
- ref={rGroup}
- transform={transform}
- {...events}
- >
- {isSelecting &&
- (isForeignObject ? (
- <ForeignObjectHover id={id} />
- ) : (
- <EventSoak
- as="use"
- href={'#' + id}
- strokeWidth={strokeWidth + 8}
- variant={shapeUtils.canStyleFill ? 'filled' : 'hollow'}
- />
- ))}
-
- {!isHidden &&
- (isForeignObject ? (
- <ForeignObjectRender id={id} />
- ) : (
- <RealShape id={id} isParent={isParent} strokeWidth={strokeWidth} />
- ))}
-
- {isParent &&
- children.map((shapeId) => (
- <Shape key={shapeId} id={shapeId} isSelecting={isSelecting} />
- ))}
- </StyledGroup>
- )
- }
-
- interface RealShapeProps {
- id: string
- isParent: boolean
- strokeWidth: number
- }
-
- const RealShape = memo(function RealShape({
- id,
- isParent,
- strokeWidth,
- }: RealShapeProps) {
- return (
- <StyledShape
- as="use"
- data-shy={isParent}
- href={'#' + id}
- strokeWidth={strokeWidth}
- />
- )
- })
-
- const ForeignObjectHover = memo(function ForeignObjectHover({
- id,
- }: {
- id: string
- }) {
- const size = useSelector((s) => {
- const shape = getPage(s.data).shapes[id]
- const bounds = getShapeUtils(shape).getBounds(shape)
-
- return [bounds.width, bounds.height]
- }, deepCompareArrays)
-
- return (
- <EventSoak
- as="rect"
- width={size[0]}
- height={size[1]}
- strokeWidth={1.5}
- variant={'ghost'}
- />
- )
- })
-
- const ForeignObjectRender = memo(function ForeignObjectRender({
- id,
- }: {
- id: string
- }) {
- const shape = useShapeDef(id)
-
- const rFocusable = useRef<HTMLTextAreaElement>(null)
-
- const isEditing = useSelector((s) => s.data.editingId === id)
-
- const shapeUtils = getShapeUtils(shape)
-
- useEffect(() => {
- if (isEditing) {
- setTimeout(() => {
- const elm = rFocusable.current
- if (!elm) return
- elm.focus()
- }, 0)
- }
- }, [isEditing])
-
- return shapeUtils.render(shape, { isEditing, ref: rFocusable })
- })
-
- const StyledShape = styled('path', {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- pointerEvents: 'none',
- })
-
- const EventSoak = styled('use', {
- opacity: 0,
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- variants: {
- variant: {
- ghost: {
- pointerEvents: 'all',
- filter: 'none',
- opacity: 0,
- },
- hollow: {
- pointerEvents: 'stroke',
- },
- filled: {
- pointerEvents: 'all',
- },
- },
- },
- })
-
- const StyledGroup = styled('g', {
- outline: 'none',
- })
-
- export default memo(Shape)
-
- function useMissingShapeTest(id: string) {
- const [isShape, setIsShape] = useState(true)
-
- useEffect(() => {
- return state.onUpdate((s) => {
- if (isShape && !getShape(s.data, id)) {
- setIsShape(false)
- }
- })
- }, [isShape, id])
-
- return isShape
- }
|