Improves handles for arrows
This commit is contained in:
parent
ff72493381
commit
72b6db12c4
13 changed files with 98 additions and 45 deletions
|
@ -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',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
|
3
todo.md
3
todo.md
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue