Adds reset bounds
This commit is contained in:
parent
c1bb554103
commit
ff826712c6
11 changed files with 217 additions and 85 deletions
|
@ -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} />
|
||||||
|
|
|
@ -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}‍`
|
melm.innerHTML = `${shape.text}‍`
|
||||||
|
@ -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: {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
1
packages/tldraw/src/state/command/reset-bounds/index.ts
Normal file
1
packages/tldraw/src/state/command/reset-bounds/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './reset-bounds.command'
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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 */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
27
yarn.lock
27
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue