[improvement] Improve rotation (#94)

* Fix rotation handle, rotation for arrows and shapes with handles

* Fix bug on draw bounds when cloning
This commit is contained in:
Steve Ruiz 2021-09-19 14:53:52 +01:00 committed by GitHub
parent bec693a1d9
commit 87d271d7aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 208 additions and 120 deletions

View file

@ -23,14 +23,22 @@ export function Bounds({
rotation, rotation,
isLocked, isLocked,
}: BoundsProps): JSX.Element { }: BoundsProps): JSX.Element {
const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom // Touch target size // Touch target size
const size = 8 / zoom // Touch target size const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom
// Handle size
const size = 8 / zoom
const smallDimension = Math.min(bounds.width, bounds.height) * zoom
// If the bounds are small, don't show the rotate handle
const showRotateHandle = !isLocked && smallDimension > 32
// If the bounds are very small, don't show the corner handles
const showHandles = !isLocked && smallDimension > 16
return ( return (
<Container bounds={bounds} rotation={rotation}> <Container bounds={bounds} rotation={rotation}>
<SVGContainer> <SVGContainer>
<CenterHandle bounds={bounds} isLocked={isLocked} /> <CenterHandle bounds={bounds} isLocked={isLocked} />
{!isLocked && ( {showHandles && (
<> <>
<EdgeHandle <EdgeHandle
targetSize={targetSize} targetSize={targetSize}
@ -80,7 +88,9 @@ export function Bounds({
bounds={bounds} bounds={bounds}
corner={TLBoundsCorner.BottomLeft} corner={TLBoundsCorner.BottomLeft}
/> />
<RotateHandle targetSize={targetSize} size={size} bounds={bounds} /> {showRotateHandle && (
<RotateHandle targetSize={targetSize} size={size} bounds={bounds} />
)}
</> </>
)} )}
</SVGContainer> </SVGContainer>

View file

@ -18,7 +18,7 @@ export const RotateHandle = React.memo(
className="tl-transparent" className="tl-transparent"
cx={bounds.width / 2} cx={bounds.width / 2}
cy={size * -2} cy={size * -2}
r={targetSize * 2} r={targetSize}
pointerEvents="all" pointerEvents="all"
{...events} {...events}
/> />

View file

@ -448,7 +448,7 @@ export class Utils {
* @param r * @param r
* @param segments * @param segments
*/ */
static clampToRotationToSegments(r: number, segments: number): number { static snapAngleToSegments(r: number, segments: number): number {
const seg = (Math.PI * 2) / segments const seg = (Math.PI * 2) / segments
return Math.floor((Utils.clampRadians(r) + seg / 2) / seg) * seg return Math.floor((Utils.clampRadians(r) + seg / 2) / seg) * seg
} }

View file

@ -348,11 +348,11 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
nextHandles['bend'] = { nextHandles['bend'] = {
...bend, ...bend,
point: Math.abs(bendDist) < 10 ? midPoint : point, point: Vec.round(Math.abs(bendDist) < 10 ? midPoint : point),
} }
return { return {
point: [bounds.minX, bounds.minY], point: Vec.round([bounds.minX, bounds.minY]),
handles: nextHandles, handles: nextHandles,
} }
}, },
@ -486,7 +486,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
const other = handle.id === 'start' ? shape.handles.end : shape.handles.start const other = handle.id === 'start' ? shape.handles.end : shape.handles.start
const angle = Vec.angle(other.point, point) const angle = Vec.angle(other.point, point)
const distance = Vec.dist(other.point, point) const distance = Vec.dist(other.point, point)
const newAngle = Utils.clampToRotationToSegments(angle, 24) const newAngle = Utils.snapAngleToSegments(angle, 24)
handle.point = Vec.nudgeAtAngle(other.point, newAngle, distance) handle.point = Vec.nudgeAtAngle(other.point, newAngle, distance)
} }
}) })

View file

