Removes arrow command / session, replaces with handle sessions
This commit is contained in:
parent
5475197509
commit
5c87bfd4c5
9 changed files with 120 additions and 322 deletions
|
@ -1,46 +0,0 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import tld from 'utils/tld'
|
||||
import { ArrowSnapshot } from 'state/sessions/arrow-session'
|
||||
|
||||
export default function arrowCommand(
|
||||
data: Data,
|
||||
before: ArrowSnapshot,
|
||||
after: ArrowSnapshot
|
||||
): void {
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: 'point_arrow',
|
||||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data, isInitial) {
|
||||
if (isInitial) return
|
||||
|
||||
const { initialShape } = after
|
||||
|
||||
const page = tld.getPage(data)
|
||||
|
||||
page.shapes[initialShape.id] = initialShape
|
||||
|
||||
const selectedIds = tld.getSelectedIds(data)
|
||||
selectedIds.clear()
|
||||
selectedIds.add(initialShape.id)
|
||||
data.hoveredId = undefined
|
||||
data.pointedId = undefined
|
||||
},
|
||||
undo(data) {
|
||||
const { initialShape } = before
|
||||
const shapes = tld.getPage(data).shapes
|
||||
|
||||
delete shapes[initialShape.id]
|
||||
|
||||
const selectedIds = tld.getSelectedIds(data)
|
||||
selectedIds.clear()
|
||||
data.hoveredId = undefined
|
||||
data.pointedId = undefined
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import tld from 'utils/tld'
|
||||
import { HandleSnapshot } from 'state/sessions/handle-session'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
||||
export default function handleCommand(
|
||||
data: Data,
|
||||
before: HandleSnapshot,
|
||||
after: HandleSnapshot
|
||||
): void {
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: 'moved_handle',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { initialShape } = after
|
||||
|
||||
const page = tld.getPage(data)
|
||||
const shape = page.shapes[initialShape.id]
|
||||
|
||||
getShapeUtils(shape)
|
||||
.onHandleChange(shape, initialShape.handles)
|
||||
.onSessionComplete(shape)
|
||||
},
|
||||
undo(data) {
|
||||
const { initialShape } = before
|
||||
|
||||
const page = tld.getPage(data)
|
||||
page.shapes[initialShape.id] = initialShape
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import align from './align'
|
||||
import arrow from './arrow'
|
||||
import changePage from './change-page'
|
||||
import createPage from './create-page'
|
||||
import deletePage from './delete-page'
|
||||
|
@ -11,7 +10,6 @@ import duplicate from './duplicate'
|
|||
import edit from './edit'
|
||||
import generate from './generate'
|
||||
import group from './group'
|
||||
import handle from './handle'
|
||||
import move from './move'
|
||||
import moveToPage from './move-to-page'
|
||||
import mutate from './mutate'
|
||||
|
@ -30,7 +28,6 @@ import ungroup from './ungroup'
|
|||
|
||||
const commands = {
|
||||
align,
|
||||
arrow,
|
||||
changePage,
|
||||
createPage,
|
||||
deletePage,
|
||||
|
@ -42,7 +39,6 @@ const commands = {
|
|||
edit,
|
||||
generate,
|
||||
group,
|
||||
handle,
|
||||
move,
|
||||
moveToPage,
|
||||
mutate,
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
import { ArrowShape, Data } from 'types'
|
||||
import vec from 'utils/vec'
|
||||
import BaseSession from './base-session'
|
||||
import commands from 'state/commands'
|
||||
import { deepClone, getBoundsFromPoints, setToArray } from 'utils'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import tld from 'utils/tld'
|
||||
|
||||
export default class ArrowSession extends BaseSession {
|
||||
points: number[][]
|
||||
origin: number[]
|
||||
snapshot: ArrowSnapshot
|
||||
isLocked: boolean
|
||||
lockedDirection: 'horizontal' | 'vertical'
|
||||
|
||||
constructor(data: Data, id: string, point: number[], isLocked: boolean) {
|
||||
super(data)
|
||||
isLocked
|
||||
this.origin = point
|
||||
this.points = [[0, 0]]
|
||||
this.snapshot = getArrowSnapshot(data, id)
|
||||
}
|
||||
|
||||
update(data: Data, point: number[], isLocked = false): void {
|
||||
const { id } = this.snapshot
|
||||
|
||||
const delta = vec.vec(this.origin, point)
|
||||
|
||||
if (isLocked) {
|
||||
if (!this.isLocked && this.points.length > 1) {
|
||||
this.isLocked = true
|
||||
|
||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
||||
this.lockedDirection = 'vertical'
|
||||
} else {
|
||||
this.lockedDirection = 'horizontal'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.isLocked) {
|
||||
this.isLocked = false
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isLocked) {
|
||||
if (this.lockedDirection === 'vertical') {
|
||||
point[0] = this.origin[0]
|
||||
} else {
|
||||
point[1] = this.origin[1]
|
||||
}
|
||||
}
|
||||
|
||||
const shape = tld.getPage(data).shapes[id] as ArrowShape
|
||||
|
||||
getShapeUtils(shape).onHandleChange(shape, {
|
||||
end: {
|
||||
...shape.handles.end,
|
||||
point: vec.sub(point, shape.point),
|
||||
},
|
||||
})
|
||||
|
||||
tld.updateParents(data, [shape.id])
|
||||
}
|
||||
|
||||
cancel(data: Data): void {
|
||||
const { id, initialShape } = this.snapshot
|
||||
|
||||
const shape = tld.getPage(data).shapes[id] as ArrowShape
|
||||
|
||||
getShapeUtils(shape)
|
||||
.onHandleChange(shape, { end: initialShape.handles.end })
|
||||
.setProperty(shape, 'point', initialShape.point)
|
||||
|
||||
tld.updateParents(data, [shape.id])
|
||||
}
|
||||
|
||||
complete(data: Data): void {
|
||||
const { id } = this.snapshot
|
||||
|
||||
const shape = tld.getPage(data).shapes[id] as ArrowShape
|
||||
|
||||
const { start, end, bend } = shape.handles
|
||||
|
||||
// Normalize point and handles
|
||||
|
||||
const bounds = getBoundsFromPoints([start.point, end.point])
|
||||
const corner = [bounds.minX, bounds.minY]
|
||||
|
||||
const newPoint = vec.add(shape.point, corner)
|
||||
|
||||
const nextHandles = {
|
||||
start: { ...start, point: vec.sub(start.point, corner) },
|
||||
end: { ...end, point: vec.sub(end.point, corner) },
|
||||
bend: { ...bend, point: vec.sub(bend.point, corner) },
|
||||
}
|
||||
|
||||
getShapeUtils(shape)
|
||||
.setProperty(shape, 'handles', nextHandles)
|
||||
.setProperty(shape, 'point', newPoint)
|
||||
.onHandleChange(shape, nextHandles)
|
||||
|
||||
commands.arrow(
|
||||
data,
|
||||
this.snapshot,
|
||||
getArrowSnapshot(data, this.snapshot.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function getArrowSnapshot(data: Data, id: string) {
|
||||
const initialShape = deepClone(tld.getPage(data).shapes[id]) as ArrowShape
|
||||
|
||||
return {
|
||||
id,
|
||||
initialShape,
|
||||
selectedIds: setToArray(tld.getSelectedIds(data)),
|
||||
currentPageId: data.currentPageId,
|
||||
}
|
||||
}
|
||||
|
||||
export type ArrowSnapshot = ReturnType<typeof getArrowSnapshot>
|
|
@ -1,4 +1,4 @@
|
|||
import { Data } from 'types'
|
||||
import { Data, Shape } from 'types'
|
||||
import vec from 'utils/vec'
|
||||
import BaseSession from './base-session'
|
||||
import commands from 'state/commands'
|
||||
|
@ -9,73 +9,58 @@ import { deepClone } from 'utils'
|
|||
export default class HandleSession extends BaseSession {
|
||||
delta = [0, 0]
|
||||
origin: number[]
|
||||
snapshot: HandleSnapshot
|
||||
shiftKey: boolean
|
||||
initialShape: Shape
|
||||
handleId: string
|
||||
|
||||
constructor(data: Data, shapeId: string, handleId: string, point: number[]) {
|
||||
super(data)
|
||||
this.origin = point
|
||||
this.snapshot = getHandleSnapshot(data, shapeId, handleId)
|
||||
this.handleId = handleId
|
||||
this.initialShape = deepClone(tld.getShape(data, shapeId))
|
||||
}
|
||||
|
||||
update(data: Data, point: number[], isAligned: boolean): void {
|
||||
const { handleId, initialShape } = this.snapshot
|
||||
const shape = tld.getPage(data).shapes[initialShape.id]
|
||||
update(
|
||||
data: Data,
|
||||
point: number[],
|
||||
shiftKey: boolean,
|
||||
altKey: boolean,
|
||||
metaKey: boolean
|
||||
): void {
|
||||
const shape = tld.getPage(data).shapes[this.initialShape.id]
|
||||
|
||||
this.shiftKey = shiftKey
|
||||
|
||||
const delta = vec.vec(this.origin, point)
|
||||
|
||||
if (isAligned) {
|
||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
||||
delta[0] = 0
|
||||
} else {
|
||||
delta[1] = 0
|
||||
}
|
||||
}
|
||||
const handles = this.initialShape.handles
|
||||
|
||||
const handles = initialShape.handles
|
||||
|
||||
// rotate the delta ?
|
||||
// rotate the handle ?
|
||||
// rotate the shape around the previous center point
|
||||
|
||||
getShapeUtils(shape).onHandleChange(shape, {
|
||||
[handleId]: {
|
||||
...handles[handleId],
|
||||
point: vec.add(handles[handleId].point, delta), // vec.rot(delta, shape.rotation)),
|
||||
getShapeUtils(shape).onHandleChange(
|
||||
shape,
|
||||
{
|
||||
[this.handleId]: {
|
||||
...handles[this.handleId],
|
||||
point: vec.round(vec.add(handles[this.handleId].point, delta)), // vec.rot(delta, shape.rotation)),
|
||||
},
|
||||
})
|
||||
},
|
||||
{ delta, shiftKey, altKey, metaKey }
|
||||
)
|
||||
}
|
||||
|
||||
cancel(data: Data): void {
|
||||
const { initialShape } = this.snapshot
|
||||
tld.getPage(data).shapes[initialShape.id] = initialShape
|
||||
tld.getPage(data).shapes[this.initialShape.id] = this.initialShape
|
||||
}
|
||||
|
||||
complete(data: Data): void {
|
||||
commands.handle(
|
||||
data,
|
||||
this.snapshot,
|
||||
getHandleSnapshot(
|
||||
data,
|
||||
this.snapshot.initialShape.id,
|
||||
this.snapshot.handleId
|
||||
)
|
||||
)
|
||||
const before = this.initialShape
|
||||
const after = deepClone(tld.getShape(data, before.id))
|
||||
commands.mutate(data, [before], [after])
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function getHandleSnapshot(
|
||||
data: Data,
|
||||
shapeId: string,
|
||||
handleId: string
|
||||
) {
|
||||
const initialShape = deepClone(tld.getShape(data, shapeId))
|
||||
|
||||
return {
|
||||
currentPageId: data.currentPageId,
|
||||
handleId,
|
||||
initialShape,
|
||||
}
|
||||
export function getHandleSnapshot(data: Data, shapeId: string) {
|
||||
return deepClone(tld.getShape(data, shapeId))
|
||||
}
|
||||
|
||||
export type HandleSnapshot = ReturnType<typeof getHandleSnapshot>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ArrowSession from './arrow-session'
|
||||
import BaseSession from './base-session'
|
||||
import BrushSession from './brush-session'
|
||||
import DirectionSession from './direction-session'
|
||||
|
@ -11,7 +10,6 @@ import HandleSession from './handle-session'
|
|||
import EditSession from './edit-session'
|
||||
|
||||
export {
|
||||
ArrowSession,
|
||||
BaseSession,
|
||||
BrushSession,
|
||||
DirectionSession,
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
circleFromThreePoints,
|
||||
isAngleBetween,
|
||||
getPerfectDashProps,
|
||||
clampToRotationToSegments,
|
||||
} from 'utils'
|
||||
import {
|
||||
ArrowShape,
|
||||
|
@ -257,7 +258,10 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
end.point = vec.rotWith(end.point, mp, delta)
|
||||
bend.point = vec.rotWith(bend.point, mp, delta)
|
||||
|
||||
this.onHandleChange(shape, shape.handles)
|
||||
this.onHandleChange(shape, shape.handles, {
|
||||
delta: [0, 0],
|
||||
shiftKey: false,
|
||||
})
|
||||
|
||||
return this
|
||||
},
|
||||
|
@ -269,7 +273,10 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
end.point = vec.rotWith(end.point, mp, delta)
|
||||
bend.point = vec.rotWith(bend.point, mp, delta)
|
||||
|
||||
this.onHandleChange(shape, shape.handles)
|
||||
this.onHandleChange(shape, shape.handles, {
|
||||
delta: [0, 0],
|
||||
shiftKey: false,
|
||||
})
|
||||
|
||||
return this
|
||||
},
|
||||
|
@ -401,37 +408,63 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
return this
|
||||
},
|
||||
|
||||
onHandleChange(shape, handles) {
|
||||
onHandleChange(shape, handles, { shiftKey }) {
|
||||
// Apple changes to the handles
|
||||
for (const id in handles) {
|
||||
const handle = handles[id]
|
||||
|
||||
shape.handles[handle.id] = handle
|
||||
}
|
||||
|
||||
// If the user is holding shift, we want to snap the handles to angles
|
||||
for (const id in handles) {
|
||||
if ((id === 'start' || id === 'end') && shiftKey) {
|
||||
const point = handles[id].point
|
||||
const other = id === 'start' ? shape.handles.end : shape.handles.start
|
||||
const angle = vec.angle(other.point, point)
|
||||
const distance = vec.dist(other.point, point)
|
||||
const newAngle = clampToRotationToSegments(angle, 24)
|
||||
|
||||
shape.handles[id].point = vec.nudgeAtAngle(
|
||||
other.point,
|
||||
newAngle,
|
||||
distance
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const midPoint = vec.med(shape.handles.start.point, shape.handles.end.point)
|
||||
|
||||
// If the user is moving the bend handle, we want to move the bend point
|
||||
if ('bend' in handles) {
|
||||
const { start, end, bend } = shape.handles
|
||||
|
||||
const dist = vec.dist(start.point, end.point)
|
||||
|
||||
const distance = vec.dist(start.point, end.point)
|
||||
const midPoint = vec.med(start.point, end.point)
|
||||
const angle = vec.angle(start.point, end.point)
|
||||
const u = vec.uni(vec.vec(start.point, end.point))
|
||||
const ap = vec.add(midPoint, vec.mul(vec.per(u), dist / 2))
|
||||
const bp = vec.sub(midPoint, vec.mul(vec.per(u), dist / 2))
|
||||
|
||||
bend.point = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
||||
shape.bend = vec.dist(bend.point, midPoint) / (dist / 2)
|
||||
// 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 bp = vec.sub(midPoint, vec.mul(vec.per(u), distance / 2))
|
||||
|
||||
const sa = vec.angle(end.point, start.point)
|
||||
const la = sa - Math.PI / 2
|
||||
// Find the nearest point on the line segment to the bend handle
|
||||
bend.point = vec.round(
|
||||
vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
||||
)
|
||||
|
||||
if (isAngleBetween(sa, la, vec.angle(end.point, bend.point))) {
|
||||
// The "bend" is the distance between this point on the line segment
|
||||
// and the midpoint, divided by the distance between the start and end points.
|
||||
shape.bend = vec.dist(bend.point, midPoint) / (distance / 2)
|
||||
|
||||
// If the point is to the left of the line segment, we make the bend
|
||||
// negative, otherwise it's positive.
|
||||
const angleToBend = vec.angle(start.point, bend.point)
|
||||
if (isAngleBetween(angle, angle + Math.PI / 2, angleToBend)) {
|
||||
shape.bend *= -1
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
shape.handles.bend.point = getBendPoint(shape)
|
||||
}
|
||||
|
||||
if (vec.isEqual(shape.handles.bend.point, midPoint)) {
|
||||
shape.bend = 0
|
||||
|
@ -498,9 +531,11 @@ function getBendPoint(shape: ArrowShape) {
|
|||
const bendDist = (dist / 2) * shape.bend
|
||||
const u = vec.uni(vec.vec(start.point, end.point))
|
||||
|
||||
return Math.abs(bendDist) < 10
|
||||
return vec.round(
|
||||
Math.abs(bendDist) < 10
|
||||
? midPoint
|
||||
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||
)
|
||||
}
|
||||
|
||||
function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||
|
@ -513,23 +548,14 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
|||
|
||||
const st = Math.abs(getRandom())
|
||||
|
||||
const stroke = getStroke(
|
||||
[
|
||||
start.point,
|
||||
...vec.pointsBetween(start.point, end.point),
|
||||
end.point,
|
||||
end.point,
|
||||
end.point,
|
||||
],
|
||||
{
|
||||
const stroke = getStroke([...vec.pointsBetween(start.point, end.point)], {
|
||||
size: strokeWidth / 2,
|
||||
thinning: 0.5 + getRandom() * 0.3,
|
||||
easing: (t) => t * t,
|
||||
end: { taper: 1 },
|
||||
start: { taper: 1 + 32 * (st * st * st) },
|
||||
simulatePressure: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
pathCache.set(shape, getSvgPathFromStroke(stroke))
|
||||
}
|
||||
|
|
|
@ -846,7 +846,7 @@ const state = createState({
|
|||
},
|
||||
editing: {
|
||||
onExit: 'completeSession',
|
||||
onEnter: 'startArrowSession',
|
||||
onEnter: 'startArrowHandleSession',
|
||||
on: {
|
||||
STOPPED_POINTING: [
|
||||
'completeSession',
|
||||
|
@ -862,10 +862,10 @@ const state = createState({
|
|||
to: 'arrow.creating',
|
||||
else: { to: 'selecting' },
|
||||
},
|
||||
PRESSED_SHIFT: 'keyUpdateArrowSession',
|
||||
RELEASED_SHIFT: 'keyUpdateArrowSession',
|
||||
MOVED_POINTER: 'updateArrowSession',
|
||||
PANNED_CAMERA: 'updateArrowSession',
|
||||
PRESSED_SHIFT: 'keyUpdateHandleSession',
|
||||
RELEASED_SHIFT: 'keyUpdateHandleSession',
|
||||
MOVED_POINTER: 'updateHandleSession',
|
||||
PANNED_CAMERA: 'updateHandleSession',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1186,7 +1186,7 @@ const state = createState({
|
|||
return tld.getShape(data, payload.target) !== undefined
|
||||
},
|
||||
isPointingRotationHandle(
|
||||
data,
|
||||
_data,
|
||||
payload: { target: Edge | Corner | 'rotate' }
|
||||
) {
|
||||
return payload.target === 'rotate'
|
||||
|
@ -1484,19 +1484,23 @@ const state = createState({
|
|||
},
|
||||
keyUpdateHandleSession(
|
||||
data,
|
||||
payload: { shiftKey: boolean; altKey: boolean }
|
||||
payload: { shiftKey: boolean; altKey: boolean; metaKey: boolean }
|
||||
) {
|
||||
session.update<Sessions.HandleSession>(
|
||||
data,
|
||||
tld.screenToWorld(inputs.pointer.point, data),
|
||||
payload.shiftKey
|
||||
payload.shiftKey,
|
||||
payload.altKey,
|
||||
payload.metaKey
|
||||
)
|
||||
},
|
||||
updateHandleSession(data, payload: PointerInfo) {
|
||||
session.update<Sessions.HandleSession>(
|
||||
data,
|
||||
tld.screenToWorld(payload.point, data),
|
||||
payload.shiftKey
|
||||
payload.shiftKey,
|
||||
payload.altKey,
|
||||
payload.metaKey
|
||||
)
|
||||
},
|
||||
|
||||
|
@ -1582,32 +1586,19 @@ const state = createState({
|
|||
},
|
||||
|
||||
// Arrow
|
||||
startArrowSession(data, payload: PointerInfo) {
|
||||
const id = Array.from(tld.getSelectedIds(data).values())[0]
|
||||
startArrowHandleSession(data) {
|
||||
const shapeId = Array.from(tld.getSelectedIds(data).values())[0]
|
||||
const handleId = 'end'
|
||||
|
||||
session.begin(
|
||||
new Sessions.ArrowSession(
|
||||
new Sessions.HandleSession(
|
||||
data,
|
||||
id,
|
||||
tld.screenToWorld(inputs.pointer.origin, data),
|
||||
payload.shiftKey
|
||||
shapeId,
|
||||
handleId,
|
||||
tld.screenToWorld(inputs.pointer.origin, data)
|
||||
)
|
||||
)
|
||||
},
|
||||
keyUpdateArrowSession(data, payload: PointerInfo) {
|
||||
session.update<Sessions.ArrowSession>(
|
||||
data,
|
||||
tld.screenToWorld(inputs.pointer.point, data),
|
||||
payload.shiftKey
|
||||
)
|
||||
},
|
||||
updateArrowSession(data, payload: PointerInfo) {
|
||||
session.update<Sessions.ArrowSession>(
|
||||
data,
|
||||
tld.screenToWorld(payload.point, data),
|
||||
payload.shiftKey
|
||||
)
|
||||
},
|
||||
|
||||
/* -------------------- Selection ------------------- */
|
||||
|
||||
|
|
8
types.ts
8
types.ts
|
@ -577,7 +577,13 @@ export interface ShapeUtility<K extends Shape> {
|
|||
onHandleChange(
|
||||
this: ShapeUtility<K>,
|
||||
shape: Mutable<K>,
|
||||
handle: Partial<K['handles']>
|
||||
handle: Partial<K['handles']>,
|
||||
info?: Partial<{
|
||||
delta: number[]
|
||||
shiftKey: boolean
|
||||
altKey: boolean
|
||||
metaKey: boolean
|
||||
}>
|
||||
): ShapeUtility<K>
|
||||
|
||||
onDoublePointHandle(
|
||||
|
|
Loading…
Reference in a new issue