[feature] hold alt to grow arrow from center (#635)
* Add alt-drag line/arrow handles * add test
This commit is contained in:
parent
b4dd96ed19
commit
1723254e80
4 changed files with 165 additions and 27 deletions
|
@ -395,3 +395,44 @@ describe('When creating arrows inside of other shapes...', () => {
|
||||||
expect(app.shapes.length).toBe(2)
|
expect(app.shapes.length).toBe(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When holding alt and dragging a handle', () => {
|
||||||
|
it('Applies a delta to both handles', () => {
|
||||||
|
const app = new TldrawTestApp()
|
||||||
|
.selectAll()
|
||||||
|
.delete()
|
||||||
|
.createShapes({ type: TDShapeType.Arrow, id: 'arrow1', point: [0, 0] })
|
||||||
|
.select('arrow1')
|
||||||
|
.movePointer([0, 0])
|
||||||
|
.startSession(SessionType.Arrow, 'arrow1', 'start')
|
||||||
|
.movePointer([-10, -10])
|
||||||
|
|
||||||
|
// Without alt...
|
||||||
|
expect(app.getShape('arrow1')).toMatchObject({
|
||||||
|
point: [-10, -10],
|
||||||
|
handles: {
|
||||||
|
start: {
|
||||||
|
point: [0, 0],
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
point: [11, 11],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// With alt...
|
||||||
|
app.movePointer({ x: -10, y: -10, altKey: true })
|
||||||
|
|
||||||
|
expect(app.getShape('arrow1')).toMatchObject({
|
||||||
|
point: [-10, -10],
|
||||||
|
handles: {
|
||||||
|
start: {
|
||||||
|
point: [0, 0],
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
point: [21, 21], // delta is applied to both handles
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -34,25 +34,34 @@ export class ArrowSession extends BaseSession {
|
||||||
|
|
||||||
constructor(app: TldrawApp, shapeId: string, handleId: 'start' | 'end', isCreate = false) {
|
constructor(app: TldrawApp, shapeId: string, handleId: 'start' | 'end', isCreate = false) {
|
||||||
super(app)
|
super(app)
|
||||||
|
|
||||||
this.isCreate = isCreate
|
this.isCreate = isCreate
|
||||||
|
|
||||||
const { currentPageId } = app.state.appState
|
const { currentPageId } = app.state.appState
|
||||||
|
|
||||||
const page = app.state.document.pages[currentPageId]
|
const page = app.state.document.pages[currentPageId]
|
||||||
|
|
||||||
this.handleId = handleId
|
this.handleId = handleId
|
||||||
|
|
||||||
this.initialShape = deepCopy(page.shapes[shapeId] as ArrowShape)
|
this.initialShape = deepCopy(page.shapes[shapeId] as ArrowShape)
|
||||||
|
|
||||||
this.bindableShapeIds = TLDR.getBindableShapeIds(app.state).filter(
|
this.bindableShapeIds = TLDR.getBindableShapeIds(app.state).filter(
|
||||||
(id) => !(id === this.initialShape.id || id === this.initialShape.parentId)
|
(id) => !(id === this.initialShape.id || id === this.initialShape.parentId)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: find out why this the oppositeHandleBindingId is sometimes missing
|
// TODO: find out why this the oppositeHandleBindingId is sometimes missing
|
||||||
const oppositeHandleBindingId =
|
const oppositeHandleBindingId =
|
||||||
this.initialShape.handles[handleId === 'start' ? 'end' : 'start']?.bindingId
|
this.initialShape.handles[handleId === 'start' ? 'end' : 'start']?.bindingId
|
||||||
|
|
||||||
if (oppositeHandleBindingId) {
|
if (oppositeHandleBindingId) {
|
||||||
const oppositeToId = page.bindings[oppositeHandleBindingId]?.toId
|
const oppositeToId = page.bindings[oppositeHandleBindingId]?.toId
|
||||||
if (oppositeToId) {
|
if (oppositeToId) {
|
||||||
this.bindableShapeIds = this.bindableShapeIds.filter((id) => id !== oppositeToId)
|
this.bindableShapeIds = this.bindableShapeIds.filter((id) => id !== oppositeToId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { originPoint } = this.app
|
const { originPoint } = this.app
|
||||||
|
|
||||||
if (this.isCreate) {
|
if (this.isCreate) {
|
||||||
// If we're creating a new shape, should we bind its first point?
|
// If we're creating a new shape, should we bind its first point?
|
||||||
// The method may return undefined, which is correct if there is no
|
// The method may return undefined, which is correct if there is no
|
||||||
|
@ -66,6 +75,7 @@ export class ArrowSession extends BaseSession {
|
||||||
// TODO - We should be smarter here, what's the right logic?
|
// TODO - We should be smarter here, what's the right logic?
|
||||||
return b.childIndex - a.childIndex
|
return b.childIndex - a.childIndex
|
||||||
})[0]?.id
|
})[0]?.id
|
||||||
|
|
||||||
if (this.startBindingShapeId) {
|
if (this.startBindingShapeId) {
|
||||||
this.bindableShapeIds.splice(this.bindableShapeIds.indexOf(this.startBindingShapeId), 1)
|
this.bindableShapeIds.splice(this.bindableShapeIds.indexOf(this.startBindingShapeId), 1)
|
||||||
}
|
}
|
||||||
|
@ -73,6 +83,7 @@ export class ArrowSession extends BaseSession {
|
||||||
// If we're editing an existing line, is there a binding already
|
// If we're editing an existing line, is there a binding already
|
||||||
// for the dragging handle?
|
// for the dragging handle?
|
||||||
const initialBindingId = this.initialShape.handles[this.handleId].bindingId
|
const initialBindingId = this.initialShape.handles[this.handleId].bindingId
|
||||||
|
|
||||||
if (initialBindingId) {
|
if (initialBindingId) {
|
||||||
this.initialBinding = page.bindings[initialBindingId]
|
this.initialBinding = page.bindings[initialBindingId]
|
||||||
} else {
|
} else {
|
||||||
|
@ -94,35 +105,65 @@ export class ArrowSession extends BaseSession {
|
||||||
currentGrid,
|
currentGrid,
|
||||||
settings: { showGrid },
|
settings: { showGrid },
|
||||||
} = this.app
|
} = this.app
|
||||||
|
|
||||||
const shape = this.app.getShape<ArrowShape>(initialShape.id)
|
const shape = this.app.getShape<ArrowShape>(initialShape.id)
|
||||||
|
|
||||||
if (shape.isLocked) return
|
if (shape.isLocked) return
|
||||||
const handles = shape.handles
|
|
||||||
|
const { handles } = initialShape
|
||||||
|
|
||||||
const handleId = this.handleId as keyof typeof handles
|
const handleId = this.handleId as keyof typeof handles
|
||||||
// If the handle can bind, then we need to search bindable shapes for
|
// If the handle can bind, then we need to search bindable shapes for
|
||||||
// a binding.
|
// a binding.
|
||||||
if (!handles[handleId].canBind) return
|
if (!handles[handleId].canBind) return
|
||||||
// First update the handle's next point
|
|
||||||
let delta = Vec.sub(currentPoint, handles[handleId].point)
|
// Find the delta (in shape space)
|
||||||
|
let delta = Vec.sub(currentPoint, Vec.add(handles[handleId].point, initialShape.point))
|
||||||
|
|
||||||
if (shiftKey) {
|
if (shiftKey) {
|
||||||
const A = handles[handleId === 'start' ? 'end' : 'start'].point
|
const A = altKey
|
||||||
|
? Vec.med(handles.start.point, handles.end.point)
|
||||||
|
: handles[handleId === 'start' ? 'end' : 'start'].point
|
||||||
const B = handles[handleId].point
|
const B = handles[handleId].point
|
||||||
const C = Vec.toFixed(Vec.sub(Vec.add(B, delta), shape.point))
|
const C = Vec.add(B, delta)
|
||||||
|
|
||||||
const angle = Vec.angle(A, C)
|
const angle = Vec.angle(A, C)
|
||||||
|
|
||||||
const adjusted = Vec.rotWith(C, A, Utils.snapAngleToSegments(angle, 24) - angle)
|
const adjusted = Vec.rotWith(C, A, Utils.snapAngleToSegments(angle, 24) - angle)
|
||||||
|
|
||||||
delta = Vec.add(delta, Vec.sub(adjusted, C))
|
delta = Vec.add(delta, Vec.sub(adjusted, C))
|
||||||
}
|
}
|
||||||
const nextPoint = Vec.sub(Vec.add(handles[handleId].point, delta), shape.point)
|
|
||||||
const draggedHandle = {
|
const nextPoint = Vec.add(handles[handleId].point, delta)
|
||||||
|
|
||||||
|
const handleChanges = {
|
||||||
|
[handleId]: {
|
||||||
...handles[handleId],
|
...handles[handleId],
|
||||||
point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint),
|
point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint),
|
||||||
bindingId: undefined,
|
bindingId: undefined,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (altKey) {
|
||||||
|
// If the user is holding alt key, apply the inverse delta
|
||||||
|
// to the oppoosite handle.
|
||||||
|
const oppositeHandleId = handleId === 'start' ? 'end' : 'start'
|
||||||
|
|
||||||
|
const nextPoint = Vec.sub(handles[oppositeHandleId].point, delta)
|
||||||
|
|
||||||
|
handleChanges[oppositeHandleId] = {
|
||||||
|
...handles[oppositeHandleId],
|
||||||
|
point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint),
|
||||||
|
bindingId: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const utils = shapeUtils[TDShapeType.Arrow]
|
const utils = shapeUtils[TDShapeType.Arrow]
|
||||||
const handleChange = utils.onHandleChange?.(shape, {
|
const handleChange = utils.onHandleChange?.(initialShape, handleChanges)
|
||||||
[handleId]: draggedHandle,
|
|
||||||
})
|
|
||||||
// If the handle changed produced no change, bail here
|
// If the handle changed produced no change, bail here
|
||||||
if (!handleChange) return
|
if (!handleChange) return
|
||||||
|
|
||||||
// If nothing changes, we want these to be the same object reference as
|
// If nothing changes, we want these to be the same object reference as
|
||||||
// before. If it does change, we'll redefine this later on. And if we've
|
// before. If it does change, we'll redefine this later on. And if we've
|
||||||
// made it this far, the shape should be a new object reference that
|
// made it this far, the shape should be a new object reference that
|
||||||
|
@ -131,8 +172,11 @@ export class ArrowSession extends BaseSession {
|
||||||
shape: Utils.deepMerge(shape, handleChange),
|
shape: Utils.deepMerge(shape, handleChange),
|
||||||
bindings: {},
|
bindings: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
let draggedBinding: ArrowBinding | undefined
|
let draggedBinding: ArrowBinding | undefined
|
||||||
|
|
||||||
const draggingHandle = next.shape.handles[this.handleId]
|
const draggingHandle = next.shape.handles[this.handleId]
|
||||||
|
|
||||||
const oppositeHandle = next.shape.handles[this.handleId === 'start' ? 'end' : 'start']
|
const oppositeHandle = next.shape.handles[this.handleId === 'start' ? 'end' : 'start']
|
||||||
|
|
||||||
// START BINDING
|
// START BINDING
|
||||||
|
@ -140,17 +184,29 @@ export class ArrowSession extends BaseSession {
|
||||||
// point based on the current end handle position
|
// point based on the current end handle position
|
||||||
if (this.startBindingShapeId) {
|
if (this.startBindingShapeId) {
|
||||||
let nextStartBinding: ArrowBinding | undefined
|
let nextStartBinding: ArrowBinding | undefined
|
||||||
|
|
||||||
const startTarget = this.app.page.shapes[this.startBindingShapeId]
|
const startTarget = this.app.page.shapes[this.startBindingShapeId]
|
||||||
|
|
||||||
const startTargetUtils = TLDR.getShapeUtil(startTarget)
|
const startTargetUtils = TLDR.getShapeUtil(startTarget)
|
||||||
|
|
||||||
const center = startTargetUtils.getCenter(startTarget)
|
const center = startTargetUtils.getCenter(startTarget)
|
||||||
|
|
||||||
const startHandle = next.shape.handles.start
|
const startHandle = next.shape.handles.start
|
||||||
|
|
||||||
const endHandle = next.shape.handles.end
|
const endHandle = next.shape.handles.end
|
||||||
|
|
||||||
const rayPoint = Vec.add(startHandle.point, next.shape.point)
|
const rayPoint = Vec.add(startHandle.point, next.shape.point)
|
||||||
|
|
||||||
if (Vec.isEqual(rayPoint, center)) rayPoint[1]++ // Fix bug where ray and center are identical
|
if (Vec.isEqual(rayPoint, center)) rayPoint[1]++ // Fix bug where ray and center are identical
|
||||||
|
|
||||||
const rayOrigin = center
|
const rayOrigin = center
|
||||||
|
|
||||||
const isInsideShape = startTargetUtils.hitTestPoint(startTarget, currentPoint)
|
const isInsideShape = startTargetUtils.hitTestPoint(startTarget, currentPoint)
|
||||||
|
|
||||||
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
|
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
|
||||||
|
|
||||||
const hasStartBinding = this.app.getBinding(this.newStartBindingId) !== undefined
|
const hasStartBinding = this.app.getBinding(this.newStartBindingId) !== undefined
|
||||||
|
|
||||||
// Don't bind the start handle if both handles are inside of the target shape.
|
// Don't bind the start handle if both handles are inside of the target shape.
|
||||||
if (
|
if (
|
||||||
!metaKey &&
|
!metaKey &&
|
||||||
|
@ -167,10 +223,13 @@ export class ArrowSession extends BaseSession {
|
||||||
isInsideShape
|
isInsideShape
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextStartBinding && !hasStartBinding) {
|
if (nextStartBinding && !hasStartBinding) {
|
||||||
// Bind the arrow's start handle to the start target
|
// Bind the arrow's start handle to the start target
|
||||||
this.didBind = true
|
this.didBind = true
|
||||||
|
|
||||||
next.bindings[this.newStartBindingId] = nextStartBinding
|
next.bindings[this.newStartBindingId] = nextStartBinding
|
||||||
|
|
||||||
next.shape = Utils.deepMerge(next.shape, {
|
next.shape = Utils.deepMerge(next.shape, {
|
||||||
handles: {
|
handles: {
|
||||||
start: {
|
start: {
|
||||||
|
@ -181,7 +240,9 @@ export class ArrowSession extends BaseSession {
|
||||||
} else if (!nextStartBinding && hasStartBinding) {
|
} else if (!nextStartBinding && hasStartBinding) {
|
||||||
// Remove the start binding
|
// Remove the start binding
|
||||||
this.didBind = false
|
this.didBind = false
|
||||||
|
|
||||||
next.bindings[this.newStartBindingId] = undefined
|
next.bindings[this.newStartBindingId] = undefined
|
||||||
|
|
||||||
next.shape = Utils.deepMerge(initialShape, {
|
next.shape = Utils.deepMerge(initialShape, {
|
||||||
handles: {
|
handles: {
|
||||||
start: {
|
start: {
|
||||||
|
@ -195,10 +256,15 @@ export class ArrowSession extends BaseSession {
|
||||||
// DRAGGED POINT BINDING
|
// DRAGGED POINT BINDING
|
||||||
if (!metaKey) {
|
if (!metaKey) {
|
||||||
const rayOrigin = Vec.add(oppositeHandle.point, next.shape.point)
|
const rayOrigin = Vec.add(oppositeHandle.point, next.shape.point)
|
||||||
|
|
||||||
const rayPoint = Vec.add(draggingHandle.point, next.shape.point)
|
const rayPoint = Vec.add(draggingHandle.point, next.shape.point)
|
||||||
|
|
||||||
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
|
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
|
||||||
|
|
||||||
const startPoint = Vec.add(next.shape.point!, next.shape.handles!.start.point!)
|
const startPoint = Vec.add(next.shape.point!, next.shape.handles!.start.point!)
|
||||||
|
|
||||||
const endPoint = Vec.add(next.shape.point!, next.shape.handles!.end.point!)
|
const endPoint = Vec.add(next.shape.point!, next.shape.handles!.end.point!)
|
||||||
|
|
||||||
const targets = this.bindableShapeIds
|
const targets = this.bindableShapeIds
|
||||||
.map((id) => this.app.page.shapes[id])
|
.map((id) => this.app.page.shapes[id])
|
||||||
.sort((a, b) => b.childIndex - a.childIndex)
|
.sort((a, b) => b.childIndex - a.childIndex)
|
||||||
|
@ -206,6 +272,7 @@ export class ArrowSession extends BaseSession {
|
||||||
const utils = TLDR.getShapeUtil(shape)
|
const utils = TLDR.getShapeUtil(shape)
|
||||||
return ![startPoint, endPoint].every((point) => utils.hitTestPoint(shape, point))
|
return ![startPoint, endPoint].every((point) => utils.hitTestPoint(shape, point))
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
draggedBinding = this.findBindingPoint(
|
draggedBinding = this.findBindingPoint(
|
||||||
shape,
|
shape,
|
||||||
|
@ -217,13 +284,16 @@ export class ArrowSession extends BaseSession {
|
||||||
rayDirection,
|
rayDirection,
|
||||||
altKey
|
altKey
|
||||||
)
|
)
|
||||||
|
|
||||||
if (draggedBinding) break
|
if (draggedBinding) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (draggedBinding) {
|
if (draggedBinding) {
|
||||||
// Create the dragged point binding
|
// Create the dragged point binding
|
||||||
this.didBind = true
|
this.didBind = true
|
||||||
|
|
||||||
next.bindings[this.draggedBindingId] = draggedBinding
|
next.bindings[this.draggedBindingId] = draggedBinding
|
||||||
|
|
||||||
next.shape = Utils.deepMerge(next.shape, {
|
next.shape = Utils.deepMerge(next.shape, {
|
||||||
handles: {
|
handles: {
|
||||||
[this.handleId]: {
|
[this.handleId]: {
|
||||||
|
@ -234,9 +304,12 @@ export class ArrowSession extends BaseSession {
|
||||||
} else {
|
} else {
|
||||||
// Remove the dragging point binding
|
// Remove the dragging point binding
|
||||||
this.didBind = this.didBind || false
|
this.didBind = this.didBind || false
|
||||||
|
|
||||||
const currentBindingId = shape.handles[this.handleId].bindingId
|
const currentBindingId = shape.handles[this.handleId].bindingId
|
||||||
|
|
||||||
if (currentBindingId !== undefined) {
|
if (currentBindingId !== undefined) {
|
||||||
next.bindings[currentBindingId] = undefined
|
next.bindings[currentBindingId] = undefined
|
||||||
|
|
||||||
next.shape = Utils.deepMerge(next.shape, {
|
next.shape = Utils.deepMerge(next.shape, {
|
||||||
handles: {
|
handles: {
|
||||||
[this.handleId]: {
|
[this.handleId]: {
|
||||||
|
@ -316,20 +389,29 @@ export class ArrowSession extends BaseSession {
|
||||||
|
|
||||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||||
const { initialShape, initialBinding, newStartBindingId, startBindingShapeId, handleId } = this
|
const { initialShape, initialBinding, newStartBindingId, startBindingShapeId, handleId } = this
|
||||||
|
|
||||||
const currentShape = TLDR.onSessionComplete(this.app.page.shapes[initialShape.id]) as ArrowShape
|
const currentShape = TLDR.onSessionComplete(this.app.page.shapes[initialShape.id]) as ArrowShape
|
||||||
|
|
||||||
const currentBindingId = currentShape.handles[handleId].bindingId
|
const currentBindingId = currentShape.handles[handleId].bindingId
|
||||||
|
|
||||||
const length = Vec.dist(currentShape.handles.start.point, currentShape.handles.end.point)
|
const length = Vec.dist(currentShape.handles.start.point, currentShape.handles.end.point)
|
||||||
|
|
||||||
if (!(currentBindingId || initialBinding) && length < 4) return this.cancel()
|
if (!(currentBindingId || initialBinding) && length < 4) return this.cancel()
|
||||||
|
|
||||||
const beforeBindings: Partial<Record<string, TDBinding>> = {}
|
const beforeBindings: Partial<Record<string, TDBinding>> = {}
|
||||||
|
|
||||||
const afterBindings: Partial<Record<string, TDBinding>> = {}
|
const afterBindings: Partial<Record<string, TDBinding>> = {}
|
||||||
|
|
||||||
if (initialBinding) {
|
if (initialBinding) {
|
||||||
beforeBindings[initialBinding.id] = this.isCreate ? undefined : initialBinding
|
beforeBindings[initialBinding.id] = this.isCreate ? undefined : initialBinding
|
||||||
afterBindings[initialBinding.id] = undefined
|
afterBindings[initialBinding.id] = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentBindingId) {
|
if (currentBindingId) {
|
||||||
beforeBindings[currentBindingId] = undefined
|
beforeBindings[currentBindingId] = undefined
|
||||||
afterBindings[currentBindingId] = this.app.page.bindings[currentBindingId]
|
afterBindings[currentBindingId] = this.app.page.bindings[currentBindingId]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startBindingShapeId) {
|
if (startBindingShapeId) {
|
||||||
beforeBindings[newStartBindingId] = undefined
|
beforeBindings[newStartBindingId] = undefined
|
||||||
afterBindings[newStartBindingId] = this.app.page.bindings[newStartBindingId]
|
afterBindings[newStartBindingId] = this.app.page.bindings[newStartBindingId]
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { SessionType, ShapesWithProp, TldrawCommand, TldrawPatch, TDStatus } fro
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
import { BaseSession } from '../BaseSession'
|
import { BaseSession } from '../BaseSession'
|
||||||
import type { TldrawApp } from '../../internal'
|
import type { TldrawApp } from '../../internal'
|
||||||
import { TLPerformanceMode } from '@tldraw/core'
|
|
||||||
|
|
||||||
export class HandleSession extends BaseSession {
|
export class HandleSession extends BaseSession {
|
||||||
type = SessionType.Handle
|
type = SessionType.Handle
|
||||||
|
@ -29,7 +28,7 @@ export class HandleSession extends BaseSession {
|
||||||
update = (): TldrawPatch | undefined => {
|
update = (): TldrawPatch | undefined => {
|
||||||
const {
|
const {
|
||||||
initialShape,
|
initialShape,
|
||||||
app: { currentPageId, currentPoint, shiftKey, altKey, metaKey },
|
app: { currentPageId, currentPoint },
|
||||||
} = this
|
} = this
|
||||||
|
|
||||||
const shape = this.app.getShape<ShapesWithProp<'handles'>>(initialShape.id)
|
const shape = this.app.getShape<ShapesWithProp<'handles'>>(initialShape.id)
|
||||||
|
@ -42,15 +41,15 @@ export class HandleSession extends BaseSession {
|
||||||
|
|
||||||
const delta = Vec.sub(currentPoint, handles[handleId].point)
|
const delta = Vec.sub(currentPoint, handles[handleId].point)
|
||||||
|
|
||||||
const handle = {
|
const handleChanges = {
|
||||||
|
[handleId]: {
|
||||||
...handles[handleId],
|
...handles[handleId],
|
||||||
point: Vec.sub(Vec.add(handles[handleId].point, delta), shape.point),
|
point: Vec.sub(Vec.add(handles[handleId].point, delta), shape.point),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// First update the handle's next point
|
// First update the handle's next point
|
||||||
const change = TLDR.getShapeUtil(shape).onHandleChange?.(shape, {
|
const change = TLDR.getShapeUtil(shape).onHandleChange?.(shape, handleChanges)
|
||||||
[handleId]: handle,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!change) return
|
if (!change) return
|
||||||
|
|
||||||
|
|
|
@ -405,6 +405,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||||
onHandleChange = (shape: T, handles: Partial<T['handles']>): Partial<T> | void => {
|
onHandleChange = (shape: T, handles: Partial<T['handles']>): Partial<T> | void => {
|
||||||
let nextHandles = Utils.deepMerge<ArrowShape['handles']>(shape.handles, handles)
|
let nextHandles = Utils.deepMerge<ArrowShape['handles']>(shape.handles, handles)
|
||||||
let nextBend = shape.bend
|
let nextBend = shape.bend
|
||||||
|
|
||||||
nextHandles = Utils.deepMerge(nextHandles, {
|
nextHandles = Utils.deepMerge(nextHandles, {
|
||||||
start: {
|
start: {
|
||||||
point: Vec.toFixed(nextHandles.start.point),
|
point: Vec.toFixed(nextHandles.start.point),
|
||||||
|
@ -413,31 +414,43 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||||
point: Vec.toFixed(nextHandles.end.point),
|
point: Vec.toFixed(nextHandles.end.point),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// This will produce NaN values
|
// This will produce NaN values
|
||||||
if (Vec.isEqual(nextHandles.start.point, nextHandles.end.point)) return
|
if (Vec.isEqual(nextHandles.start.point, nextHandles.end.point)) return
|
||||||
|
|
||||||
// If the user is moving the bend handle, we want to move the bend point
|
// If the user is moving the bend handle, we want to move the bend point
|
||||||
if ('bend' in handles) {
|
if ('bend' in handles) {
|
||||||
const { start, end, bend } = nextHandles
|
const { start, end, bend } = nextHandles
|
||||||
|
|
||||||
const distance = Vec.dist(start.point, end.point)
|
const distance = Vec.dist(start.point, end.point)
|
||||||
const midPoint = Vec.med(start.point, end.point)
|
const midPoint = Vec.med(start.point, end.point)
|
||||||
const angle = Vec.angle(start.point, end.point)
|
const angle = Vec.angle(start.point, end.point)
|
||||||
const u = Vec.uni(Vec.vec(start.point, end.point))
|
const u = Vec.uni(Vec.vec(start.point, end.point))
|
||||||
|
|
||||||
// Create a line segment perendicular to the line between the start and end points
|
// Create a line segment perendicular to the line between the start and end points
|
||||||
const ap = Vec.add(midPoint, Vec.mul(Vec.per(u), distance / 2))
|
const ap = Vec.add(midPoint, Vec.mul(Vec.per(u), distance))
|
||||||
const bp = Vec.sub(midPoint, Vec.mul(Vec.per(u), distance / 2))
|
const bp = Vec.sub(midPoint, Vec.mul(Vec.per(u), distance))
|
||||||
|
|
||||||
const bendPoint = Vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
const bendPoint = Vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
||||||
|
|
||||||
// Find the distance between the midpoint and the nearest point on the
|
// Find the distance between the midpoint and the nearest point on the
|
||||||
// line segment to the bend handle's dragged point
|
// line segment to the bend handle's dragged point
|
||||||
const bendDist = Vec.dist(midPoint, bendPoint)
|
const bendDist = Vec.dist(midPoint, bendPoint)
|
||||||
|
|
||||||
// The shape's "bend" is the ratio of the bend to the distance between
|
// The shape's "bend" is the ratio of the bend to the distance between
|
||||||
// the start and end points. If the bend is below a certain amount, the
|
// the start and end points. If the bend is below a certain amount, the
|
||||||
// bend should be zero.
|
// bend should be zero.
|
||||||
nextBend = Utils.clamp(bendDist / (distance / 2), -0.99, 0.99)
|
const realBend = bendDist / (distance / 2)
|
||||||
|
|
||||||
|
nextBend = Utils.clamp(realBend, -0.99, 0.99)
|
||||||
|
|
||||||
// If the point is to the left of the line segment, we make the bend
|
// If the point is to the left of the line segment, we make the bend
|
||||||
// negative, otherwise it's positive.
|
// negative, otherwise it's positive.
|
||||||
const angleToBend = Vec.angle(start.point, bendPoint)
|
const angleToBend = Vec.angle(start.point, bendPoint)
|
||||||
|
|
||||||
// If resulting bend is low enough that the handle will snap to center,
|
// If resulting bend is low enough that the handle will snap to center,
|
||||||
// then also snap the bend to center
|
// then also snap the bend to center
|
||||||
|
|
||||||
if (Vec.isEqual(midPoint, getBendPoint(nextHandles, nextBend))) {
|
if (Vec.isEqual(midPoint, getBendPoint(nextHandles, nextBend))) {
|
||||||
nextBend = 0
|
nextBend = 0
|
||||||
} else if (isAngleBetween(angle, angle + Math.PI, angleToBend)) {
|
} else if (isAngleBetween(angle, angle + Math.PI, angleToBend)) {
|
||||||
|
@ -445,6 +458,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||||
nextBend *= -1
|
nextBend *= -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextShape = {
|
const nextShape = {
|
||||||
point: shape.point,
|
point: shape.point,
|
||||||
bend: nextBend,
|
bend: nextBend,
|
||||||
|
@ -456,20 +470,22 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zero out the handles to prevent handles with negative points. If a handle's x or y
|
// Zero out the handles to prevent handles with negative points. If a handle's x or y
|
||||||
// is below zero, we need to move the shape left or up to make it zero.
|
// is below zero, we need to move the shape left or up to make it zero.
|
||||||
const topLeft = shape.point
|
const topLeft = shape.point
|
||||||
|
|
||||||
const nextBounds = this.getBounds({ ...nextShape } as ArrowShape)
|
const nextBounds = this.getBounds({ ...nextShape } as ArrowShape)
|
||||||
|
|
||||||
const offset = Vec.sub([nextBounds.minX, nextBounds.minY], topLeft)
|
const offset = Vec.sub([nextBounds.minX, nextBounds.minY], topLeft)
|
||||||
|
|
||||||
if (!Vec.isEqual(offset, [0, 0])) {
|
if (!Vec.isEqual(offset, [0, 0])) {
|
||||||
Object.values(nextShape.handles).forEach((handle) => {
|
Object.values(nextShape.handles).forEach((handle) => {
|
||||||
handle.point = Vec.toFixed(Vec.sub(handle.point, offset))
|
handle.point = Vec.toFixed(Vec.sub(handle.point, offset))
|
||||||
})
|
})
|
||||||
nextShape.point = Vec.toFixed(Vec.add(nextShape.point, offset))
|
nextShape.point = Vec.toFixed(Vec.add(nextShape.point, offset))
|
||||||
if (Vec.isEqual(nextShape.point, [0, 0])) {
|
|
||||||
throw Error('here!')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextShape
|
return nextShape
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue