Improves handles for arrows

This commit is contained in:
Steve Ruiz 2021-06-05 07:36:39 +01:00
parent ff72493381
commit 72b6db12c4
13 changed files with 98 additions and 45 deletions

View file

@ -5,7 +5,6 @@ import { useSelector } from 'state'
import styled from 'styles' import styled from 'styles'
import { deepCompareArrays, getPage } from 'utils/utils' import { deepCompareArrays, getPage } from 'utils/utils'
import * as vec from 'utils/vec' import * as vec from 'utils/vec'
import { DotCircle } from '../misc'
export default function Handles() { export default function Handles() {
const selectedIds = useSelector( const selectedIds = useSelector(
@ -18,7 +17,9 @@ export default function Handles() {
selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]] selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
) )
const isSelecting = useSelector((s) => s.isIn('selecting.notPointing')) const isSelecting = useSelector((s) =>
s.isInAny('notPointing', 'pinching', 'translatingHandles')
)
if (!shape.handles || !isSelecting) return null if (!shape.handles || !isSelecting) return null
@ -49,29 +50,43 @@ function Handle({
const events = useHandleEvents(id, rGroup) const events = useHandleEvents(id, rGroup)
return ( return (
<g <StyledGroup
key={id} key={id}
className="handles"
ref={rGroup} ref={rGroup}
{...events} {...events}
cursor="pointer"
pointerEvents="all" pointerEvents="all"
transform={`translate(${point})`} transform={`translate(${point})`}
> >
<HandleCircleOuter r={12} /> <HandleCircleOuter r={12} />
<DotCircle r={4} /> <use href="#handle" pointerEvents="none" />
</g> </StyledGroup>
) )
} }
const HandleCircleOuter = styled('circle', { const StyledGroup = styled('g', {
fill: 'transparent', '&:hover': {
pointerEvents: 'all',
cursor: 'pointer', cursor: 'pointer',
},
'&:active': {
cursor: 'none',
},
}) })
const HandleCircle = styled('circle', { const HandleCircleOuter = styled('circle', {
zStrokeWidth: 2, fill: 'transparent',
stroke: '$text', stroke: 'none',
fill: '$panel', opacity: 0.2,
pointerEvents: 'none', pointerEvents: 'all',
cursor: 'pointer',
transform: 'scale(var(--scale))',
'&:hover': {
fill: '$selected',
'& > *': {
stroke: '$selected',
},
},
'&:active': {
fill: '$selected',
},
}) })

View file

@ -62,7 +62,7 @@ export default function Canvas() {
<g ref={rGroup}> <g ref={rGroup}>
<BoundsBg /> <BoundsBg />
<Page /> <Page />
{/* <Selected /> */} <Selected />
<Bounds /> <Bounds />
<Handles /> <Handles />
<Brush /> <Brush />

View file

@ -2,6 +2,7 @@ import { getShapeUtils } from 'lib/shape-utils'
import { memo } from 'react' import { memo } from 'react'
import { useSelector } from 'state' import { useSelector } from 'state'
import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils' import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
import { DotCircle, Handle } from './misc'
export default function Defs() { export default function Defs() {
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom) const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
@ -17,6 +18,8 @@ export default function Defs() {
{currentPageShapeIds.map((id) => ( {currentPageShapeIds.map((id) => (
<Def key={id} id={id} /> <Def key={id} id={id} />
))} ))}
<DotCircle id="dot" r={4} />
<Handle id="handle" r={4} />
<filter id="expand"> <filter id="expand">
<feMorphology operator="dilate" radius={2 / zoom} /> <feMorphology operator="dilate" radius={2 / zoom} />
</filter> </filter>

View file

@ -7,6 +7,13 @@ export const DotCircle = styled('circle', {
strokeWidth: '2', strokeWidth: '2',
}) })
export const Handle = styled('circle', {
transform: 'scale(var(--scale))',
fill: '$canvas',
stroke: '$selected',
strokeWidth: '2',
})
export const ThinLine = styled('line', { export const ThinLine = styled('line', {
zStrokeWidth: 1, zStrokeWidth: 1,
}) })

View file

@ -12,7 +12,7 @@ export default function Selected() {
return Array.from(data.selectedIds.values()) return Array.from(data.selectedIds.values())
}, deepCompareArrays) }, deepCompareArrays)
const isSelecting = useSelector((s) => s.isIn('selecting')) const isSelecting = useSelector((s) => s.isInAny('notPointing', 'pinching'))
if (!isSelecting) return null if (!isSelecting) return null
@ -44,7 +44,6 @@ export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
rotate(${shape.rotation * (180 / Math.PI)}, rotate(${shape.rotation * (180 / Math.PI)},
${center}) ${center})
translate(${bounds.minX},${bounds.minY}) translate(${bounds.minX},${bounds.minY})
rotate(${(bounds.rotation || 0) * (180 / Math.PI)}, 0, 0)
` `
return ( return (

View file

@ -93,10 +93,11 @@ const arrow = registerShapeUtils<ArrowShape>({
}, },
render(shape) { render(shape) {
const { id, bend, points, handles } = shape const { id, bend, handles } = shape
const { start, end, bend: _bend } = handles const { start, end, bend: _bend } = handles
const arrowDist = vec.dist(start.point, end.point) const arrowDist = vec.dist(start.point, end.point)
const showCircle = !vec.isEqual( const showCircle = !vec.isEqual(
_bend.point, _bend.point,
vec.med(start.point, end.point) vec.med(start.point, end.point)
@ -145,8 +146,8 @@ const arrow = registerShapeUtils<ArrowShape>({
const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2) const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2)
const u = vec.uni(vec.vec(start.point, end.point)) const u = vec.uni(vec.vec(start.point, end.point))
const v = vec.rot(vec.mul(vec.neg(u), length), endAngle) const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
const b = vec.add(points[1], vec.rot(v, Math.PI / 6)) const b = vec.add(end.point, vec.rot(v, Math.PI / 6))
const c = vec.add(points[1], vec.rot(v, -(Math.PI / 6))) const c = vec.add(end.point, vec.rot(v, -(Math.PI / 6)))
return ( return (
<g id={id}> <g id={id}>
@ -159,7 +160,7 @@ const arrow = registerShapeUtils<ArrowShape>({
strokeDasharray="none" strokeDasharray="none"
/> />
<polyline <polyline
points={[b, points[1], c].join()} points={[b, end.point, c].join()}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
fill="none" fill="none"
@ -170,6 +171,15 @@ const arrow = registerShapeUtils<ArrowShape>({
}, },
getBounds(shape) { getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const { start, end } = shape.handles
this.boundsCache.set(shape, getBoundsFromPoints([start.point, end.point]))
}
return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
if (!this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
this.boundsCache.set(shape, getBoundsFromPoints(shape.points)) this.boundsCache.set(shape, getBoundsFromPoints(shape.points))
} }

View file

@ -4,7 +4,6 @@ import { DotShape, ShapeType } from 'types'
import { registerShapeUtils } from './index' import { registerShapeUtils } from './index'
import { boundsContained } from 'utils/bounds' import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { DotCircle } from 'components/canvas/misc'
import { translateBounds } from 'utils/utils' import { translateBounds } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles' import { defaultStyle } from 'lib/shape-styles'
@ -34,7 +33,7 @@ const dot = registerShapeUtils<DotShape>({
}, },
render({ id }) { render({ id }) {
return <DotCircle id={id} cx={0} cy={0} r={3} /> return <use href="#dot" />
}, },
getBounds(shape) { getBounds(shape) {

View file

@ -4,7 +4,7 @@ import { LineShape, ShapeType } from 'types'
import { registerShapeUtils } from './index' import { registerShapeUtils } from './index'
import { boundsContained } from 'utils/bounds' import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { DotCircle, ThinLine } from 'components/canvas/misc' import { ThinLine } from 'components/canvas/misc'
import { translateBounds } from 'utils/utils' import { translateBounds } from 'utils/utils'
import styled from 'styles' import styled from 'styles'
import { defaultStyle } from 'lib/shape-styles' import { defaultStyle } from 'lib/shape-styles'
@ -42,7 +42,7 @@ const line = registerShapeUtils<LineShape>({
return ( return (
<g id={id}> <g id={id}>
<ThinLine x1={x1} y1={y1} x2={x2} y2={y2} /> <ThinLine x1={x1} y1={y1} x2={x2} y2={y2} />
<DotCircle cx={0} cy={0} r={3} /> <use href="dot" />
</g> </g>
) )
}, },

View file

@ -4,7 +4,7 @@ import { RayShape, ShapeType } from 'types'
import { registerShapeUtils } from './index' import { registerShapeUtils } from './index'
import { boundsContained } from 'utils/bounds' import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { DotCircle, ThinLine } from 'components/canvas/misc' import { ThinLine } from 'components/canvas/misc'
import { translateBounds } from 'utils/utils' import { translateBounds } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles' import { defaultStyle } from 'lib/shape-styles'
@ -40,7 +40,7 @@ const ray = registerShapeUtils<RayShape>({
return ( return (
<g id={id}> <g id={id}>
<ThinLine x1={0} y1={0} x2={x2} y2={y2} /> <ThinLine x1={0} y1={0} x2={x2} y2={y2} />
<DotCircle cx={0} cy={0} r={3} /> <use href="#dot" />
</g> </g>
) )
}, },

View file

@ -1,6 +1,7 @@
import Command from './command' import Command from './command'
import history from '../history' import history from '../history'
import { ArrowShape, Data } from 'types' import { ArrowShape, Data } from 'types'
import * as vec from 'utils/vec'
import { getPage } from 'utils/utils' import { getPage } from 'utils/utils'
import { ArrowSnapshot } from 'state/sessions/arrow-session' import { ArrowSnapshot } from 'state/sessions/arrow-session'
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
@ -21,7 +22,9 @@ export default function arrowCommand(
const { initialShape, currentPageId } = after const { initialShape, currentPageId } = after
getPage(data, currentPageId).shapes[initialShape.id] = initialShape const page = getPage(data, currentPageId)
page.shapes[initialShape.id] = initialShape
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(initialShape.id) data.selectedIds.add(initialShape.id)

View file

@ -4,6 +4,7 @@ import { Data } from 'types'
import { getPage } from 'utils/utils' import { getPage } from 'utils/utils'
import { HandleSnapshot } from 'state/sessions/handle-session' import { HandleSnapshot } from 'state/sessions/handle-session'
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
import * as vec from 'utils/vec'
export default function handleCommand( export default function handleCommand(
data: Data, data: Data,
@ -16,13 +17,26 @@ export default function handleCommand(
name: 'moved_handle', name: 'moved_handle',
category: 'canvas', category: 'canvas',
do(data, isInitial) { do(data, isInitial) {
if (isInitial) return // if (isInitial) return
const { initialShape, currentPageId } = after const { initialShape, currentPageId } = after
const shape = getPage(data, currentPageId).shapes[initialShape.id] const page = getPage(data, currentPageId)
const shape = page.shapes[initialShape.id]
getShapeUtils(shape).onHandleChange(shape, initialShape.handles) getShapeUtils(shape).onHandleChange(shape, initialShape.handles)
const bounds = getShapeUtils(shape).getBounds(shape)
const offset = vec.sub([bounds.minX, bounds.minY], shape.point)
getShapeUtils(shape).translateTo(shape, vec.add(shape.point, offset))
const { start, end, bend } = page.shapes[initialShape.id].handles
start.point = vec.sub(start.point, offset)
end.point = vec.sub(end.point, offset)
bend.point = vec.sub(bend.point, offset)
}, },
undo(data) { undo(data) {
const { initialShape, currentPageId } = before const { initialShape, currentPageId } = before

View file

@ -330,8 +330,6 @@ const state = createState({
CANCELLED: { do: 'cancelSession', to: 'selecting' }, CANCELLED: { do: 'cancelSession', to: 'selecting' },
}, },
}, },
},
},
pinching: { pinching: {
on: { on: {
PINCHED: { do: 'pinchCamera' }, PINCHED: { do: 'pinchCamera' },
@ -350,6 +348,8 @@ const state = createState({
}, },
}, },
}, },
},
},
usingTool: { usingTool: {
initial: 'draw', initial: 'draw',
onEnter: 'clearSelectedIds', onEnter: 'clearSelectedIds',

View file

@ -8,3 +8,6 @@
- Allow single-selected groups to transform their children correctly - Allow single-selected groups to transform their children correctly
- (merge transform-session and transform-single-session) - (merge transform-session and transform-single-session)
- fix drift when moving children of rotated group - fix drift when moving children of rotated group
- shift dragging arrow handles should lock to directions
- arrow rotation with handles
- fix ellipse when scaleX < 0 or scaleY < 0