Adjust pointer events for shapes

This commit is contained in:
Steve Ruiz 2021-07-09 20:43:18 +01:00
parent 5d12a2fd54
commit 552c8457ef
8 changed files with 80 additions and 296 deletions

View file

@ -19,7 +19,7 @@ function ExpandDef() {
const zoom = useSelector((s) => tld.getCurrentCamera(s.data).zoom) const zoom = useSelector((s) => tld.getCurrentCamera(s.data).zoom)
return ( return (
<filter id="expand"> <filter id="expand">
<feMorphology operator="dilate" radius={1 / zoom} /> <feMorphology operator="dilate" radius={0.5 / zoom} />
</filter> </filter>
) )
} }

View file

@ -127,8 +127,9 @@ const TranslatedShape = memo(
}: TranslatedShapeProps) => { }: TranslatedShapeProps) => {
const rGroup = useRef<SVGGElement>(null) const rGroup = useRef<SVGGElement>(null)
const events = useShapeEvents(shape.id, isCurrentParent, rGroup) const events = useShapeEvents(shape.id, isCurrentParent, rGroup)
const utils = getShapeUtils(shape)
const center = getShapeUtils(shape).getCenter(shape) const center = utils.getCenter(shape)
const rotation = shape.rotation * (180 / Math.PI) const rotation = shape.rotation * (180 / Math.PI)
const transform = ` const transform = `
rotate(${rotation}, ${center}) rotate(${rotation}, ${center})
@ -136,7 +137,13 @@ const TranslatedShape = memo(
` `
return ( return (
<g ref={rGroup} transform={transform} pointerEvents="all" {...events}> <g
ref={rGroup}
id={shape.id}
transform={transform}
filter={isHovered ? 'url(#expand)' : 'none'}
{...events}
>
{isEditing && shape.type === ShapeType.Text ? ( {isEditing && shape.type === ShapeType.Text ? (
<EditingTextShape shape={shape} /> <EditingTextShape shape={shape} />
) : ( ) : (

View file

@ -1,209 +0,0 @@
import React, { useRef, memo, useEffect } from 'react'
import state, { useSelector } from 'state'
import styled from 'styles'
import { getShapeUtils } from 'state/shape-utils'
import { deepCompareArrays } from 'utils'
import tld from 'utils/tld'
import useShapeEvents from 'hooks/useShapeEvents'
import useShape from 'hooks/useShape'
import vec from 'utils/vec'
import { getShapeStyle } from 'state/shape-styles'
import { Shape as _Shape } from 'types'
interface ShapeProps {
shape: _Shape
parent?: _Shape
}
function Shape({ shape, parent }: ShapeProps): JSX.Element {
const rGroup = useRef<SVGGElement>(null)
const { id, isHidden, children } = shape
const style = getShapeStyle(shape.style)
const { strokeWidth } = style
const center = getShapeUtils(shape).getCenter(shape)
const rotation = shape.rotation * (180 / Math.PI)
const parentPoint = parent?.point || [0, 0]
const transform = `
translate(${vec.neg(parentPoint)})
rotate(${rotation}, ${center})
translate(${shape.point})
`
const isCurrentParent = false
const events = useShapeEvents(shape.id, isCurrentParent, rGroup)
// From here on, not reactive—if we're here, we can trust that the
// shape in state is a shape with changes that we need to render.
const { isParent, isForeignObject, canStyleFill } = getShapeUtils(shape)
return (
<StyledGroup
id={id + '-group'}
ref={rGroup}
transform={transform}
isCurrentParent={isCurrentParent}
{...events}
>
{isForeignObject ? (
<ForeignObjectHover id={id} />
) : (
<EventSoak
as="use"
href={'#' + id}
strokeWidth={strokeWidth + 8}
variant={canStyleFill ? 'filled' : 'hollow'}
/>
)}
{!isHidden &&
(isForeignObject ? (
<ForeignObjectRender id={id} />
) : (
<RealShape
id={id}
isParent={isParent}
shape={shape}
strokeWidth={strokeWidth}
/>
))}
{isParent &&
children.map((shapeId) => (
<Shape
key={shapeId}
shape={tld.getShape(state.data, shapeId)}
parent={shape}
/>
))}
</StyledGroup>
)
}
export default memo(Shape)
// function Def({ id }: { id: string }) {
// const shape = useShape(id)
// if (!shape) return null
// return getShapeUtils(shape).render(shape, { isEditing: false })
// }
interface RealShapeProps {
id: string
isParent: boolean
strokeWidth: number
shape: _Shape
}
const RealShape = memo(
function RealShape({ shape }: RealShapeProps) {
return getShapeUtils(shape).render(shape, { isEditing: false })
},
(prev, next) => {
return (
prev.shape &&
next.shape &&
next.shape !== prev.shape &&
getShapeUtils(next.shape).shouldRender(next.shape, prev.shape)
)
}
)
const ForeignObjectHover = memo(function ForeignObjectHover({
id,
}: {
id: string
}) {
const size = useSelector((s) => {
const shape = tld.getPage(s.data).shapes[id]
if (shape === undefined) return [0, 0]
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 = useShape(id)
const rFocusable = useRef<HTMLTextAreaElement>(null)
const isEditing = useSelector((s) => s.data.editingId === id)
useEffect(() => {
if (isEditing) {
setTimeout(() => {
const elm = rFocusable.current
if (!elm) return
elm.focus()
}, 0)
}
}, [isEditing])
if (shape === undefined) return null
return getShapeUtils(shape).render(shape, { isEditing, ref: rFocusable })
})
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',
'& > *[data-shy=true]': {
opacity: 0,
},
'&:hover': {
'& > *[data-shy=true]': {
opacity: 1,
},
},
variants: {
isCurrentParent: {
true: {
'& > *[data-shy=true]': {
opacity: 1,
},
},
},
},
})

View file

@ -7,7 +7,6 @@ import {
getBoundsFromPoints, getBoundsFromPoints,
translateBounds, translateBounds,
pointInBounds, pointInBounds,
pointInCircle,
circleFromThreePoints, circleFromThreePoints,
isAngleBetween, isAngleBetween,
getPerfectDashProps, getPerfectDashProps,
@ -102,8 +101,8 @@ const arrow = registerShapeUtils<ArrowShape>({
return shape.handles !== prev.handles || shape.style !== prev.style return shape.handles !== prev.handles || shape.style !== prev.style
}, },
render(shape, { isHovered }) { render(shape) {
const { id, bend, handles, style } = shape const { bend, handles, style } = shape
const { start, end, bend: _bend } = handles const { start, end, bend: _bend } = handles
const isStraightLine = const isStraightLine =
@ -146,11 +145,11 @@ const arrow = registerShapeUtils<ArrowShape>({
<path <path
d={path} d={path}
fill="none" fill="none"
stroke="transparent"
strokeWidth={Math.max(8, strokeWidth * 2)} strokeWidth={Math.max(8, strokeWidth * 2)}
strokeDasharray="none" strokeDasharray="none"
strokeDashoffset="none" strokeDashoffset="none"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round"
/> />
<path <path
d={path} d={path}
@ -160,6 +159,7 @@ const arrow = registerShapeUtils<ArrowShape>({
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset} strokeDashoffset={strokeDashoffset}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round"
/> />
</> </>
) )
@ -206,6 +206,7 @@ const arrow = registerShapeUtils<ArrowShape>({
strokeDasharray="none" strokeDasharray="none"
strokeDashoffset="none" strokeDashoffset="none"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round"
/> />
<path <path
d={path} d={path}
@ -215,6 +216,7 @@ const arrow = registerShapeUtils<ArrowShape>({
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset} strokeDashoffset={strokeDashoffset}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round"
/> />
</> </>
) )
@ -223,7 +225,7 @@ const arrow = registerShapeUtils<ArrowShape>({
const sw = strokeWidth * 1.618 const sw = strokeWidth * 1.618
return ( return (
<g id={id} filter={isHovered ? 'url(#expand)' : 'none'}> <g pointerEvents="all">
{shaftPath} {shaftPath}
{shape.decorations.start === Decoration.Arrow && ( {shape.decorations.start === Decoration.Arrow && (
<path <path
@ -235,6 +237,7 @@ const arrow = registerShapeUtils<ArrowShape>({
strokeDasharray="none" strokeDasharray="none"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
pointerEvents="stroke"
/> />
)} )}
{shape.decorations.end === Decoration.Arrow && ( {shape.decorations.end === Decoration.Arrow && (
@ -247,6 +250,7 @@ const arrow = registerShapeUtils<ArrowShape>({
strokeDasharray="none" strokeDasharray="none"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
pointerEvents="stroke"
/> />
)} )}
</g> </g>
@ -308,21 +312,8 @@ const arrow = registerShapeUtils<ArrowShape>({
return vec.add(shape.point, vec.med(start.point, end.point)) return vec.add(shape.point, vec.med(start.point, end.point))
}, },
hitTest(shape, point) { hitTest() {
const { start, end } = shape.handles return true
if (shape.bend === 0) {
return (
vec.distanceToLineSegment(
start.point,
end.point,
vec.sub(point, shape.point)
) < 4
)
}
const [cx, cy, r] = getCtp(shape)
return !pointInCircle(point, vec.add(shape.point, [cx, cy]), r - 4)
}, },
hitTestBounds(this, shape, brushBounds) { hitTestBounds(this, shape, brushBounds) {

View file

@ -41,13 +41,14 @@ const draw = registerShapeUtils<DrawShape>({
}, },
render(shape, { isHovered }) { render(shape, { isHovered }) {
const { id, points, style } = shape const { points, style } = shape
const styles = getShapeStyle(style) const styles = getShapeStyle(style)
const strokeWidth = +styles.strokeWidth const strokeWidth = +styles.strokeWidth
const shouldFill = const shouldFill =
style.isFilled &&
points.length > 3 && points.length > 3 &&
vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2 vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2
@ -58,11 +59,11 @@ const draw = registerShapeUtils<DrawShape>({
return ( return (
<circle <circle
id={id}
r={strokeWidth * 0.618} r={strokeWidth * 0.618}
fill={styles.stroke} fill={styles.stroke}
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={sw} strokeWidth={sw}
pointerEvents="all"
filter={isHovered ? 'url(#expand)' : 'none'} filter={isHovered ? 'url(#expand)' : 'none'}
/> />
) )
@ -80,15 +81,15 @@ const draw = registerShapeUtils<DrawShape>({
}) })
return ( return (
<g id={id}> <>
{shouldFill && ( {shouldFill && (
<path <path
d={polygonPathData} d={polygonPathData}
strokeWidth="0"
stroke="none" stroke="none"
fill={styles.fill} fill={styles.fill}
strokeLinejoin="round" strokeLinejoin="round"
strokeLinecap="round" strokeLinecap="round"
pointerEvents="fill"
/> />
)} )}
<path <path
@ -98,9 +99,10 @@ const draw = registerShapeUtils<DrawShape>({
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
strokeLinejoin="round" strokeLinejoin="round"
strokeLinecap="round" strokeLinecap="round"
pointerEvents="all"
filter={isHovered ? 'url(#expand)' : 'none'} filter={isHovered ? 'url(#expand)' : 'none'}
/> />
</g> </>
) )
} }
@ -129,29 +131,29 @@ const draw = registerShapeUtils<DrawShape>({
const sw = strokeWidth * 1.618 const sw = strokeWidth * 1.618
return ( return (
<g id={id}> <>
{style.dash !== DashStyle.Solid && (
<path
d={path}
fill="transparent"
stroke="transparent"
strokeWidth={strokeWidth * 2}
strokeLinejoin="round"
strokeLinecap="round"
/>
)}
<path <path
d={path} d={path}
fill={shouldFill ? styles.fill : 'none'} fill={shouldFill ? styles.fill : 'none'}
stroke="transparent"
strokeWidth={Math.min(4, strokeWidth * 2)}
strokeLinejoin="round"
strokeLinecap="round"
pointerEvents={shouldFill ? 'all' : 'stroke'}
/>
<path
d={path}
fill="transparent"
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={sw} strokeWidth={sw}
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset} strokeDashoffset={strokeDashoffset}
strokeLinejoin="round" strokeLinejoin="round"
strokeLinecap="round" strokeLinecap="round"
pointerEvents="stroke"
filter={isHovered ? 'url(#expand)' : 'none'} filter={isHovered ? 'url(#expand)' : 'none'}
/> />
</g> </>
) )
}, },
@ -174,13 +176,8 @@ const draw = registerShapeUtils<DrawShape>({
return getBoundsCenter(this.getBounds(shape)) return getBoundsCenter(this.getBounds(shape))
}, },
hitTest(shape, point) { hitTest() {
const pt = vec.sub(point, shape.point) return true
const min = +getShapeStyle(shape.style).strokeWidth
return shape.points.some(
(curr, i) =>
i > 0 && vec.distanceToLineSegment(shape.points[i - 1], curr, pt) < min
)
}, },
hitTestBounds(this, shape, brushBounds) { hitTestBounds(this, shape, brushBounds) {

View file

@ -58,16 +58,16 @@ const ellipse = registerShapeUtils<EllipseShape>({
const path = pathCache.get(shape) const path = pathCache.get(shape)
return ( return (
<g id={id}> <g id={id} pointerEvents={style.isFilled ? 'all' : 'stroke'}>
{style.isFilled && ( {style.isFilled && (
<ellipse <ellipse
id={id}
cx={radiusX} cx={radiusX}
cy={radiusY} cy={radiusY}
rx={rx} rx={rx}
ry={ry} ry={ry}
stroke="none" stroke="none"
fill={styles.fill} fill={styles.fill}
pointerEvents="fill"
/> />
)} )}
<path <path
@ -75,6 +75,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
fill={styles.stroke} fill={styles.stroke}
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
pointerEvents="all"
filter={isHovered ? 'url(#expand)' : 'none'} filter={isHovered ? 'url(#expand)' : 'none'}
/> />
</g> </g>
@ -97,7 +98,6 @@ const ellipse = registerShapeUtils<EllipseShape>({
return ( return (
<ellipse <ellipse
id={id}
cx={radiusX} cx={radiusX}
cy={radiusY} cy={radiusY}
rx={rx} rx={rx}
@ -107,6 +107,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
strokeWidth={sw} strokeWidth={sw}
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset} strokeDashoffset={strokeDashoffset}
pointerEvents={style.isFilled ? 'all' : 'stroke'}
filter={isHovered ? 'url(#expand)' : 'none'} filter={isHovered ? 'url(#expand)' : 'none'}
/> />
) )

View file

@ -28,19 +28,20 @@ const polyline = registerShapeUtils<PolylineShape>({
shouldRender(shape, prev) { shouldRender(shape, prev) {
return shape.points !== prev.points || shape.style !== prev.style return shape.points !== prev.points || shape.style !== prev.style
}, },
render(shape, { isHovered }) { render(shape) {
const { id, points } = shape const { points, style } = shape
const styles = getShapeStyle(shape.style) const styles = getShapeStyle(style)
return ( return (
<polyline <polyline
id={id}
points={points.toString()} points={points.toString()}
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={styles.strokeWidth} strokeWidth={styles.strokeWidth * 1.618}
fill={shape.style.isFilled ? styles.fill : 'none'} fill={shape.style.isFilled ? styles.fill : 'none'}
filter={isHovered ? 'url(#expand)' : 'none'} pointerEvents={style.isFilled ? 'all' : 'stroke'}
strokeLinecap="round"
strokeLinejoin="round"
/> />
) )
}, },
@ -62,19 +63,8 @@ const polyline = registerShapeUtils<PolylineShape>({
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2] return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
}, },
hitTest(shape, point) { hitTest() {
const pt = vec.sub(point, shape.point) return true
let prev = shape.points[0]
for (let i = 1; i < shape.points.length; i++) {
const curr = shape.points[i]
if (vec.distanceToLineSegment(prev, curr, pt) < 4) {
return true
}
prev = curr
}
return false
}, },
hitTestBounds(this, shape, brushBounds) { hitTestBounds(this, shape, brushBounds) {
@ -126,7 +116,7 @@ const polyline = registerShapeUtils<PolylineShape>({
canTransform: true, canTransform: true,
canChangeAspectRatio: true, canChangeAspectRatio: true,
canStyleFill: false, canStyleFill: true,
}) })
export default polyline export default polyline

View file

@ -39,26 +39,29 @@ const rectangle = registerShapeUtils<RectangleShape>({
}) })
return ( return (
<g id={id}> <>
<rect {style.isFilled && (
rx={radius} <rect
ry={radius} rx={radius}
x={+styles.strokeWidth / 2} ry={radius}
y={+styles.strokeWidth / 2} x={+styles.strokeWidth / 2}
width={Math.max(0, size[0] - strokeWidth)} y={+styles.strokeWidth / 2}
height={Math.max(0, size[1] - strokeWidth)} width={Math.max(0, size[0] - strokeWidth)}
strokeWidth={0} height={Math.max(0, size[1] - strokeWidth)}
fill={styles.fill} strokeWidth={0}
stroke={styles.stroke} fill={styles.fill}
/> stroke={styles.stroke}
/>
)}
<path <path
d={pathData} d={pathData}
fill={styles.stroke} fill={styles.stroke}
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={styles.strokeWidth} strokeWidth={styles.strokeWidth}
filter={isHovered ? 'url(#expand)' : 'none'} filter={isHovered ? 'url(#expand)' : 'none'}
pointerEvents={style.isFilled ? 'all' : 'stroke'}
/> />
</g> </>
) )
} }
@ -98,17 +101,21 @@ const rectangle = registerShapeUtils<RectangleShape>({
}) })
return ( return (
<g id={id}> <>
<rect <rect
x={sw / 2} x={sw / 2}
y={sw / 2} y={sw / 2}
width={w} width={w}
height={h} height={h}
fill={styles.fill} fill={styles.fill}
stroke="none" stroke="transparent"
strokeWidth={sw}
pointerEvents={style.isFilled ? 'all' : 'stroke'}
/> />
<g filter={isHovered ? 'url(#expand)' : 'none'}>{paths}</g> <g filter={isHovered ? 'url(#expand)' : 'none'} pointerEvents="stroke">
</g> {paths}
</g>
</>
) )
}, },