[fix] rotate center (#213)

* fixes rotate center after translating / transforming

* Adds test, fixes issue on undo/redo

* Update tsconfig.base.json
This commit is contained in:
Steve Ruiz 2021-10-30 10:04:33 +01:00 committed by GitHub
parent cb96740dbc
commit 12e425ddc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 95 additions and 35 deletions

View file

@ -66,7 +66,7 @@
"@radix-ui/react-tooltip": "^0.1.1", "@radix-ui/react-tooltip": "^0.1.1",
"@stitches/core": "^1.2.5", "@stitches/core": "^1.2.5",
"@stitches/react": "^1.0.0", "@stitches/react": "^1.0.0",
"@tldraw/core": "^0.1.9", "@tldraw/core": "^0.1.10",
"@tldraw/intersect": "^0.0.132", "@tldraw/intersect": "^0.0.132",
"@tldraw/vec": "^0.0.132", "@tldraw/vec": "^0.0.132",
"perfect-freehand": "^1.0.16", "perfect-freehand": "^1.0.16",

View file

@ -1,10 +1,13 @@
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import type { Data, TLDrawCommand, PagePartial } from '~types' import { Data, TLDrawCommand, PagePartial, Session } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function translate(data: Data, ids: string[], delta: number[]): TLDrawCommand { export function translate(data: Data, ids: string[], delta: number[]): TLDrawCommand {
const { currentPageId } = data.appState const { currentPageId } = data.appState
// Clear session cache
Session.cache.selectedIds = TLDR.getSelectedIds(data, data.appState.currentPageId)
const before: PagePartial = { const before: PagePartial = {
shapes: {}, shapes: {},
bindings: {}, bindings: {},

View file

@ -119,16 +119,64 @@ describe('Rotate session', () => {
) )
) )
tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]) tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
const centerAfter = Vec.round( const centerAfterA = Vec.round(
Utils.getBoundsCenter(
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
)
)
tlstate.startSession(SessionType.Rotate, [100, 0]).updateSession([50, 0]).completeSession()
const centerAfterB = Vec.round(
Utils.getBoundsCenter( Utils.getBoundsCenter(
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id))) Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
) )
) )
expect(tlstate.getShape('rect1').rotation) expect(tlstate.getShape('rect1').rotation)
expect(centerBefore).toStrictEqual(centerAfter) expect(centerBefore).toStrictEqual(centerAfterA)
expect(centerAfterA).toStrictEqual(centerAfterB)
})
it.todo('clears the cached center after transforming')
it.todo('clears the cached center after translating')
it.todo('clears the cached center after undoing')
it.todo('clears the cached center after redoing')
it.todo('clears the cached center after any command other than a rotate command, tbh')
it('changes the center after nudging', () => {
const tlstate = new TLDrawState().loadDocument(mockDocument).select('rect1', 'rect2')
const centerBefore = Vec.round(
Utils.getBoundsCenter(
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
)
)
tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
const centerAfterA = Vec.round(
Utils.getBoundsCenter(
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
)
)
expect(tlstate.getShape('rect1').rotation)
expect(centerBefore).toStrictEqual(centerAfterA)
tlstate.selectAll().nudge([10, 10])
tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
const centerAfterB = Vec.round(
Utils.getBoundsCenter(
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
)
)
expect(centerAfterB).not.toStrictEqual(centerAfterA)
}) })
}) })
}) })

View file

