From 5c87bfd4c53f680142599162b814c2728af84aee Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 8 Jul 2021 11:05:25 +0100 Subject: [PATCH] Removes arrow command / session, replaces with handle sessions --- state/commands/arrow.ts | 46 ------------ state/commands/handle.ts | 36 --------- state/commands/index.ts | 4 - state/sessions/arrow-session.ts | 122 ------------------------------- state/sessions/handle-session.ts | 79 ++++++++------------ state/sessions/index.ts | 2 - state/shape-utils/arrow.tsx | 96 +++++++++++++++--------- state/state.ts | 49 +++++-------- types.ts | 8 +- 9 files changed, 120 insertions(+), 322 deletions(-) delete mode 100644 state/commands/arrow.ts delete mode 100644 state/commands/handle.ts delete mode 100644 state/sessions/arrow-session.ts diff --git a/state/commands/arrow.ts b/state/commands/arrow.ts deleted file mode 100644 index f80c9c85f..000000000 --- a/state/commands/arrow.ts +++ /dev/null @@ -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 - }, - }) - ) -} diff --git a/state/commands/handle.ts b/state/commands/handle.ts deleted file mode 100644 index e5078b2ab..000000000 --- a/state/commands/handle.ts +++ /dev/null @@ -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 - }, - }) - ) -} diff --git a/state/commands/index.ts b/state/commands/index.ts index b803e9445..a29c58ae3 100644 --- a/state/commands/index.ts +++ b/state/commands/index.ts @@ -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, diff --git a/state/sessions/arrow-session.ts b/state/sessions/arrow-session.ts deleted file mode 100644 index da545861b..000000000 --- a/state/sessions/arrow-session.ts +++ /dev/null @@ -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 diff --git a/state/sessions/handle-session.ts b/state/sessions/handle-session.ts index 6500be207..fbbffe508 100644 --- a/state/sessions/handle-session.ts +++ b/state/sessions/handle-session.ts @@ -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 diff --git a/state/sessions/index.ts b/state/sessions/index.ts index b50be84b5..4a4b30831 100644 --- a/state/sessions/index.ts +++ b/state/sessions/index.ts @@ -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, diff --git a/state/shape-utils/arrow.tsx b/state/shape-utils/arrow.tsx index ede0ad3f7..bc53358a5 100644 --- a/state/shape-utils/arrow.tsx +++ b/state/shape-utils/arrow.tsx @@ -11,6 +11,7 @@ import { circleFromThreePoints, isAngleBetween, getPerfectDashProps, + clampToRotationToSegments, } from 'utils' import { ArrowShape, @@ -257,7 +258,10 @@ const arrow = registerShapeUtils({ 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({ 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,38 +408,64 @@ const arrow = registerShapeUtils({ 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) } - 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 - ? midPoint - : vec.add(midPoint, vec.mul(vec.per(u), bendDist)) + 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, - ], - { - 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, - } - ) + 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)) } diff --git a/state/state.ts b/state/state.ts index 76dea97b2..38f350c55 100644 --- a/state/state.ts +++ b/state/state.ts @@ -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( data, tld.screenToWorld(inputs.pointer.point, data), - payload.shiftKey + payload.shiftKey, + payload.altKey, + payload.metaKey ) }, updateHandleSession(data, payload: PointerInfo) { session.update( 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( - data, - tld.screenToWorld(inputs.pointer.point, data), - payload.shiftKey - ) - }, - updateArrowSession(data, payload: PointerInfo) { - session.update( - data, - tld.screenToWorld(payload.point, data), - payload.shiftKey - ) - }, /* -------------------- Selection ------------------- */ diff --git a/types.ts b/types.ts index 5acdd955d..52875d41b 100644 --- a/types.ts +++ b/types.ts @@ -577,7 +577,13 @@ export interface ShapeUtility { onHandleChange( this: ShapeUtility, shape: Mutable, - handle: Partial + handle: Partial, + info?: Partial<{ + delta: number[] + shiftKey: boolean + altKey: boolean + metaKey: boolean + }> ): ShapeUtility onDoublePointHandle(