Adds double-pointing handles action, toggled arrowheads, removes circles.

This commit is contained in:
Steve Ruiz 2021-06-22 22:06:51 +01:00
parent 07f96a8416
commit 7d14791d00
12 changed files with 227 additions and 368 deletions

View file

@ -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]
)

View file

@ -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
}
}

View file

@ -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,

View 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])
},
})
)
}

View file

@ -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,

View file

@ -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,

View file

@ -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())
),
}

View file

@ -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

View file

@ -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,
}
/**

View file

@ -82,6 +82,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
return this
},
onDoublePointHandle() {
return this
},
onDoubleFocus() {
return this
},

View file

@ -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
},

View file

@ -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>