Adds reset bounds

This commit is contained in:
Steve Ruiz 2021-09-06 12:07:15 +01:00
parent c1bb554103
commit ff826712c6
11 changed files with 217 additions and 85 deletions

View file

@ -8,19 +8,7 @@ export default function Editor(): JSX.Element {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.tlstate = state window.tlstate = state
rTLDrawState.current = state rTLDrawState.current = state
state.selectAll()
state.createShapes({
id: 'rect1',
type: 'rectangle',
point: [100, 100],
size: [200, 200],
})
state.updateShapes({
id: 'rect1',
point: [150, 150],
})
}, []) }, [])
return <TLDraw id="tldraw" onMount={handleMount} /> return <TLDraw id="tldraw" onMount={handleMount} />

View file

@ -254,7 +254,7 @@ export class Text extends TLDrawShapeUtil<TextShape> {
const bounds = Utils.getFromCache(this.boundsCache, shape, () => { const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
if (!melm) { if (!melm) {
// We're in SSR // We're in SSR
return { minX: 0, minY: 0, maxX: 0, maxY: 0, width: 0, height: 0 } return { minX: 0, minY: 0, maxX: 10, maxY: 0, width: 0, height: 0 }
} }
melm.innerHTML = `${shape.text}&zwj;` melm.innerHTML = `${shape.text}&zwj;`
@ -340,9 +340,13 @@ export class Text extends TLDrawShapeUtil<TextShape> {
onBoundsReset(shape: TextShape): Partial<TextShape> { onBoundsReset(shape: TextShape): Partial<TextShape> {
const center = this.getCenter(shape) const center = this.getCenter(shape)
this.boundsCache.delete(shape) const newCenter = this.getCenter({
...shape,
const newCenter = this.getCenter(shape) style: {
...shape.style,
scale: 1,
},
})
return { return {
style: { style: {

View file

@ -5,8 +5,11 @@ import type { RectangleShape } from '~types'
describe('Flip command', () => { describe('Flip command', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => { beforeEach(() => {
tlstate.loadDocument(mockDocument) tlstate.loadDocument(mockDocument)
})
it('does, undoes and redoes command', () => {
tlstate.select('rect1', 'rect2') tlstate.select('rect1', 'rect2')
tlstate.flipHorizontal() tlstate.flipHorizontal()
@ -22,7 +25,6 @@ describe('Flip command', () => {
}) })
it('flips horizontally', () => { it('flips horizontally', () => {
tlstate.loadDocument(mockDocument)
tlstate.select('rect1', 'rect2') tlstate.select('rect1', 'rect2')
tlstate.flipHorizontal() tlstate.flipHorizontal()
@ -30,7 +32,6 @@ describe('Flip command', () => {
}) })
it('flips vertically', () => { it('flips vertically', () => {
tlstate.loadDocument(mockDocument)
tlstate.select('rect1', 'rect2') tlstate.select('rect1', 'rect2')
tlstate.flipVertical() tlstate.flipVertical()

View file

@ -12,6 +12,7 @@ export * from './group'
export * from './move-to-page' export * from './move-to-page'
export * from './move' export * from './move'
export * from './rename-page' export * from './rename-page'
export * from './reset-bounds'
export * from './rotate' export * from './rotate'
export * from './stretch' export * from './stretch'
export * from './style' export * from './style'

View file

@ -0,0 +1 @@
export * from './reset-bounds.command'

View file

@ -0,0 +1,58 @@
import { TLBoundsCorner, Utils } from '@tldraw/core'
import { TLDrawState } from '~state'
import { TLDR } from '~state/tldr'
import { mockDocument } from '~test'
import { TLDrawShapeType } from '~types'
describe('Reset bounds command', () => {
const tlstate = new TLDrawState()
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
it('does, undoes and redoes command', () => {
tlstate.createShapes({
id: 'text1',
type: TLDrawShapeType.Text,
point: [0, 0],
text: 'Hello World',
})
// Scale is undefined by default
expect(tlstate.getShape('text1').style.scale).toBeUndefined()
// Transform the shape in order to change its point and scale
tlstate
.select('text1')
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
.updateTransformSession([-100, -100], false, false)
.completeSession()
const scale = tlstate.getShape('text1').style.scale
const bounds = TLDR.getBounds(tlstate.getShape('text1'))
const center = Utils.getBoundsCenter(bounds)
expect(scale).not.toBe(1)
expect(Number.isNaN(scale)).toBe(false)
// Reset the bounds
tlstate.resetBounds(['text1'])
const newScale = tlstate.getShape('text1').style.scale
const newBounds = TLDR.getBounds(tlstate.getShape('text1'))
const newCenter = Utils.getBoundsCenter(newBounds)
console.log(newScale)
expect(newScale).toBe(1)
expect(newCenter).toStrictEqual(center) // The centers should be the same
tlstate.undo()
tlstate.redo()
})
})

View file

@ -0,0 +1,29 @@
import type { Data, TLDrawCommand } from '~types'
import { TLDR } from '~state/tldr'
export function resetBounds(data: Data, ids: string[], pageId: string): TLDrawCommand {
const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => TLDR.getShapeUtils(shape).onBoundsReset(shape),
pageId
)
return {
id: 'reset_bounds',
before: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: before },
},
},
},
after: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: after },
},
},
},
}
}

View file

@ -23,14 +23,9 @@ export class RotateSession implements Session {
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 page = TLDR.getPage(data, pageId)
const pageState = TLDR.getPageState(data, pageId) const pageState = TLDR.getPageState(data, pageId)
const shapes: Record<string, TLDrawShape> = {} const shapes: Record<string, Partial<TLDrawShape>> = {}
for (const { id, shape } of initialShapes) {
shapes[id] = shape
}
const a1 = Vec.angle(commonBoundsCenter, this.origin) const a1 = Vec.angle(commonBoundsCenter, this.origin)
const a2 = Vec.angle(commonBoundsCenter, point) const a2 = Vec.angle(commonBoundsCenter, point)
@ -46,23 +41,16 @@ export class RotateSession implements Session {
pageState.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2 pageState.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2
initialShapes.forEach(({ id, center, offset, shape: { rotation = 0 } }) => { initialShapes.forEach(({ id, center, offset, shape: { rotation = 0 } }) => {
const shape = page.shapes[id]
const nextRotation = isLocked const nextRotation = isLocked
? Utils.clampToRotationToSegments(rotation + rot, 24) ? Utils.clampToRotationToSegments(rotation + rot, 24)
: rotation + rot : rotation + rot
const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset) const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset)
shapes[id] = TLDR.mutate( shapes[id] = {
data,
shape,
{
point: nextPoint, point: nextPoint,
rotation: (PI2 + nextRotation) % PI2, rotation: (PI2 + nextRotation) % PI2,
}, }
pageId
)
}) })
return { return {

View file

@ -407,7 +407,7 @@ export class TLDR {
static mutateShapes<T extends TLDrawShape>( static mutateShapes<T extends TLDrawShape>(
data: Data, data: Data,
ids: string[], ids: string[],
fn: (shape: T, i: number) => Partial<T>, fn: (shape: T, i: number) => Partial<T> | void,
pageId: string pageId: string
): { ): {
before: Record<string, Partial<T>> before: Record<string, Partial<T>>
@ -420,10 +420,12 @@ export class TLDR {
ids.forEach((id, i) => { ids.forEach((id, i) => {
const shape = this.getShape<T>(data, id, pageId) const shape = this.getShape<T>(data, id, pageId)
const change = fn(shape, i) const change = fn(shape, i)
if (change) {
beforeShapes[id] = Object.fromEntries( beforeShapes[id] = Object.fromEntries(
Object.keys(change).map((key) => [key, shape[key as keyof T]]) Object.keys(change).map((key) => [key, shape[key as keyof T]])
) as Partial<T> ) as Partial<T>
afterShapes[id] = change afterShapes[id] = change
}
}) })
const dataWithMutations = Utils.deepMerge(data, { const dataWithMutations = Utils.deepMerge(data, {
@ -610,6 +612,16 @@ export class TLDR {
} }
} }
static mutate<T extends TLDrawShape>(data: Data, shape: T, props: Partial<T>, pageId: string) {
let next = getShapeUtils(shape).mutate(shape, props)
if (props.children) {
next = this.onChildrenChange(data, next, pageId) || next
}
return next
}
static onSessionComplete<T extends TLDrawShape>(data: Data, shape: T, pageId: string) { static onSessionComplete<T extends TLDrawShape>(data: Data, shape: T, pageId: string) {
const delta = getShapeUtils(shape).onSessionComplete(shape) const delta = getShapeUtils(shape).onSessionComplete(shape)
if (!delta) return shape if (!delta) return shape
@ -670,16 +682,6 @@ export class TLDR {
) )
} }
static mutate<T extends TLDrawShape>(data: Data, shape: T, props: Partial<T>, pageId: string) {
let next = getShapeUtils(shape).mutate(shape, props)
if (props.children) {
next = this.onChildrenChange(data, next, pageId) || next
}
return next
}
/* -------------------------------------------------- */ /* -------------------------------------------------- */
/* Parents */ /* Parents */
/* -------------------------------------------------- */ /* -------------------------------------------------- */

View file

@ -15,6 +15,7 @@ import {
brushUpdater, brushUpdater,
TLPointerInfo, TLPointerInfo,
inputs, inputs,
TLBounds,
} from '@tldraw/core' } from '@tldraw/core'
import { import {
FlipType, FlipType,
@ -322,9 +323,12 @@ export class TLDrawState extends StateManager<Data> {
* @returns * @returns
*/ */
private setStatus(status: TLDrawStatus) { private setStatus(status: TLDrawStatus) {
return this.patchState({ return this.patchState(
{
appState: { status: { current: status, previous: this.appState.status.current } }, appState: { status: { current: status, previous: this.appState.status.current } },
}) },
`set_status:${status}`
)
} }
/* -------------------------------------------------- */ /* -------------------------------------------------- */
@ -483,7 +487,8 @@ export class TLDrawState extends StateManager<Data> {
this.session = undefined this.session = undefined
this.selectedGroupId = undefined this.selectedGroupId = undefined
return this.replaceState({ return this.replaceState(
{
...this.state, ...this.state,
appState: { appState: {
...this.appState, ...this.appState,
@ -519,7 +524,9 @@ export class TLDrawState extends StateManager<Data> {
}) })
), ),
}, },
}) },
`loaded_document:${document.id}`
)
} }
/** /**
@ -599,6 +606,19 @@ export class TLDrawState extends StateManager<Data> {
return TLDR.getShape<T>(this.state, id, pageId) return TLDR.getShape<T>(this.state, id, pageId)
} }
/**
* Get the bounds of a shape on a given page.
* @param id The shape's id.
* @param pageId (optional) The page's id.
*/
getShapeBounds = (id: string, pageId = this.currentPageId): TLBounds => {
return TLDR.getBounds(this.getShape(id, pageId))
}
greet() {
return 'hello'
}
/** /**
* Get a binding from a given page. * Get a binding from a given page.
* @param id The binding's id. * @param id The binding's id.
@ -1761,6 +1781,17 @@ export class TLDrawState extends StateManager<Data> {
return this.setState(Commands.duplicate(this.state, ids)) return this.setState(Commands.duplicate(this.state, ids))
} }
/**
* Reset the bounds for one or more shapes. Usually when the
* bounding box of a shape is double-clicked. Different shapes may
* handle this differently.
* @param ids The ids to change (defaults to selection).
*/
resetBounds = (ids = this.selectedIds): this => {
const command = Commands.resetBounds(this.state, ids, this.currentPageId)
return this.setState(Commands.resetBounds(this.state, ids, this.currentPageId), command.id)
}
/** /**
* Toggle the hidden property of one or more shapes. * Toggle the hidden property of one or more shapes.
* @param ids The ids to change (defaults to selection). * @param ids The ids to change (defaults to selection).
@ -2560,7 +2591,9 @@ export class TLDrawState extends StateManager<Data> {
} }
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = () => { onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = () => {
// TODO if (this.selectedIds.length === 1) {
this.resetBounds(this.selectedIds)
}
} }
onRightPointBoundsHandle: TLBoundsHandleEventHandler = () => { onRightPointBoundsHandle: TLBoundsHandleEventHandler = () => {

View file

@ -3627,6 +3627,33 @@
ismobilejs "^1.1.1" ismobilejs "^1.1.1"
react-use-gesture "^9.1.3" react-use-gesture "^9.1.3"
"@tldraw/core@^0.0.64":
version "0.0.64"
resolved "https://registry.yarnpkg.com/@tldraw/core/-/core-0.0.64.tgz#b81041250a023834f6f34d6723bde41f6ef4483e"
integrity sha512-m9A/5IiMWftWYaFGM6Yccbw6WKkxGG1RUyfkswRxUYFJAeeUEdyhtvpR+JmwipRjeEJIL5AEgMqjxr1Mfq1xsw==
dependencies:
deepmerge "^4.2.2"
react-use-gesture "^9.1.3"
"@tldraw/tldraw@^0.0.64":
version "0.0.64"
resolved "https://registry.yarnpkg.com/@tldraw/tldraw/-/tldraw-0.0.64.tgz#7c3ac1ecf9d74d047d84b3e44c8fe51dbf199ec4"
integrity sha512-NRL2oY7fnmtH/PzwsVOm9xzVCzP5ECyxGZASg+a0iOKXJCxZoCwxloorHITNt/zRZPBBTJkVtS8gG6nHJOtchw==
dependencies:
"@radix-ui/react-alert-dialog" "^0.0.20"
"@radix-ui/react-checkbox" "^0.0.16"
"@radix-ui/react-context-menu" "^0.0.23"
"@radix-ui/react-dropdown-menu" "^0.0.22"
"@radix-ui/react-icons" "^1.0.3"
"@radix-ui/react-id" "^0.0.6"
"@radix-ui/react-radio-group" "^0.0.18"
"@radix-ui/react-tooltip" "^0.0.20"
"@stitches/react" "^1.0.0"
"@tldraw/core" "^0.0.64"
perfect-freehand "^0.5.3"
react-hotkeys-hook "^3.4.0"
rko "^0.5.20"
"@tootallnate/once@1": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"