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 { deepCompareArrays, getPage } from 'utils/utils'
import * as vec from 'utils/vec'
import { DotCircle } from '../misc'
export default function Handles() {
const selectedIds = useSelector(
@ -18,7 +17,9 @@ export default function Handles() {
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
@ -49,29 +50,43 @@ function Handle({
const events = useHandleEvents(id, rGroup)
return (
<g
<StyledGroup
key={id}
className="handles"
ref={rGroup}
{...events}
cursor="pointer"
pointerEvents="all"
transform={`translate(${point})`}
>
<HandleCircleOuter r={12} />
<DotCircle r={4} />
</g>
<use href="#handle" pointerEvents="none" />
</StyledGroup>
)
}
const HandleCircleOuter = styled('circle', {
fill: 'transparent',
pointerEvents: 'all',
const StyledGroup = styled('g', {
'&:hover': {
cursor: 'pointer',
},
'&:active': {
cursor: 'none',
},
})
const HandleCircle = styled('circle', {
zStrokeWidth: 2,
stroke: '$text',
fill: '$panel',
pointerEvents: 'none',
const HandleCircleOuter = styled('circle', {
fill: 'transparent',
stroke: 'none',
opacity: 0.2,
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}>
<BoundsBg />
<Page />
{/* <Selected /> */}
<Selected />
<Bounds />
<Handles />
<Brush />

View file

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

View file

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

View file

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

View file

@ -93,10 +93,11 @@ const arrow = registerShapeUtils<ArrowShape>({
},
render(shape) {
const { id, bend, points, handles } = shape
const { id, bend, handles } = shape
const { start, end, bend: _bend } = handles
const arrowDist = vec.dist(start.point, end.point)
const showCircle = !vec.isEqual(
_bend.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 u = vec.uni(vec.vec(start.point, end.point))
const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
const b = vec.add(points[1], vec.rot(v, Math.PI / 6))
const c = 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(end.point, vec.rot(v, -(Math.PI / 6)))
return (
<g id={id}>
@ -159,7 +160,7 @@ const arrow = registerShapeUtils<ArrowShape>({
strokeDasharray="none"
/>
<polyline
points={[b, points[1], c].join()}
points={[b, end.point, c].join()}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
@ -170,6 +171,15 @@ const arrow = registerShapeUtils<ArrowShape>({
},
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)) {
this.boundsCache.set(shape, getBoundsFromPoints(shape.points))
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import Command from './command'
import history from '../history'
import { ArrowShape, Data } from 'types'
import * as vec from 'utils/vec'
import { getPage } from 'utils/utils'
import { ArrowSnapshot } from 'state/sessions/arrow-session'
import { getShapeUtils } from 'lib/shape-utils'
@ -21,7 +22,9 @@ export default function arrowCommand(
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.add(initialShape.id)

View file

@ -4,6 +4,7 @@ import { Data } from 'types'
import { getPage } from 'utils/utils'
import { HandleSnapshot } from 'state/sessions/handle-session'
import { getShapeUtils } from 'lib/shape-utils'
import * as vec from 'utils/vec'
export default function handleCommand(
data: Data,
@ -16,13 +17,26 @@ export default function handleCommand(
name: 'moved_handle',
category: 'canvas',
do(data, isInitial) {
if (isInitial) return
// if (isInitial) return
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)
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) {
const { initialShape, currentPageId } = before

View file

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

View file

@ -8,3 +8,6 @@
- Allow single-selected groups to transform their children correctly
- (merge transform-session and transform-single-session)
- 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