parent
c56bf3b0eb
commit
6661ab0586
6 changed files with 206 additions and 29 deletions
|
@ -1524,10 +1524,11 @@ left past the initial left edge) then swap points on that axis.
|
|||
* @param bounds
|
||||
*/
|
||||
static getBoundsWithCenter(bounds: TLBounds): TLBounds & { midX: number; midY: number } {
|
||||
const center = Utils.getBoundsCenter(bounds)
|
||||
return {
|
||||
...bounds,
|
||||
midX: bounds.minX + bounds.width / 2,
|
||||
midY: bounds.minY + bounds.height / 2,
|
||||
midX: center[0],
|
||||
midY: center[1],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,3 +44,14 @@ describe('When creating with a transform-single session', () => {
|
|||
expect(tlstate.getShape('rect1')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When snapping', () => {
|
||||
it.todo('Does not snap when moving quicky')
|
||||
it.todo('Snaps only matching edges when moving slowly')
|
||||
it.todo('Snaps any edge to any edge when moving very slowly')
|
||||
it.todo('Snaps a clone to its parent')
|
||||
it.todo('Cleans up snap lines when cancelled')
|
||||
it.todo('Cleans up snap lines when completed')
|
||||
it.todo('Cleans up snap lines when starting to clone / not clone')
|
||||
it.todo('Does not snap rotated shapes')
|
||||
})
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
||||
import { TLBoundsCorner, TLSnapLine, TLBoundsEdge, Utils, TLBoundsWithCenter } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import type { Session } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { SNAP_DISTANCE } from '~state/constants'
|
||||
|
||||
type SnapInfo =
|
||||
| {
|
||||
state: 'empty'
|
||||
}
|
||||
| {
|
||||
state: 'ready'
|
||||
bounds: TLBoundsWithCenter[]
|
||||
}
|
||||
|
||||
export class TransformSingleSession implements Session {
|
||||
type = SessionType.TransformSingle
|
||||
|
@ -14,6 +24,9 @@ export class TransformSingleSession implements Session {
|
|||
scaleY = 1
|
||||
isCreate: boolean
|
||||
snapshot: TransformSingleSnapshot
|
||||
snapInfo: SnapInfo = { state: 'empty' }
|
||||
prevPoint = [0, 0]
|
||||
speed = 1
|
||||
|
||||
constructor(
|
||||
data: Data,
|
||||
|
@ -27,12 +40,17 @@ export class TransformSingleSession implements Session {
|
|||
this.isCreate = isCreate
|
||||
}
|
||||
|
||||
start = () => void null
|
||||
start = (data: Data) => {
|
||||
this.createSnapInfo(data)
|
||||
return void null
|
||||
}
|
||||
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const { transformType } = this
|
||||
|
||||
const { initialShapeBounds, initialShape, id } = this.snapshot
|
||||
const { currentPageId, initialShapeBounds, initialShape, id } = this.snapshot
|
||||
|
||||
const delta = Vec.sub(point, this.origin)
|
||||
|
||||
const shapes = {} as Record<string, Partial<TLDrawShape>>
|
||||
|
||||
|
@ -40,15 +58,55 @@ export class TransformSingleSession implements Session {
|
|||
|
||||
const utils = TLDR.getShapeUtils(shape)
|
||||
|
||||
const newBounds = Utils.getTransformedBoundingBox(
|
||||
let newBounds = Utils.getTransformedBoundingBox(
|
||||
initialShapeBounds,
|
||||
transformType,
|
||||
Vec.sub(point, this.origin),
|
||||
delta,
|
||||
shape.rotation,
|
||||
shiftKey || shape.isAspectRatioLocked || utils.isAspectRatioLocked
|
||||
)
|
||||
|
||||
const change = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
|
||||
// Should we snap?
|
||||
|
||||
const speed = Vec.dist(point, this.prevPoint)
|
||||
|
||||
this.prevPoint = point
|
||||
|
||||
const speedChange = speed - this.speed
|
||||
|
||||
this.speed = this.speed + speedChange * (speedChange > 1 ? 0.5 : 0.15)
|
||||
|
||||
let snapLines: TLSnapLine[] = []
|
||||
|
||||
const { zoom } = data.document.pageStates[currentPageId].camera
|
||||
|
||||
if (
|
||||
!metaKey &&
|
||||
!initialShape.rotation && // not now anyway
|
||||
this.speed * zoom < 5 &&
|
||||
this.snapInfo.state === 'ready'
|
||||
) {
|
||||
const snapResult = Utils.getSnapPoints(
|
||||
Utils.getBoundsWithCenter(newBounds),
|
||||
this.snapInfo.bounds,
|
||||
SNAP_DISTANCE / zoom,
|
||||
this.speed * zoom < 0.45
|
||||
)
|
||||
|
||||
if (snapResult) {
|
||||
snapLines = snapResult.snapLines
|
||||
|
||||
newBounds = Utils.getTransformedBoundingBox(
|
||||
initialShapeBounds,
|
||||
transformType,
|
||||
Vec.sub(delta, snapResult.offset),
|
||||
shape.rotation,
|
||||
shiftKey || shape.isAspectRatioLocked || utils.isAspectRatioLocked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const afterShape = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
|
||||
initialShape,
|
||||
type: this.transformType,
|
||||
scaleX: newBounds.scaleX,
|
||||
|
@ -56,11 +114,14 @@ export class TransformSingleSession implements Session {
|
|||
transformOrigin: [0.5, 0.5],
|
||||
})
|
||||
|
||||
if (change) {
|
||||
shapes[shape.id] = change
|
||||
if (afterShape) {
|
||||
shapes[shape.id] = afterShape
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -83,6 +144,9 @@ export class TransformSingleSession implements Session {
|
|||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -115,6 +179,9 @@ export class TransformSingleSession implements Session {
|
|||
return {
|
||||
id: 'transform_single',
|
||||
before: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -131,6 +198,9 @@ export class TransformSingleSession implements Session {
|
|||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -148,30 +218,40 @@ export class TransformSingleSession implements Session {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createSnapInfo = async (data: Data) => {
|
||||
const { initialShape } = this.snapshot
|
||||
const { currentPageId } = data.appState
|
||||
const page = data.document.pages[currentPageId]
|
||||
|
||||
this.snapInfo = {
|
||||
state: 'ready',
|
||||
bounds: Object.values(page.shapes)
|
||||
.filter((shape) => shape.id !== initialShape.id)
|
||||
.map((shape) => Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTransformSingleSnapshot(
|
||||
data: Data,
|
||||
transformType: TLBoundsEdge | TLBoundsCorner
|
||||
) {
|
||||
const shape = TLDR.getShape(
|
||||
data,
|
||||
TLDR.getSelectedIds(data, data.appState.currentPageId)[0],
|
||||
data.appState.currentPageId
|
||||
)
|
||||
const { currentPageId } = data.appState
|
||||
const shape = TLDR.getShape(data, TLDR.getSelectedIds(data, currentPageId)[0], currentPageId)
|
||||
|
||||
if (!shape) {
|
||||
throw Error('You must have one shape selected.')
|
||||
}
|
||||
|
||||
const bounds = TLDR.getBounds(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
currentPageId,
|
||||
hasUnlockedShape: !shape.isLocked,
|
||||
type: transformType,
|
||||
initialShape: Utils.deepClone(shape),
|
||||
initialShapeBounds: bounds,
|
||||
initialShapeBounds: TLDR.getBounds(shape),
|
||||
commonBounds: TLDR.getRotatedBounds(shape),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import type { TLSnapLine, TLBoundsWithCenter } from '@tldraw/core'
|
||||
import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import type { Command } from 'rko'
|
||||
import { SNAP_DISTANCE } from '~state/constants'
|
||||
|
||||
type SnapInfo =
|
||||
| {
|
||||
state: 'empty'
|
||||
}
|
||||
| {
|
||||
state: 'ready'
|
||||
bounds: TLBoundsWithCenter[]
|
||||
}
|
||||
|
||||
export class TransformSession implements Session {
|
||||
static type = SessionType.Transform
|
||||
|
@ -15,6 +26,9 @@ export class TransformSession implements Session {
|
|||
snapshot: TransformSnapshot
|
||||
isCreate: boolean
|
||||
initialSelectedIds: string[]
|
||||
snapInfo: SnapInfo = { state: 'empty' }
|
||||
prevPoint = [0, 0]
|
||||
speed = 1
|
||||
|
||||
constructor(
|
||||
data: Data,
|
||||
|
@ -29,7 +43,10 @@ export class TransformSession implements Session {
|
|||
this.initialSelectedIds = TLDR.getSelectedIds(data, data.appState.currentPageId)
|
||||
}
|
||||
|
||||
start = () => void null
|
||||
start = (data: Data) => {
|
||||
this.createSnapInfo(data)
|
||||
return void null
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
|
@ -42,22 +59,61 @@ export class TransformSession implements Session {
|
|||
|
||||
const pageState = TLDR.getPageState(data, data.appState.currentPageId)
|
||||
|
||||
const newBoundingBox = Utils.getTransformedBoundingBox(
|
||||
const delta = Vec.sub(point, this.origin)
|
||||
|
||||
let newBounds = Utils.getTransformedBoundingBox(
|
||||
initialBounds,
|
||||
transformType,
|
||||
Vec.sub(point, this.origin),
|
||||
delta,
|
||||
pageState.boundsRotation,
|
||||
shiftKey || isAllAspectRatioLocked
|
||||
)
|
||||
|
||||
// Should we snap?
|
||||
|
||||
const speed = Vec.dist(point, this.prevPoint)
|
||||
|
||||
this.prevPoint = point
|
||||
|
||||
const speedChange = speed - this.speed
|
||||
|
||||
this.speed = this.speed + speedChange * (speedChange > 1 ? 0.5 : 0.15)
|
||||
|
||||
let snapLines: TLSnapLine[] = []
|
||||
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const { zoom } = data.document.pageStates[currentPageId].camera
|
||||
|
||||
if (!metaKey && this.speed * zoom < 5 && this.snapInfo.state === 'ready') {
|
||||
const snapResult = Utils.getSnapPoints(
|
||||
Utils.getBoundsWithCenter(newBounds),
|
||||
this.snapInfo.bounds,
|
||||
SNAP_DISTANCE / zoom,
|
||||
this.speed * zoom < 0.45
|
||||
)
|
||||
|
||||
if (snapResult) {
|
||||
snapLines = snapResult.snapLines
|
||||
|
||||
newBounds = Utils.getTransformedBoundingBox(
|
||||
initialBounds,
|
||||
transformType,
|
||||
Vec.sub(delta, snapResult.offset),
|
||||
pageState.boundsRotation,
|
||||
shiftKey || isAllAspectRatioLocked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Now work backward to calculate a new bounding box for each of the shapes.
|
||||
|
||||
this.scaleX = newBoundingBox.scaleX
|
||||
this.scaleY = newBoundingBox.scaleY
|
||||
this.scaleX = newBounds.scaleX
|
||||
this.scaleY = newBounds.scaleY
|
||||
|
||||
shapeBounds.forEach(({ id, initialShape, initialShapeBounds, transformOrigin }) => {
|
||||
const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
|
||||
newBoundingBox,
|
||||
newBounds,
|
||||
initialBounds,
|
||||
initialShapeBounds,
|
||||
this.scaleX < 0,
|
||||
|
@ -78,6 +134,9 @@ export class TransformSession implements Session {
|
|||
})
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -100,6 +159,9 @@ export class TransformSession implements Session {
|
|||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -145,6 +207,9 @@ export class TransformSession implements Session {
|
|||
return {
|
||||
id: 'transform',
|
||||
before: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -161,6 +226,9 @@ export class TransformSession implements Session {
|
|||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -178,11 +246,26 @@ export class TransformSession implements Session {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createSnapInfo = async (data: Data) => {
|
||||
const { initialShapeIds } = this.snapshot
|
||||
const { currentPageId } = data.appState
|
||||
const page = data.document.pages[currentPageId]
|
||||
|
||||
this.snapInfo = {
|
||||
state: 'ready',
|
||||
bounds: Object.values(page.shapes)
|
||||
.filter((shape) => !initialShapeIds.includes(shape.id))
|
||||
.map((shape) => Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTransformSnapshot(data: Data, transformType: TLBoundsEdge | TLBoundsCorner) {
|
||||
const initialShapes = TLDR.getSelectedBranchSnapshot(data, data.appState.currentPageId)
|
||||
|
||||
const initialShapeIds = initialShapes.map((shape) => shape.id)
|
||||
|
||||
const hasUnlockedShapes = initialShapes.length > 0
|
||||
|
||||
const isAllAspectRatioLocked = initialShapes.every(
|
||||
|
@ -205,6 +288,7 @@ export function getTransformSnapshot(data: Data, transformType: TLBoundsEdge | T
|
|||
type: transformType,
|
||||
hasUnlockedShapes,
|
||||
isAllAspectRatioLocked,
|
||||
initialShapeIds,
|
||||
initialShapes,
|
||||
initialBounds: commonBounds,
|
||||
shapeBounds: initialShapes.map((shape) => {
|
||||
|
|
|
@ -335,4 +335,5 @@ describe('When snapping', () => {
|
|||
it.todo('Cleans up snap lines when cancelled')
|
||||
it.todo('Cleans up snap lines when completed')
|
||||
it.todo('Cleans up snap lines when starting to clone / not clone')
|
||||
it.todo('Snaps the rotated bounding box of rotated shapes')
|
||||
})
|
||||
|
|
|
@ -137,9 +137,9 @@ export class TranslateSession implements Session {
|
|||
|
||||
this.snapLines = []
|
||||
|
||||
if (!metaKey && this.speed < 4 && this.snapInfo.state === 'ready') {
|
||||
const { zoom } = data.document.pageStates[currentPageId].camera
|
||||
const { zoom } = data.document.pageStates[currentPageId].camera
|
||||
|
||||
if (!metaKey && this.speed * zoom < 5 && this.snapInfo.state === 'ready') {
|
||||
const bounds = Utils.getBoundsWithCenter(
|
||||
Utils.translateBounds(this.snapshot.commonBounds, delta)
|
||||
)
|
||||
|
@ -153,7 +153,7 @@ export class TranslateSession implements Session {
|
|||
|
||||
if (snapResult) {
|
||||
this.snapLines = snapResult.snapLines
|
||||
delta = Vec.sub(delta, snapResult?.offset)
|
||||
delta = Vec.sub(delta, snapResult.offset)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -504,7 +504,7 @@ export class TranslateSession implements Session {
|
|||
const otherBounds: TLBoundsWithCenter[] = []
|
||||
|
||||
Object.values(page.shapes).forEach((shape) => {
|
||||
const bounds = Utils.getBoundsWithCenter(TLDR.getBounds(shape))
|
||||
const bounds = Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))
|
||||
allBounds.push(bounds)
|
||||
if (!selectedIds.includes(shape.id)) {
|
||||
otherBounds.push(bounds)
|
||||
|
@ -655,7 +655,7 @@ export function getTranslateSnapshot(data: Data) {
|
|||
initialParentChildren[id] = shape.children!
|
||||
})
|
||||
|
||||
const commonBounds = Utils.getCommonBounds(shapesToMove.map(TLDR.getBounds))
|
||||
const commonBounds = Utils.getCommonBounds(shapesToMove.map(TLDR.getRotatedBounds))
|
||||
|
||||
return {
|
||||
selectedIds,
|
||||
|
|
Loading…
Reference in a new issue