From 7d14791d00184b4a45b37d35801525f1c97c88f9 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 22 Jun 2021 22:06:51 +0100 Subject: [PATCH] Adds double-pointing handles action, toggled arrowheads, removes circles. --- hooks/useHandleEvents.ts | 13 +- state/code/circle.ts | 41 ---- state/code/generate.ts | 2 - state/commands/double-point-handle.ts | 33 +++ state/commands/index.ts | 12 +- state/inputs.tsx | 8 +- state/shape-utils/arrow.tsx | 284 +++++++++++++------------- state/shape-utils/circle.tsx | 120 ----------- state/shape-utils/index.tsx | 10 +- state/shape-utils/register.tsx | 4 + state/state.ts | 53 ++--- types.ts | 15 +- 12 files changed, 227 insertions(+), 368 deletions(-) delete mode 100644 state/code/circle.ts create mode 100644 state/commands/double-point-handle.ts delete mode 100644 state/shape-utils/circle.tsx diff --git a/hooks/useHandleEvents.ts b/hooks/useHandleEvents.ts index 1f97968f4..ad5b21bde 100644 --- a/hooks/useHandleEvents.ts +++ b/hooks/useHandleEvents.ts @@ -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] ) diff --git a/state/code/circle.ts b/state/code/circle.ts deleted file mode 100644 index fc87768ec..000000000 --- a/state/code/circle.ts +++ /dev/null @@ -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 { - constructor(props = {} as Partial) { - 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 - } -} diff --git a/state/code/generate.ts b/state/code/generate.ts index 7bd9f2408..2fdb3d87c 100644 --- a/state/code/generate.ts +++ b/state/code/generate.ts @@ -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, diff --git a/state/commands/double-point-handle.ts b/state/commands/double-point-handle.ts new file mode 100644 index 000000000..a553ffdbf --- /dev/null +++ b/state/commands/double-point-handle.ts @@ -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]) + }, + }) + ) +} diff --git a/state/commands/index.ts b/state/commands/index.ts index d3b61f7ef..14406eb37 100644 --- a/state/commands/index.ts +++ b/state/commands/index.ts @@ -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, diff --git a/state/inputs.tsx b/state/inputs.tsx index eff6dea31..385fd19f8 100644 --- a/state/inputs.tsx +++ b/state/inputs.tsx @@ -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, diff --git a/state/shape-utils/arrow.tsx b/state/shape-utils/arrow.tsx index 00ae9007a..3079cbd0f 100644 --- a/state/shape-utils/arrow.tsx +++ b/state/shape-utils/arrow.tsx @@ -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({ handles, decorations: { start: null, - end: null, middle: null, + end: Decoration.Arrow, }, ...props, style: { @@ -98,13 +104,19 @@ const arrow = registerShapeUtils({ 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({ 2 ) - return ( - - {/* Improves hit testing */} + startAngle = Math.PI + + endAngle = 0 + + shaftPath = ( + <> ({ strokeDashoffset="none" strokeLinecap="round" /> - {/* Arrowshaft */} ({ strokeDasharray={strokeDasharray} strokeDashoffset={strokeDashoffset} strokeLinecap="round" - /> - {/* Arrowhead */} - {style.dash !== DashStyle.Solid && ( - - )} - + > + ) - } + } 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 = ( + <> + + + ) } - 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 ( - {/* Improves hit testing */} - - {/* Arrow Shaft */} - - {/* Arrowhead */} - + {shaftPath} + {shape.decorations.start === Decoration.Arrow && ( + + )} + {shape.decorations.end === Decoration.Arrow && ( + + )} ) }, @@ -347,10 +368,29 @@ const arrow = registerShapeUtils({ 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()) ), } diff --git a/state/shape-utils/circle.tsx b/state/shape-utils/circle.tsx deleted file mode 100644 index be32bd4fa..000000000 --- a/state/shape-utils/circle.tsx +++ /dev/null @@ -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({ - 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 ( - - ) - }, - - 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 diff --git a/state/shape-utils/index.tsx b/state/shape-utils/index.tsx index a4ee9f722..0c4fd623d 100644 --- a/state/shape-utils/index.tsx +++ b/state/shape-utils/index.tsx @@ -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.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, } /** diff --git a/state/shape-utils/register.tsx b/state/shape-utils/register.tsx index 7c964a02d..ddd7a1060 100644 --- a/state/shape-utils/register.tsx +++ b/state/shape-utils/register.tsx @@ -82,6 +82,10 @@ function getDefaultShapeUtil(): ShapeUtility { return this }, + onDoublePointHandle() { + return this + }, + onDoubleFocus() { return this }, diff --git a/state/state.ts b/state/state.ts index bde349306..208220dd7 100644 --- a/state/state.ts +++ b/state/state.ts @@ -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 }, diff --git a/types.ts b/types.ts index 638a77bfd..4fc82cd21 100644 --- a/types.ts +++ b/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 - [ShapeType.Circle]: Readonly [ShapeType.Ellipse]: Readonly [ShapeType.Line]: Readonly [ShapeType.Ray]: Readonly @@ -538,6 +530,13 @@ export interface ShapeUtility { handle: Partial ): ShapeUtility + onDoublePointHandle( + this: ShapeUtility, + shape: Mutable, + handle: keyof K['handles'], + info: PointerInfo + ): ShapeUtility + // Respond when a user double clicks the shape's bounds. onBoundsReset(this: ShapeUtility, shape: Mutable): ShapeUtility