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
// @ts-ignore
window.tlstate = 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} />

View file

@ -254,7 +254,7 @@ export class Text extends TLDrawShapeUtil<TextShape> {
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
if (!melm) {
// 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;`
@ -340,9 +340,13 @@ export class Text extends TLDrawShapeUtil<TextShape> {
onBoundsReset(shape: TextShape): Partial<TextShape> {
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 {
style: {

View file

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

View file

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

View file

@ -407,7 +407,7 @@ export class TLDR {
static mutateShapes<T extends TLDrawShape>(
data: Data,
ids: string[],
fn: (shape: T, i: number) => Partial<T>,
fn: (shape: T, i: number) => Partial<T> | void,
pageId: string
): {
before: Record<string, Partial<T>>
@ -420,10 +420,12 @@ export class TLDR {
ids.forEach((id, i) => {
const shape = this.getShape<T>(data, id, pageId)
const change = fn(shape, i)
beforeShapes[id] = Object.fromEntries(
Object.keys(change).map((key) => [key, shape[key as keyof T]])
) as Partial<T>
afterShapes[id] = change
if (change) {
beforeShapes[id] = Object.fromEntries(
Object.keys(change).map((key) => [key, shape[key as keyof T]])
) as Partial<T>
afterShapes[id] = change
}
})
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) {
const delta = getShapeUtils(shape).onSessionComplete(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 */
/* -------------------------------------------------- */

View file

@ -15,6 +15,7 @@ import {
brushUpdater,
TLPointerInfo,
inputs,
TLBounds,
} from '@tldraw/core'
import {
FlipType,
@ -322,9 +323,12 @@ export class TLDrawState extends StateManager<Data> {
* @returns
*/
private setStatus(status: TLDrawStatus) {
return this.patchState({
appState: { status: { current: status, previous: this.appState.status.current } },
})
return this.patchState(
{
appState: { status: { current: status, previous: this.appState.status.current } },
},
`set_status:${status}`
)
}
/* -------------------------------------------------- */
@ -483,43 +487,46 @@ export class TLDrawState extends StateManager<Data> {
this.session = undefined
this.selectedGroupId = undefined
return this.replaceState({
...this.state,
appState: {
...this.appState,
currentPageId: Object.keys(document.pages)[0],
},
document: {
...document,
pages: Object.fromEntries(
Object.entries(document.pages)
.sort((a, b) => (a[1].childIndex || 0) - (b[1].childIndex || 0))
.map(([id, page], i) => {
return this.replaceState(
{
...this.state,
appState: {
...this.appState,
currentPageId: Object.keys(document.pages)[0],
},
document: {
...document,
pages: Object.fromEntries(
Object.entries(document.pages)
.sort((a, b) => (a[1].childIndex || 0) - (b[1].childIndex || 0))
.map(([id, page], i) => {
return [
id,
{
...page,
name: page.name ? page.name : `Page ${i++}`,
},
]
})
),
pageStates: Object.fromEntries(
Object.entries(document.pageStates).map(([id, pageState]) => {
return [
id,
{
...page,
name: page.name ? page.name : `Page ${i++}`,
...pageState,
bindingId: undefined,
editingId: undefined,
hoveredId: undefined,
pointedId: undefined,
},
]
})
),
pageStates: Object.fromEntries(
Object.entries(document.pageStates).map(([id, pageState]) => {
return [
id,
{
...pageState,
bindingId: undefined,
editingId: undefined,
hoveredId: undefined,
pointedId: undefined,
},
]
})
),
),
},
},
})
`loaded_document:${document.id}`
)
}
/**
@ -599,6 +606,19 @@ export class TLDrawState extends StateManager<Data> {
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.
* @param id The binding's id.
@ -1761,6 +1781,17 @@ export class TLDrawState extends StateManager<Data> {
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.
* @param ids The ids to change (defaults to selection).
@ -2560,7 +2591,9 @@ export class TLDrawState extends StateManager<Data> {
}
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = () => {
// TODO
if (this.selectedIds.length === 1) {
this.resetBounds(this.selectedIds)
}
}
onRightPointBoundsHandle: TLBoundsHandleEventHandler = () => {

View file

@ -3627,6 +3627,33 @@
ismobilejs "^1.1.1"
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":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"