@ -4,8 +4,6 @@ import { Session, SessionType, 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 centerCache = new WeakMap<string[], number[]>()
export class RotateSession extends Session { export class RotateSession extends Session {
static type = SessionType.Rotate static type = SessionType.Rotate
status = TLDrawStatus.Transforming status = TLDrawStatus.Transforming
@ -17,6 +15,7 @@ export class RotateSession extends Session {
constructor(data: Data, viewport: TLBounds, point: number[]) { constructor(data: Data, viewport: TLBounds, point: number[]) {
super(viewport) super(viewport)
this.origin = point this.origin = point
this.snapshot = getRotateSnapshot(data) this.snapshot = getRotateSnapshot(data)
this.initialAngle = Vec.angle(this.snapshot.commonBoundsCenter, this.origin) this.initialAngle = Vec.angle(this.snapshot.commonBoundsCenter, this.origin)
@ -134,19 +133,25 @@ 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)
const commonBoundsCenter = Utils.getFromCache(centerCache, pageState.selectedIds, () => { if (initialShapes.length === 0) {
if (initialShapes.length === 0) { throw Error('No selected shapes!')
throw Error('No selected shapes!') }
let commonBoundsCenter: number[]
if (Session.cache.selectedIds === pageState.selectedIds) {
if (Session.cache.center === undefined) {
throw Error('The center was not added to the cache!')
} }
const shapesBounds = Object.fromEntries( commonBoundsCenter = Session.cache.center
initialShapes.map((shape) => [shape.id, TLDR.getBounds(shape)]) } else {
commonBoundsCenter = Utils.getBoundsCenter(
Utils.getCommonBounds(initialShapes.map(TLDR.getBounds))
) )
Session.cache.selectedIds = pageState.selectedIds
const bounds = Utils.getCommonBounds(Object.values(shapesBounds)) Session.cache.center = commonBoundsCenter
}
return Utils.getBoundsCenter(bounds)
})
return { return {
commonBoundsCenter, commonBoundsCenter,

View file

@ -47,6 +47,7 @@ export class TransformSingleSession extends Session {
this.transformType = transformType this.transformType = transformType
this.snapshot = getTransformSingleSnapshot(data, transformType) this.snapshot = getTransformSingleSnapshot(data, transformType)
this.isCreate = isCreate this.isCreate = isCreate
Session.cache.selectedIds = [...this.snapshot.initialShape.id]
} }
start = (data: Data) => { start = (data: Data) => {

View file

@ -43,6 +43,7 @@ export class TransformSession extends Session {
this.snapshot = getTransformSnapshot(data, transformType) this.snapshot = getTransformSnapshot(data, transformType)
this.isCreate = isCreate this.isCreate = isCreate
this.initialSelectedIds = TLDR.getSelectedIds(data, data.appState.currentPageId) this.initialSelectedIds = TLDR.getSelectedIds(data, data.appState.currentPageId)
Session.cache.selectedIds = [...this.initialSelectedIds]
} }
start = (data: Data) => { start = (data: Data) => {
@ -59,8 +60,6 @@ export class TransformSession extends Session {
const shapes = {} as Record<string, TLDrawShape> const shapes = {} as Record<string, TLDrawShape>
const pageState = TLDR.getPageState(data, data.appState.currentPageId)
const delta = Vec.sub(point, this.origin) const delta = Vec.sub(point, this.origin)
let newBounds = Utils.getTransformedBoundingBox( let newBounds = Utils.getTransformedBoundingBox(

View file

@ -12,7 +12,6 @@ import {
GroupShape, GroupShape,
SessionType, SessionType,
ArrowBinding, ArrowBinding,
TLDrawShapeType,
} from '~types' } from '~types'
import { SLOW_SPEED, SNAP_DISTANCE } from '~constants' import { SLOW_SPEED, SNAP_DISTANCE } from '~constants'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
@ -70,6 +69,7 @@ export class TranslateSession extends Session {
this.snapshot = getTranslateSnapshot(data, isLinked) this.snapshot = getTranslateSnapshot(data, isLinked)
this.isCreate = isCreate this.isCreate = isCreate
this.isLinked = isLinked this.isLinked = isLinked
Session.cache.selectedIds = [...TLDR.getSelectedIds(data, data.appState.currentPageId)]
} }
start = (data: Data) => { start = (data: Data) => {
@ -94,14 +94,7 @@ export class TranslateSession extends Session {
} }
} }
update = ( update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
data: Data,
point: number[],
shiftKey = false,
altKey = false,
metaKey = false,
viewPort = {} as TLBounds
) => {
const { selectedIds, initialParentChildren, initialShapes, bindingsToDelete } = this.snapshot const { selectedIds, initialParentChildren, initialShapes, bindingsToDelete } = this.snapshot
const { currentPageId } = data.appState const { currentPageId } = data.appState

View file

@ -347,6 +347,10 @@ export class TLDrawState extends StateManager<Data> {
this.clearSelectHistory() this.clearSelectHistory()
} }
if (id.startsWith('undo') || id.startsWith('redo')) {
Session.cache.selectedIds = [...this.selectedIds]
}
this._onChange?.(this, state, id) this._onChange?.(this, state, id)
} }
@ -1670,6 +1674,7 @@ export class TLDrawState extends StateManager<Data> {
} }
const Session = getSession(type) const Session = getSession(type)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.session = new Session(this.state, this.viewport, ...args) this.session = new Session(this.state, this.viewport, ...args)

View file

@ -155,6 +155,14 @@ export abstract class Session {
updateViewport = (viewport: TLBounds) => { updateViewport = (viewport: TLBounds) => {
this.viewport = viewport this.viewport = viewport
} }
static cache: {
selectedIds: string[]
center: number[]
} = {
selectedIds: [],
center: [0, 0],
}
} }
export enum TLDrawStatus { export enum TLDrawStatus {

View file

@ -11,10 +11,7 @@ async function main() {
bundle: true, bundle: true,
format: 'cjs', format: 'cjs',
target: 'es6', target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json', tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
incremental: true, incremental: true,
watch: { watch: {
onRebuild(error) { onRebuild(error) {

View file

@ -11,6 +11,7 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"importHelpers": true, "importHelpers": true,
"importsNotUsedAsValues": "error", "importsNotUsedAsValues": "error",
"resolveJsonModule": true,
"incremental": true, "incremental": true,
"jsx": "preserve", "jsx": "preserve",
"lib": ["dom", "esnext"], "lib": ["dom", "esnext"],

View file

@ -3722,10 +3722,10 @@
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0" "@testing-library/dom" "^8.0.0"
"@tldraw/core@^0.1.9": "@tldraw/core@^0.1.10":
version "0.1.9" version "0.1.10"
resolved "https://registry.yarnpkg.com/@tldraw/core/-/core-0.1.9.tgz#006748ee0c395ef15756ee1aa3c93b7eef109432" resolved "https://registry.yarnpkg.com/@tldraw/core/-/core-0.1.10.tgz#fe6aa880b619361473ebccb5c932082efb535518"
integrity sha512-3vzFceettMZVkVM+ASaAFyFy2YibwnEkIcIiWTmKER+5R0HIEKnwDuH+zXiv+C1dcbagUvArHRw0AiuzA5ft8g== integrity sha512-kLqCDQHRykWfskMvcbSn3Ll8Ssd46hMmMdTpqw1O3i3AV2zuWDYkdgGD3PO6PRSkkMZyGNr5/zpkU+Yrk5DYZQ==
dependencies: dependencies:
"@use-gesture/react" "^10.0.2" "@use-gesture/react" "^10.0.2"