@ -10,7 +10,7 @@ import { EASINGS } from '~state/utils'
const pointsBoundsCache = new WeakMap<DrawShape['points'], TLBounds>([]) const pointsBoundsCache = new WeakMap<DrawShape['points'], TLBounds>([])
const shapeBoundsCache = new Map<string, TLBounds>() const shapeBoundsCache = new Map<string, TLBounds>()
const rotatedCache = new WeakMap<DrawShape, number[][]>([]) const rotatedCache = new WeakMap<DrawShape, number[][]>([])
const pointCache = new WeakSet<DrawShape['point']>([]) const pointCache: Record<string, number[]> = {}
export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({ export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({
type: TLDrawShapeType.Draw, type: TLDrawShapeType.Draw,
@ -167,17 +167,17 @@ export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({
// previous bounds-from-points result if we can. // previous bounds-from-points result if we can.
const pointsHaveChanged = !pointsBoundsCache.has(shape.points) const pointsHaveChanged = !pointsBoundsCache.has(shape.points)
const pointHasChanged = !pointCache.has(shape.point) const pointHasChanged = !(pointCache[shape.id] === shape.point)
if (pointsHaveChanged) { if (pointsHaveChanged) {
// If the points have changed, then bust the points cache // If the points have changed, then bust the points cache
const bounds = Utils.getBoundsFromPoints(shape.points) const bounds = Utils.getBoundsFromPoints(shape.points)
pointsBoundsCache.set(shape.points, bounds) pointsBoundsCache.set(shape.points, bounds)
shapeBoundsCache.set(shape.id, Utils.translateBounds(bounds, shape.point)) shapeBoundsCache.set(shape.id, Utils.translateBounds(bounds, shape.point))
pointCache.add(shape.point) pointCache[shape.id] = shape.point
} else if (pointHasChanged && !pointsHaveChanged) { } else if (pointHasChanged && !pointsHaveChanged) {
// If the point have has changed, then bust the point cache // If the point have has changed, then bust the point cache
pointCache.add(shape.point) pointCache[shape.id] = shape.point
shapeBoundsCache.set( shapeBoundsCache.set(
shape.id, shape.id,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View file

@ -180,6 +180,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
hitTestBounds(shape, bounds) { hitTestBounds(shape, bounds) {
return ( return (
Utils.boundsContain(bounds, this.getBounds(shape)) ||
intersectBoundsEllipse( intersectBoundsEllipse(
bounds, bounds,
this.getCenter(shape), this.getCenter(shape),

View file

@ -86,20 +86,42 @@ export const Group = new ShapeUtil<GroupShape, SVGSVGElement, TLDrawMeta>(() =>
}, },
Indicator({ shape }) { Indicator({ shape }) {
const [width, height] = shape.size const { id, size } = shape
const sw = 2 const sw = 2
const w = Math.max(0, size[0] - sw / 2)
const h = Math.max(0, size[1] - sw / 2)
return ( const strokes: [number[], number[], number][] = [
<rect [[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
x={sw / 2} [[w, sw / 2], [w, h], h - sw / 2],
y={sw / 2} [[w, h], [sw / 2, h], w - sw / 2],
rx={1} [[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
ry={1} ]
width={Math.max(1, width - sw)}
height={Math.max(1, height - sw)} const paths = strokes.map(([start, end, length], i) => {
/> const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
) length,
sw,
DashStyle.Dotted
)
return (
<line
key={id + '_' + i}
x1={start[0]}
y1={start[1]}
x2={end[0]}
y2={end[1]}
strokeWidth={sw}
strokeLinecap="round"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
/>
)
})
return <g>{paths}</g>
}, },
shouldRender(prev, next) { shouldRender(prev, next) {

View file

@ -1,48 +1,46 @@
import { Utils } from '@tldraw/core' import { Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import type { TLDrawCommand, Data } from '~types' import type { TLDrawCommand, Data, TLDrawShape } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
const PI2 = Math.PI * 2 const PI2 = Math.PI * 2
export function rotate(data: Data, ids: string[], delta = -PI2 / 4): TLDrawCommand { export function rotate(data: Data, ids: string[], delta = -PI2 / 4): TLDrawCommand | void {
const { currentPageId } = data.appState const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const boundsForShapes = initialShapes.map((shape) => { // The shapes for the before patch
const utils = TLDR.getShapeUtils(shape) const before: Record<string, Partial<TLDrawShape>> = {}
return {
id: shape.id, // The shapes for the after patch
point: [...shape.point], const after: Record<string, Partial<TLDrawShape>> = {}
bounds: utils.getBounds(shape),
center: utils.getCenter(shape), // Find the shapes that we want to rotate.
rotation: shape.rotation, // We don't rotate groups: we rotate their children instead.
} const shapesToRotate = ids.flatMap((id) => {
const shape = TLDR.getShape(data, id, currentPageId)
return shape.children
? shape.children.map((childId) => TLDR.getShape(data, childId, currentPageId))
: shape
}) })
const commonBounds = Utils.getCommonBounds(boundsForShapes.map(({ bounds }) => bounds)) // Find the common center to all shapes
const commonBoundsCenter = Utils.getBoundsCenter(commonBounds) // This is the point that we'll rotate around
const origin = shapesToRotate.reduce((acc, shape) => {
return Vec.med(acc, TLDR.getCenter(shape))
}, TLDR.getCenter(shapesToRotate[0]))
const rotations = Object.fromEntries( // Find the rotate mutations for each shape
boundsForShapes.map(({ id, point, center, rotation }) => { shapesToRotate.forEach((shape) => {
const offset = Vec.sub(center, point) const change = TLDR.getRotatedShapeMutation(shape, TLDR.getCenter(shape), origin, delta)
const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, -(PI2 / 4)), offset) if (!change) return
const nextRotation = (PI2 + ((rotation || 0) + delta)) % PI2 before[shape.id] = TLDR.getBeforeShape(shape, change)
after[shape.id] = change
})
return [id, { point: nextPoint, rotation: nextRotation }] // Also rotate the bounds.
}) const beforeBoundsRotation = TLDR.getPageState(data, currentPageId).boundsRotation
)
const pageState = TLDR.getPageState(data, currentPageId) const afterBoundsRotation = Utils.clampRadians((beforeBoundsRotation || 0) + delta)
const prevBoundsRotation = pageState.boundsRotation
const nextBoundsRotation = (PI2 + ((pageState.boundsRotation || 0) + delta)) % PI2
const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => rotations[shape.id],
currentPageId
)
return { return {
id: 'rotate', id: 'rotate',
@ -52,7 +50,7 @@ export function rotate(data: Data, ids: string[], delta = -PI2 / 4): TLDrawComma
[currentPageId]: { shapes: before }, [currentPageId]: { shapes: before },
}, },
pageStates: { pageStates: {
[currentPageId]: { boundsRotation: prevBoundsRotation }, [currentPageId]: { boundsRotation: beforeBoundsRotation },
}, },
}, },
}, },
@ -62,7 +60,7 @@ export function rotate(data: Data, ids: string[], delta = -PI2 / 4): TLDrawComma
[currentPageId]: { shapes: after }, [currentPageId]: { shapes: after },
}, },
pageStates: { pageStates: {
[currentPageId]: { boundsRotation: nextBoundsRotation }, [currentPageId]: { boundsRotation: afterBoundsRotation },
}, },
}, },
}, },

View file

@ -4,7 +4,7 @@ import { Session, TLDrawShape, TLDrawStatus } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
const PI2 = Math.PI * 2 const centerCache = new WeakMap<string[], number[]>()
export class RotateSession implements Session { export class RotateSession implements Session {
id = 'rotate' id = 'rotate'
@ -12,48 +12,45 @@ export class RotateSession implements Session {
delta = [0, 0] delta = [0, 0]
origin: number[] origin: number[]
snapshot: RotateSnapshot snapshot: RotateSnapshot
prev = 0 initialAngle: number
changes: Record<string, Partial<TLDrawShape>> = {}
constructor(data: Data, point: number[]) { constructor(data: Data, point: number[]) {
this.origin = point this.origin = point
this.snapshot = getRotateSnapshot(data) this.snapshot = getRotateSnapshot(data)
this.initialAngle = Vec.angle(this.snapshot.commonBoundsCenter, this.origin)
} }
start = () => void null start = () => void null
update = (data: Data, point: number[], isLocked = false) => { update = (data: Data, point: number[], isLocked = false) => {
const { commonBoundsCenter, initialShapes } = this.snapshot const { commonBoundsCenter, initialShapes } = this.snapshot
const pageId = data.appState.currentPageId const pageId = data.appState.currentPageId
const pageState = TLDR.getPageState(data, pageId)
const shapes: Record<string, Partial<TLDrawShape>> = {} const shapes: Record<string, Partial<TLDrawShape>> = {}
const a1 = Vec.angle(commonBoundsCenter, this.origin) const nextDirection = Vec.angle(commonBoundsCenter, point) - this.initialAngle
const a2 = Vec.angle(commonBoundsCenter, point)
let rot = a2 - a1 let nextBoundsRotation = this.snapshot.boundsRotation + nextDirection
this.prev = rot
if (isLocked) { if (isLocked) {
rot = Utils.clampToRotationToSegments(rot, 24) nextBoundsRotation = Utils.snapAngleToSegments(nextBoundsRotation, 24)
} }
pageState.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2 const delta = nextBoundsRotation - this.snapshot.boundsRotation
initialShapes.forEach(({ id, center, offset, shape: { rotation = 0 } }) => { // Update the shapes
const nextRotation = isLocked initialShapes.forEach(({ id, center, shape }) => {
? Utils.clampToRotationToSegments(rotation + rot, 24) const change = TLDR.getRotatedShapeMutation(shape, center, commonBoundsCenter, delta)
: rotation + rot
const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset) if (change) {
shapes[id] = change
shapes[id] = {
point: nextPoint,
rotation: (PI2 + nextRotation) % PI2,
} }
}) })
this.changes = shapes
return { return {
document: { document: {
pages: { pages: {
@ -61,6 +58,9 @@ export class RotateSession implements Session {
shapes, shapes,
}, },
}, },
pageState: {
boundsRotation: Utils.clampRadians(nextBoundsRotation),
},
}, },
} }
} }
@ -87,18 +87,16 @@ export class RotateSession implements Session {
} }
complete(data: Data) { complete(data: Data) {
const { hasUnlockedShapes, initialShapes } = this.snapshot const { initialShapes } = this.snapshot
const pageId = data.appState.currentPageId const pageId = data.appState.currentPageId
if (!hasUnlockedShapes) return data // if (!hasUnlockedShapes) return data
const beforeShapes = {} as Record<string, Partial<TLDrawShape>> const beforeShapes = {} as Record<string, Partial<TLDrawShape>>
const afterShapes = {} as Record<string, Partial<TLDrawShape>> const afterShapes = this.changes
initialShapes.forEach(({ id, shape: { point, rotation } }) => { initialShapes.forEach(({ id, shape: { point, rotation, handles } }) => {
beforeShapes[id] = { point, rotation } beforeShapes[id] = { point, rotation, handles }
const afterShape = TLDR.getShape(data, id, pageId)
afterShapes[id] = { point: afterShape.point, rotation: afterShape.rotation }
}) })
return { return {
@ -131,42 +129,30 @@ export function getRotateSnapshot(data: Data) {
const pageState = TLDR.getPageState(data, currentPageId) const pageState = TLDR.getPageState(data, currentPageId)
const initialShapes = TLDR.getSelectedBranchSnapshot(data, currentPageId) const initialShapes = TLDR.getSelectedBranchSnapshot(data, currentPageId)
if (initialShapes.length === 0) { const commonBoundsCenter = Utils.getFromCache(centerCache, pageState.selectedIds, () => {
throw Error('No selected shapes!') if (initialShapes.length === 0) {
} throw Error('No selected shapes!')
}
const hasUnlockedShapes = initialShapes.length > 0 const shapesBounds = Object.fromEntries(
initialShapes.map((shape) => [shape.id, TLDR.getBounds(shape)])
)
const shapesBounds = Object.fromEntries( const bounds = Utils.getCommonBounds(Object.values(shapesBounds))
initialShapes.map((shape) => [shape.id, TLDR.getBounds(shape)])
)
const rotatedBounds = Object.fromEntries( return Utils.getBoundsCenter(bounds)
initialShapes.map((shape) => [shape.id, TLDR.getRotatedBounds(shape)]) })
)
const bounds = Utils.getCommonBounds(Object.values(shapesBounds))
const commonBoundsCenter = Utils.getBoundsCenter(bounds)
return { return {
hasUnlockedShapes,
boundsRotation: pageState.boundsRotation || 0, boundsRotation: pageState.boundsRotation || 0,
commonBoundsCenter, commonBoundsCenter,
initialShapes: initialShapes initialShapes: initialShapes
.filter((shape) => shape.children === undefined) .filter((shape) => shape.children === undefined)
.map((shape) => { .map((shape) => {
const bounds = TLDR.getBounds(shape) const center = TLDR.getShapeUtils(shape).getCenter(shape)
const center = Utils.getBoundsCenter(bounds)
const offset = Vec.sub(center, shape.point)
const rotationOffset = Vec.sub(center, Utils.getBoundsCenter(rotatedBounds[shape.id]))
return { return {
id: shape.id, id: shape.id,
shape: Utils.deepClone(shape), shape,
offset,
rotationOffset,
center, center,
} }
}), }),

View file

@ -10,6 +10,7 @@ import {
TLDrawStatus, TLDrawStatus,
ArrowShape, ArrowShape,
GroupShape, GroupShape,
TLDrawPatch,
} from '~types' } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
import type { Patch } from 'rko' import type { Patch } from 'rko'
@ -115,14 +116,14 @@ export class TranslateSession implements Session {
} }
// Either way, move the clones // Either way, move the clones
clones.forEach((shape) => { clones.forEach((clone) => {
const current = (nextShapes[shape.id] || const current = (nextShapes[clone.id] ||
TLDR.getShape(data, shape.id, data.appState.currentPageId)) as TLDrawShape TLDR.getShape(data, clone.id, data.appState.currentPageId)) as TLDrawShape
if (!current.point) throw Error('No point on that clone!') if (!current.point) throw Error('No point on that clone!')
nextShapes[shape.id] = { nextShapes[clone.id] = {
...nextShapes[shape.id], ...nextShapes[clone.id],
point: Vec.round(Vec.add(current.point, trueDelta)), point: Vec.round(Vec.add(current.point, trueDelta)),
} }
}) })
@ -391,12 +392,14 @@ export function getTranslateSnapshot(data: Data) {
cloneMap[shape.id] = newId cloneMap[shape.id] = newId
clones.push({ const clone = {
...Utils.deepClone(shape), ...Utils.deepClone(shape),
id: newId, id: newId,
parentId: shape.parentId, parentId: shape.parentId,
childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId), childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId),
}) }
clones.push(clone)
}) })
clones.forEach((clone) => { clones.forEach((clone) => {
@ -425,6 +428,7 @@ export function getTranslateSnapshot(data: Data) {
if (clonedShapeIds.has(binding.fromId)) { if (clonedShapeIds.has(binding.fromId)) {
if (clonedShapeIds.has(binding.toId)) { if (clonedShapeIds.has(binding.toId)) {
const cloneId = Utils.uniqueId() const cloneId = Utils.uniqueId()
const cloneBinding = { const cloneBinding = {
...Utils.deepClone(binding), ...Utils.deepClone(binding),
id: cloneId, id: cloneId,

View file

@ -79,6 +79,10 @@ export class TLDR {
return TLDR.getPage(data, pageId).shapes[shapeId] as T return TLDR.getPage(data, pageId).shapes[shapeId] as T
} }
static getCenter<T extends TLDrawShape>(shape: T) {
return TLDR.getShapeUtils(shape).getCenter(shape)
}
static getBounds<T extends TLDrawShape>(shape: T) { static getBounds<T extends TLDrawShape>(shape: T) {
return TLDR.getShapeUtils(shape).getBounds(shape) return TLDR.getShapeUtils(shape).getBounds(shape)
} }
@ -308,6 +312,12 @@ export class TLDR {
/* Mutations */ /* Mutations */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
static getBeforeShape<T extends TLDrawShape>(shape: T, change: Partial<T>): Partial<T> {
return Object.fromEntries(
Object.keys(change).map((k) => [k, shape[k as keyof T]])
) as Partial<T>
}
static mutateShapes<T extends TLDrawShape>( static mutateShapes<T extends TLDrawShape>(
data: Data, data: Data,
ids: string[], ids: string[],
@ -325,9 +335,7 @@ export class TLDR {
const shape = TLDR.getShape<T>(data, id, pageId) const shape = TLDR.getShape<T>(data, id, pageId)
const change = fn(shape, i) const change = fn(shape, i)
if (change) { if (change) {
beforeShapes[id] = Object.fromEntries( beforeShapes[id] = TLDR.getBeforeShape(shape, change)
Object.keys(change).map((key) => [key, shape[key as keyof T]])
) as Partial<T>
afterShapes[id] = change afterShapes[id] = change
} }
}) })
@ -559,6 +567,61 @@ export class TLDR {
return { ...shape, ...delta } return { ...shape, ...delta }
} }
/**
* Rotate a shape around an origin point.
* @param shape a shape.
* @param center the shape's center in page space.
* @param origin the page point to rotate around.
* @param rotation the amount to rotate the shape.
*/
static getRotatedShapeMutation<T extends TLDrawShape>(
shape: T, // in page space
center: number[], // in page space
origin: number[], // in page space (probably the center of common bounds)
delta: number // The shape's rotation delta
): Partial<T> | void {
// The shape's center relative to the shape's point
const relativeCenter = Vec.sub(center, shape.point)
// Rotate the center around the origin
const rotatedCenter = Vec.rotWith(center, origin, delta)
// Get the top left point relative to the rotated center
const nextPoint = Vec.round(Vec.sub(rotatedCenter, relativeCenter))
// If the shape has handles, we need to rotate the handles instead
// of rotating the shape. Shapes with handles should never be rotated,
// because that makes a lot of other things incredible difficult.
if (shape.handles !== undefined) {
const change = this.getShapeUtils(shape).onHandleChange(
// Base the change on a shape with the next point
{ ...shape, point: nextPoint },
Object.fromEntries(
Object.entries(shape.handles).map(([handleId, handle]) => {
// Rotate each handle's point around the shape's center
// (in relative shape space, as the handle's point will be).
const point = Vec.round(Vec.rotWith(handle.point, relativeCenter, delta))
return [handleId, { ...handle, point }]
})
) as T['handles'],
{ shiftKey: false }
)
return change
}
// If the shape has no handles, move the shape to the new point
// and set the rotation.
// Clamp the next rotation between 0 and PI2
const nextRotation = Utils.clampRadians((shape.rotation || 0) + delta)
return {
point: nextPoint,
rotation: nextRotation,
} as Partial<T>
}
/* -------------------------------------------------- */ /* -------------------------------------------------- */
/* Parents */ /* Parents */
/* -------------------------------------------------- */ /* -------------------------------------------------- */

View file

@ -582,7 +582,7 @@ export class TLDrawState extends StateManager<Data> {
* @todo * @todo
*/ */
saveProject = () => { saveProject = () => {
// TODO this.persist()
} }
/** /**
@ -1906,7 +1906,9 @@ export class TLDrawState extends StateManager<Data> {
*/ */
rotate = (delta = Math.PI * -0.5, ids = this.selectedIds): this => { rotate = (delta = Math.PI * -0.5, ids = this.selectedIds): this => {
if (ids.length === 0) return this if (ids.length === 0) return this
return this.setState(Commands.rotate(this.state, ids, delta)) const change = Commands.rotate(this.state, ids, delta)
if (!change) return this
return this.setState(change)
} }
/** /**

View file

@ -1,5 +1,7 @@
import type { Easing } from '~types' import type { Easing } from '~types'
export const PI2 = Math.PI * 2
export const EASINGS: Record<Easing, (t: number) => number> = { export const EASINGS: Record<Easing, (t: number) => number> = {
linear: (t) => t, linear: (t) => t,
easeInQuad: (t) => t * t, easeInQuad: (t) => t * t,