Adds double-pointing handles action, toggled arrowheads, removes circles.
This commit is contained in:
parent
07f96a8416
commit
7d14791d00
12 changed files with 227 additions and 368 deletions
|
@ -12,7 +12,9 @@ export default function useHandleEvents(
|
|||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
rGroup.current.setPointerCapture(e.pointerId)
|
||||
state.send('POINTED_HANDLE', inputs.pointerDown(e, id))
|
||||
const info = inputs.pointerDown(e, id)
|
||||
|
||||
state.send('POINTED_HANDLE', info)
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
@ -22,7 +24,14 @@ export default function useHandleEvents(
|
|||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
rGroup.current.releasePointerCapture(e.pointerId)
|
||||
state.send('STOPPED_POINTING', inputs.pointerUp(e))
|
||||
const isDoubleClick = inputs.isDoubleClick()
|
||||
const info = inputs.pointerUp(e, id)
|
||||
|
||||
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
||||
state.send('DOUBLE_POINTED_HANDLE', info)
|
||||
} else {
|
||||
state.send('STOPPED_POINTING', inputs.pointerUp(e))
|
||||
}
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { CircleShape, ShapeType } from 'types'
|
||||
import Utils from './utils'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
export default class Circle extends CodeShape<CircleShape> {
|
||||
constructor(props = {} as Partial<CircleShape>) {
|
||||
props.point = Utils.vectorToPoint(props.point)
|
||||
|
||||
super({
|
||||
id: uniqueId(),
|
||||
seed: Math.random(),
|
||||
parentId: (window as any).currentPageId,
|
||||
type: ShapeType.Circle,
|
||||
isGenerated: true,
|
||||
name: 'Circle',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
radius: 20,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
...props,
|
||||
style: { ...defaultStyle, ...props.style },
|
||||
})
|
||||
}
|
||||
|
||||
export(): CircleShape {
|
||||
const shape = { ...this.shape }
|
||||
|
||||
shape.point = Utils.vectorToPoint(shape.point)
|
||||
|
||||
return shape
|
||||
}
|
||||
|
||||
get radius(): number {
|
||||
return this.shape.radius
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import Rectangle from './rectangle'
|
||||
import Circle from './circle'
|
||||
import Ellipse from './ellipse'
|
||||
import Polyline from './polyline'
|
||||
import Dot from './dot'
|
||||
|
@ -13,7 +12,6 @@ import { CodeControl, Data, Shape } from 'types'
|
|||
|
||||
const baseScope = {
|
||||
Dot,
|
||||
Circle,
|
||||
Ellipse,
|
||||
Ray,
|
||||
Line,
|
||||
|
|
33
state/commands/double-point-handle.ts
Normal file
33
state/commands/double-point-handle.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data, PointerInfo } from 'types'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import { deepClone, getPage, getShape, updateParents } from 'utils/utils'
|
||||
|
||||
export default function doublePointHandleCommand(
|
||||
data: Data,
|
||||
id: string,
|
||||
payload: PointerInfo
|
||||
): void {
|
||||
const initialShape = deepClone(getShape(data, id))
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: 'double_point_handle',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
const shape = shapes[id]
|
||||
getShapeUtils(shape).onDoublePointHandle(shape, payload.target, payload)
|
||||
updateParents(data, [id])
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data)
|
||||
shapes[id] = initialShape
|
||||
updateParents(data, [id])
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
|
@ -6,30 +6,30 @@ import deletePage from './delete-page'
|
|||
import deleteSelected from './delete-selected'
|
||||
import direct from './direct'
|
||||
import distribute from './distribute'
|
||||
import doublePointHandle from './double-point-handle'
|
||||
import draw from './draw'
|
||||
import duplicate from './duplicate'
|
||||
import edit from './edit'
|
||||
import generate from './generate'
|
||||
import group from './group'
|
||||
import handle from './handle'
|
||||
import move from './move'
|
||||
import moveToPage from './move-to-page'
|
||||
import mutate from './mutate'
|
||||
import nudge from './nudge'
|
||||
import rotate from './rotate'
|
||||
import paste from './paste'
|
||||
import resetBounds from './reset-bounds'
|
||||
import rotate from './rotate'
|
||||
import rotateCcw from './rotate-ccw'
|
||||
import stretch from './stretch'
|
||||
import style from './style'
|
||||
import mutate from './mutate'
|
||||
import toggle from './toggle'
|
||||
import transform from './transform'
|
||||
import transformSingle from './transform-single'
|
||||
import translate from './translate'
|
||||
import ungroup from './ungroup'
|
||||
import edit from './edit'
|
||||
import resetBounds from './reset-bounds'
|
||||
|
||||
const commands = {
|
||||
mutate,
|
||||
align,
|
||||
arrow,
|
||||
changePage,
|
||||
|
@ -38,6 +38,7 @@ const commands = {
|
|||
deleteSelected,
|
||||
direct,
|
||||
distribute,
|
||||
doublePointHandle,
|
||||
draw,
|
||||
duplicate,
|
||||
edit,
|
||||
|
@ -46,6 +47,7 @@ const commands = {
|
|||
handle,
|
||||
move,
|
||||
moveToPage,
|
||||
mutate,
|
||||
nudge,
|
||||
paste,
|
||||
resetBounds,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { PointerInfo } from 'types'
|
|||
import vec from 'utils/vec'
|
||||
import { isDarwin, getPoint } from 'utils/utils'
|
||||
|
||||
const DOUBLE_CLICK_DURATION = 300
|
||||
const DOUBLE_CLICK_DURATION = 250
|
||||
|
||||
class Inputs {
|
||||
activePointerId?: number
|
||||
|
@ -104,13 +104,14 @@ class Inputs {
|
|||
return info
|
||||
}
|
||||
|
||||
pointerMove(e: PointerEvent | React.PointerEvent) {
|
||||
pointerMove(e: PointerEvent | React.PointerEvent, target = '') {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
|
||||
const prev = this.points[e.pointerId]
|
||||
|
||||
const info = {
|
||||
...prev,
|
||||
target,
|
||||
pointerId: e.pointerId,
|
||||
point: getPoint(e),
|
||||
pressure: e.pressure || 0.5,
|
||||
|
@ -129,13 +130,14 @@ class Inputs {
|
|||
return info
|
||||
}
|
||||
|
||||
pointerUp = (e: PointerEvent | React.PointerEvent) => {
|
||||
pointerUp = (e: PointerEvent | React.PointerEvent, target = '') => {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
|
||||
const prev = this.points[e.pointerId]
|
||||
|
||||
const info = {
|
||||
...prev,
|
||||
target,
|
||||
origin: prev?.origin || getPoint(e),
|
||||
point: getPoint(e),
|
||||
pressure: e.pressure || 0.5,
|
||||
|
|
|
@ -7,7 +7,13 @@ import {
|
|||
translateBounds,
|
||||
pointsBetween,
|
||||
} from 'utils/utils'
|
||||
import { ArrowShape, DashStyle, ShapeHandle, ShapeType } from 'types'
|
||||
import {
|
||||
ArrowShape,
|
||||
DashStyle,
|
||||
Decoration,
|
||||
ShapeHandle,
|
||||
ShapeType,
|
||||
} from 'types'
|
||||
import { circleFromThreePoints, isAngleBetween } from 'utils/utils'
|
||||
import { pointInBounds } from 'utils/hitTests'
|
||||
import {
|
||||
|
@ -71,8 +77,8 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
handles,
|
||||
decorations: {
|
||||
start: null,
|
||||
end: null,
|
||||
middle: null,
|
||||
end: Decoration.Arrow,
|
||||
},
|
||||
...props,
|
||||
style: {
|
||||
|
@ -98,13 +104,19 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
|
||||
const arrowDist = vec.dist(start.point, end.point)
|
||||
|
||||
let shaftPath: JSX.Element
|
||||
let startAngle: number
|
||||
let endAngle: number
|
||||
|
||||
if (isStraightLine) {
|
||||
// Render a straight arrow as a freehand path.
|
||||
if (!pathCache.has(shape)) {
|
||||
renderPath(shape)
|
||||
if (shape.style.dash === DashStyle.Solid && !pathCache.has(shape)) {
|
||||
renderFreehandArrowShaft(shape)
|
||||
}
|
||||
|
||||
const path = pathCache.get(shape)
|
||||
const path =
|
||||
shape.style.dash === DashStyle.Solid
|
||||
? pathCache.get(shape)
|
||||
: 'M' + start.point + 'L' + end.point
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } =
|
||||
shape.style.dash === DashStyle.Solid
|
||||
|
@ -119,9 +131,12 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
2
|
||||
)
|
||||
|
||||
return (
|
||||
<g id={id}>
|
||||
{/* Improves hit testing */}
|
||||
startAngle = Math.PI
|
||||
|
||||
endAngle = 0
|
||||
|
||||
shaftPath = (
|
||||
<>
|
||||
<path
|
||||
d={path}
|
||||
stroke="transparent"
|
||||
|
@ -131,7 +146,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
strokeDashoffset="none"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Arrowshaft */}
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
|
@ -141,79 +155,86 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Arrowhead */}
|
||||
{style.dash !== DashStyle.Solid && (
|
||||
<path
|
||||
d={getArrowHeadPath(shape, 0)}
|
||||
strokeWidth={strokeWidth * 1.618}
|
||||
fill="none"
|
||||
strokeDashoffset="none"
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
></path>
|
||||
</>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const circle = getCtp(shape)
|
||||
|
||||
const circle = getCtp(shape)
|
||||
const path = getArrowArcPath(start, end, circle, bend)
|
||||
|
||||
if (!pathCache.has(shape)) {
|
||||
renderPath(
|
||||
shape,
|
||||
const { strokeDasharray, strokeDashoffset } =
|
||||
shape.style.dash === DashStyle.Solid
|
||||
? {
|
||||
strokeDasharray: 'none',
|
||||
strokeDashoffset: '0',
|
||||
}
|
||||
: getPerfectDashProps(
|
||||
getArcLength(
|
||||
[circle[0], circle[1]],
|
||||
circle[2],
|
||||
start.point,
|
||||
end.point
|
||||
) - 1,
|
||||
strokeWidth * 1.618,
|
||||
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed',
|
||||
2
|
||||
)
|
||||
|
||||
startAngle =
|
||||
vec.angle([circle[0], circle[1]], start.point) -
|
||||
vec.angle(end.point, start.point) +
|
||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
||||
|
||||
endAngle =
|
||||
vec.angle([circle[0], circle[1]], end.point) -
|
||||
vec.angle(start.point, end.point) +
|
||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
||||
vec.angle(start.point, end.point) +
|
||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
||||
|
||||
shaftPath = (
|
||||
<>
|
||||
<path
|
||||
d={path}
|
||||
stroke="transparent"
|
||||
fill="none"
|
||||
strokeWidth={Math.max(8, strokeWidth * 2)}
|
||||
strokeDasharray="none"
|
||||
strokeDashoffset="none"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth * 2}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
></path>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const path = getArrowArcPath(start, end, circle, bend)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } =
|
||||
shape.style.dash === DashStyle.Solid
|
||||
? {
|
||||
strokeDasharray: 'none',
|
||||
strokeDashoffset: '0',
|
||||
}
|
||||
: getPerfectDashProps(
|
||||
getArcLength(
|
||||
[circle[0], circle[1]],
|
||||
circle[2],
|
||||
start.point,
|
||||
end.point
|
||||
) - 1,
|
||||
strokeWidth * 1.618,
|
||||
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed',
|
||||
2
|
||||
)
|
||||
|
||||
return (
|
||||
<g id={id}>
|
||||
{/* Improves hit testing */}
|
||||
<path
|
||||
d={path}
|
||||
stroke="transparent"
|
||||
fill="none"
|
||||
strokeWidth={Math.max(8, strokeWidth * 2)}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
{/* Arrow Shaft */}
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth * 1.618}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
/>
|
||||
{/* Arrowhead */}
|
||||
<path
|
||||
d={pathCache.get(shape)}
|
||||
strokeWidth={strokeWidth * 1.618}
|
||||
strokeDasharray="none"
|
||||
fill="none"
|
||||
/>
|
||||
{shaftPath}
|
||||
{shape.decorations.start === Decoration.Arrow && (
|
||||
<path
|
||||
d={getArrowHeadPath(shape, start.point, startAngle)}
|
||||
strokeWidth={strokeWidth * 1.618}
|
||||
fill="none"
|
||||
strokeDashoffset="none"
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
)}
|
||||
{shape.decorations.end === Decoration.Arrow && (
|
||||
<path
|
||||
d={getArrowHeadPath(shape, end.point, endAngle)}
|
||||
strokeWidth={strokeWidth * 1.618}
|
||||
fill="none"
|
||||
strokeDashoffset="none"
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
},
|
||||
|
@ -347,10 +368,29 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
return this
|
||||
},
|
||||
|
||||
onHandleChange(shape, handles) {
|
||||
// const oldBounds = this.getRotatedBounds(shape)
|
||||
// const prevCenter = getBoundsCenter(oldBounds)
|
||||
onDoublePointHandle(shape, handle) {
|
||||
switch (handle) {
|
||||
case 'bend': {
|
||||
shape.bend = 0
|
||||
shape.handles.bend.point = getBendPoint(shape)
|
||||
break
|
||||
}
|
||||
case 'start': {
|
||||
shape.decorations.start = shape.decorations.start
|
||||
? null
|
||||
: Decoration.Arrow
|
||||
break
|
||||
}
|
||||
case 'end': {
|
||||
shape.decorations.end = shape.decorations.end ? null : Decoration.Arrow
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
|
||||
onHandleChange(shape, handles) {
|
||||
for (const id in handles) {
|
||||
const handle = handles[id]
|
||||
|
||||
|
@ -450,7 +490,7 @@ function getBendPoint(shape: ArrowShape) {
|
|||
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||
}
|
||||
|
||||
function renderPath(shape: ArrowShape, endAngle = 0) {
|
||||
function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||
const { style, id } = shape
|
||||
const { start, end } = shape.handles
|
||||
|
||||
|
@ -458,73 +498,38 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
|
|||
|
||||
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
|
||||
|
||||
const sw = strokeWidth
|
||||
|
||||
// Start
|
||||
const a = start.point
|
||||
|
||||
// End
|
||||
const b = end.point
|
||||
|
||||
// Middle
|
||||
const m = vec.add(
|
||||
vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2),
|
||||
[getRandom() * sw, getRandom() * sw]
|
||||
[getRandom() * strokeWidth, getRandom() * strokeWidth]
|
||||
)
|
||||
|
||||
// Left and right sides of the arrowhead
|
||||
let { left: c, right: d } = getArrowHeadPoints(shape, endAngle)
|
||||
|
||||
// Switch which side of the arrow is drawn first
|
||||
if (getRandom() > 0) [c, d] = [d, c]
|
||||
|
||||
if (style.dash !== DashStyle.Solid) {
|
||||
pathCache.set(
|
||||
shape,
|
||||
(endAngle ? ['M', c, 'L', b, d] : ['M', a, 'L', b]).join(' ')
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const points = endAngle
|
||||
? [
|
||||
// Just the arrowhead
|
||||
...pointsBetween(b, c),
|
||||
...pointsBetween(c, b),
|
||||
...pointsBetween(b, d),
|
||||
...pointsBetween(d, b),
|
||||
]
|
||||
: [
|
||||
// The arrow shaft
|
||||
b,
|
||||
a,
|
||||
...pointsBetween(a, m),
|
||||
...pointsBetween(m, b),
|
||||
...pointsBetween(b, c),
|
||||
...pointsBetween(c, b),
|
||||
...pointsBetween(b, d),
|
||||
...pointsBetween(d, b),
|
||||
]
|
||||
|
||||
const stroke = getStroke(points, {
|
||||
size: 1 + strokeWidth,
|
||||
thinning: 0.6,
|
||||
easing: (t) => t * t * t * t,
|
||||
end: { taper: strokeWidth * 20 },
|
||||
start: { taper: strokeWidth * 20 },
|
||||
simulatePressure: false,
|
||||
})
|
||||
const stroke = getStroke(
|
||||
[
|
||||
...pointsBetween(start.point, m),
|
||||
...pointsBetween(m, end.point),
|
||||
end.point,
|
||||
end.point,
|
||||
end.point,
|
||||
],
|
||||
{
|
||||
size: 1 + strokeWidth,
|
||||
thinning: 0.6,
|
||||
easing: (t) => t * t * t * t,
|
||||
end: { taper: strokeWidth * 2 },
|
||||
start: { taper: strokeWidth * 2 },
|
||||
simulatePressure: false,
|
||||
}
|
||||
)
|
||||
|
||||
pathCache.set(shape, getSvgPathFromStroke(stroke))
|
||||
}
|
||||
|
||||
function getArrowHeadPath(shape: ArrowShape, endAngle = 0) {
|
||||
const { end } = shape.handles
|
||||
const { left, right } = getArrowHeadPoints(shape, endAngle)
|
||||
return ['M', left, 'L', end.point, right].join(' ')
|
||||
function getArrowHeadPath(shape: ArrowShape, point: number[], angle = 0) {
|
||||
const { left, right } = getArrowHeadPoints(shape, point, angle)
|
||||
return ['M', left, 'L', point, right].join(' ')
|
||||
}
|
||||
|
||||
function getArrowHeadPoints(shape: ArrowShape, endAngle = 0) {
|
||||
function getArrowHeadPoints(shape: ArrowShape, point: number[], angle = 0) {
|
||||
const { start, end } = shape.handles
|
||||
|
||||
const stroke = +getShapeStyle(shape.style).strokeWidth * 2
|
||||
|
@ -537,18 +542,15 @@ function getArrowHeadPoints(shape: ArrowShape, endAngle = 0) {
|
|||
const u = vec.uni(vec.vec(start.point, end.point))
|
||||
|
||||
// The end of the arrowhead wings
|
||||
const v = vec.rot(vec.mul(vec.neg(u), arrowHeadlength), endAngle)
|
||||
const v = vec.rot(vec.mul(vec.neg(u), arrowHeadlength), angle)
|
||||
|
||||
// Use the shape's random seed to create minor offsets for the angles
|
||||
const getRandom = rng(shape.id)
|
||||
|
||||
return {
|
||||
left: vec.add(
|
||||
end.point,
|
||||
vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())
|
||||
),
|
||||
left: vec.add(point, vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())),
|
||||
right: vec.add(
|
||||
end.point,
|
||||
point,
|
||||
vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
|
||||
),
|
||||
}
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { CircleShape, ShapeType } from 'types'
|
||||
import { boundsContained } from 'utils/bounds'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
import { pointInCircle } from 'utils/hitTests'
|
||||
import { translateBounds } from 'utils/utils'
|
||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||
import { registerShapeUtils } from './register'
|
||||
|
||||
const circle = registerShapeUtils<CircleShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
seed: Math.random(),
|
||||
type: ShapeType.Circle,
|
||||
isGenerated: false,
|
||||
name: 'Circle',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
radius: 1,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id, radius, style }) {
|
||||
const styles = getShapeStyle(style)
|
||||
|
||||
return (
|
||||
<circle
|
||||
id={id}
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={Math.max(0, radius - Number(styles.strokeWidth) / 2)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const { radius } = shape
|
||||
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: radius * 2,
|
||||
minY: 0,
|
||||
maxY: radius * 2,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
}
|
||||
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||
},
|
||||
|
||||
getRotatedBounds(shape) {
|
||||
return this.getBounds(shape)
|
||||
},
|
||||
|
||||
getCenter(shape) {
|
||||
return [shape.point[0] + shape.radius, shape.point[1] + shape.radius]
|
||||
},
|
||||
|
||||
hitTest(shape, point) {
|
||||
return pointInCircle(
|
||||
point,
|
||||
vec.addScalar(shape.point, shape.radius),
|
||||
shape.radius
|
||||
)
|
||||
},
|
||||
|
||||
hitTestBounds(shape, bounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
|
||||
return (
|
||||
boundsContained(shapeBounds, bounds) ||
|
||||
intersectCircleBounds(
|
||||
vec.addScalar(shape.point, shape.radius),
|
||||
shape.radius,
|
||||
bounds
|
||||
).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
|
||||
shape.radius =
|
||||
initialShape.radius * Math.min(Math.abs(scaleX), Math.abs(scaleY))
|
||||
|
||||
shape.point = [
|
||||
bounds.minX +
|
||||
(bounds.width - shape.radius * 2) *
|
||||
(scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
|
||||
bounds.minY +
|
||||
(bounds.height - shape.radius * 2) *
|
||||
(scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
|
||||
]
|
||||
|
||||
return this
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds) {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
return this
|
||||
},
|
||||
|
||||
canChangeAspectRatio: false,
|
||||
})
|
||||
|
||||
export default circle
|
|
@ -1,5 +1,4 @@
|
|||
import { Shape, ShapeType, ShapeByType, ShapeUtility } from 'types'
|
||||
import circle from './circle'
|
||||
import dot from './dot'
|
||||
import polyline from './polyline'
|
||||
import rectangle from './rectangle'
|
||||
|
@ -13,17 +12,16 @@ import text from './text'
|
|||
|
||||
// A mapping of shape types to shape utilities.
|
||||
const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
|
||||
[ShapeType.Circle]: circle,
|
||||
[ShapeType.Dot]: dot,
|
||||
[ShapeType.Polyline]: polyline,
|
||||
[ShapeType.Rectangle]: rectangle,
|
||||
[ShapeType.Ellipse]: ellipse,
|
||||
[ShapeType.Line]: line,
|
||||
[ShapeType.Ray]: ray,
|
||||
[ShapeType.Draw]: draw,
|
||||
[ShapeType.Arrow]: arrow,
|
||||
[ShapeType.Text]: text,
|
||||
[ShapeType.Group]: group,
|
||||
[ShapeType.Dot]: dot,
|
||||
[ShapeType.Polyline]: polyline,
|
||||
[ShapeType.Line]: line,
|
||||
[ShapeType.Ray]: ray,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -82,6 +82,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
|||
return this
|
||||
},
|
||||
|
||||
onDoublePointHandle() {
|
||||
return this
|
||||
},
|
||||
|
||||
onDoubleFocus() {
|
||||
return this
|
||||
},
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
getSelectedIds,
|
||||
setSelectedIds,
|
||||
getPageState,
|
||||
setToArray,
|
||||
} from 'utils/utils'
|
||||
import {
|
||||
Data,
|
||||
|
@ -161,7 +162,6 @@ const state = createState({
|
|||
SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
|
||||
SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
|
||||
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
|
||||
SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
|
||||
SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
|
||||
SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
|
||||
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
|
||||
|
@ -408,6 +408,10 @@ const state = createState({
|
|||
PRESSED_SHIFT_KEY: 'keyUpdateHandleSession',
|
||||
RELEASED_SHIFT_KEY: 'keyUpdateHandleSession',
|
||||
STOPPED_POINTING: { to: 'selecting' },
|
||||
DOUBLE_POINTED_HANDLE: {
|
||||
do: ['cancelSession', 'doublePointHandle'],
|
||||
to: 'selecting',
|
||||
},
|
||||
CANCELLED: { do: 'cancelSession', to: 'selecting' },
|
||||
},
|
||||
},
|
||||
|
@ -627,37 +631,6 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
onEnter: 'setActiveToolCircle',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
on: {
|
||||
CANCELLED: { to: 'selecting' },
|
||||
POINTED_SHAPE: {
|
||||
to: 'circle.editing',
|
||||
},
|
||||
POINTED_CANVAS: {
|
||||
to: 'circle.editing',
|
||||
},
|
||||
},
|
||||
},
|
||||
editing: {
|
||||
on: {
|
||||
STOPPED_POINTING: { to: 'selecting' },
|
||||
CANCELLED: { to: 'selecting' },
|
||||
MOVED_POINTER: {
|
||||
if: 'distanceImpliesDrag',
|
||||
then: {
|
||||
get: 'newCircle',
|
||||
do: 'createShape',
|
||||
to: 'drawingShape.bounds',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ellipse: {
|
||||
onEnter: 'setActiveToolEllipse',
|
||||
initial: 'creating',
|
||||
|
@ -871,9 +844,6 @@ const state = createState({
|
|||
newArrow() {
|
||||
return ShapeType.Arrow
|
||||
},
|
||||
newCircle() {
|
||||
return ShapeType.Circle
|
||||
},
|
||||
newEllipse() {
|
||||
return ShapeType.Ellipse
|
||||
},
|
||||
|
@ -1099,6 +1069,12 @@ const state = createState({
|
|||
)
|
||||
},
|
||||
|
||||
// Handles
|
||||
doublePointHandle(data, payload: PointerInfo) {
|
||||
const id = setToArray(getSelectedIds(data))[0]
|
||||
commands.doublePointHandle(data, id, payload)
|
||||
},
|
||||
|
||||
// Dragging Handle
|
||||
startHandleSession(data, payload: PointerInfo) {
|
||||
const shapeId = Array.from(getSelectedIds(data).values())[0]
|
||||
|
@ -1230,6 +1206,8 @@ const state = createState({
|
|||
)
|
||||
},
|
||||
|
||||
/* -------------------- Selection ------------------- */
|
||||
|
||||
// Nudges
|
||||
nudgeSelection(data, payload: { delta: number[]; shiftKey: boolean }) {
|
||||
commands.nudge(
|
||||
|
@ -1243,8 +1221,6 @@ const state = createState({
|
|||
)
|
||||
},
|
||||
|
||||
/* -------------------- Selection ------------------- */
|
||||
|
||||
clearInputs() {
|
||||
inputs.clear()
|
||||
},
|
||||
|
@ -1377,9 +1353,6 @@ const state = createState({
|
|||
setActiveToolRay(data) {
|
||||
data.activeTool = ShapeType.Ray
|
||||
},
|
||||
setActiveToolCircle(data) {
|
||||
data.activeTool = ShapeType.Circle
|
||||
},
|
||||
setActiveToolLine(data) {
|
||||
data.activeTool = ShapeType.Line
|
||||
},
|
||||
|
|
15
types.ts
15
types.ts
|
@ -61,7 +61,6 @@ export interface PageState {
|
|||
|
||||
export enum ShapeType {
|
||||
Dot = 'dot',
|
||||
Circle = 'circle',
|
||||
Ellipse = 'ellipse',
|
||||
Line = 'line',
|
||||
Ray = 'ray',
|
||||
|
@ -137,11 +136,6 @@ export interface DotShape extends BaseShape {
|
|||
type: ShapeType.Dot
|
||||
}
|
||||
|
||||
export interface CircleShape extends BaseShape {
|
||||
type: ShapeType.Circle
|
||||
radius: number
|
||||
}
|
||||
|
||||
export interface EllipseShape extends BaseShape {
|
||||
type: ShapeType.Ellipse
|
||||
radiusX: number
|
||||
|
@ -201,7 +195,6 @@ export interface GroupShape extends BaseShape {
|
|||
|
||||
export type MutableShape =
|
||||
| DotShape
|
||||
| CircleShape
|
||||
| EllipseShape
|
||||
| LineShape
|
||||
| RayShape
|
||||
|
@ -214,7 +207,6 @@ export type MutableShape =
|
|||
|
||||
export interface Shapes {
|
||||
[ShapeType.Dot]: Readonly<DotShape>
|
||||
[ShapeType.Circle]: Readonly<CircleShape>
|
||||
[ShapeType.Ellipse]: Readonly<EllipseShape>
|
||||
[ShapeType.Line]: Readonly<LineShape>
|
||||
[ShapeType.Ray]: Readonly<RayShape>
|
||||
|
@ -538,6 +530,13 @@ export interface ShapeUtility<K extends Shape> {
|
|||
handle: Partial<K['handles']>
|
||||
): ShapeUtility<K>
|
||||
|
||||
onDoublePointHandle(
|
||||
this: ShapeUtility<K>,
|
||||
shape: Mutable<K>,
|
||||
handle: keyof K['handles'],
|
||||
info: PointerInfo
|
||||
): ShapeUtility<K>
|
||||
|
||||
// Respond when a user double clicks the shape's bounds.
|
||||
onBoundsReset(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
|
||||
|
||||
|
|
Loading…
Reference in a new issue