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
|
||||
// @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} />
|
||||
|
|
|
@ -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}‍`
|
||||
|
@ -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: {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
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) => {
|
||||
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 {
|
||||
|
|
|
@ -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 */
|
||||
/* -------------------------------------------------- */
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
27
yarn.lock
27
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue