From 81141e7bb58f7e9a16b6b21718dddcbf5c997f90 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 5 Jun 2021 15:29:49 +0100 Subject: [PATCH] improves arrow rotation --- components/canvas/bounds/handles.tsx | 11 +-- lib/shape-utils/arrow.tsx | 119 ++++++++++++++++++++++----- lib/shape-utils/index.tsx | 7 ++ state/commands/handle.ts | 23 +++--- state/commands/rotate.ts | 12 +-- state/data.ts | 50 +++++------ state/sessions/handle-session.ts | 8 +- state/state.ts | 13 ++- todo.md | 22 +++-- 9 files changed, 179 insertions(+), 86 deletions(-) diff --git a/components/canvas/bounds/handles.tsx b/components/canvas/bounds/handles.tsx index 36310594d..4b0bba5a8 100644 --- a/components/canvas/bounds/handles.tsx +++ b/components/canvas/bounds/handles.tsx @@ -30,7 +30,6 @@ export default function Handles() { {Object.values(shape.handles).map((handle) => ( @@ -39,15 +38,7 @@ export default function Handles() { ) } -function Handle({ - shapeId, - id, - point, -}: { - shapeId: string - id: string - point: number[] -}) { +function Handle({ id, point }: { id: string; point: number[] }) { const rGroup = useRef(null) const events = useHandleEvents(id, rGroup) diff --git a/lib/shape-utils/arrow.tsx b/lib/shape-utils/arrow.tsx index 4aec325d5..26ecd9a62 100644 --- a/lib/shape-utils/arrow.tsx +++ b/lib/shape-utils/arrow.tsx @@ -3,6 +3,7 @@ import * as vec from 'utils/vec' import * as svg from 'utils/svg' import { ArrowShape, + Bounds, ColorStyle, DashStyle, ShapeHandle, @@ -10,7 +11,13 @@ import { SizeStyle, } from 'types' import { registerShapeUtils } from './index' -import { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils' +import { + circleFromThreePoints, + clamp, + getBoundsCenter, + isAngleBetween, + rotateBounds, +} from 'utils/utils' import { pointInBounds } from 'utils/bounds' import { intersectArcBounds, @@ -170,25 +177,27 @@ const arrow = registerShapeUtils({ ) }, + rotateBy(shape, delta) { + const { start, end, bend } = shape.handles + const mp = vec.med(start.point, end.point) + start.point = vec.rotWith(start.point, mp, delta) + end.point = vec.rotWith(end.point, mp, delta) + bend.point = vec.rotWith(bend.point, mp, delta) + + this.onHandleChange(shape, shape.handles) + + return this + }, + rotateTo(shape, rotation, delta) { const { start, end, bend } = shape.handles - // const mp = vec.med(start.point, end.point) - // start.point = vec.rotWith(start.point, mp, delta) - // end.point = vec.rotWith(end.point, mp, delta) - // bend.point = vec.rotWith(bend.point, mp, delta) - // this.onHandleChange(shape, shape.handles) + const mp = vec.med(start.point, end.point) + start.point = vec.rotWith(start.point, mp, delta) + end.point = vec.rotWith(end.point, mp, delta) + bend.point = vec.rotWith(bend.point, mp, delta) - // const bounds = this.getBounds(shape) + this.onHandleChange(shape, shape.handles) - // const offset = vec.sub([bounds.minX, bounds.minY], shape.point) - - // this.translateTo(shape, vec.add(shape.point, offset)) - - // start.point = vec.sub(start.point, offset) - // end.point = vec.sub(end.point, offset) - // bend.point = vec.sub(bend.point, offset) - - shape.rotation = rotation return this }, @@ -202,11 +211,16 @@ const arrow = registerShapeUtils({ }, getRotatedBounds(shape) { - if (!this.boundsCache.has(shape)) { - this.boundsCache.set(shape, getBoundsFromPoints(shape.points)) - } + const { start, end } = shape.handles + return translateBounds( + getBoundsFromPoints([start.point, end.point], shape.rotation), + shape.point + ) + }, - return translateBounds(this.boundsCache.get(shape), shape.point) + getCenter(shape) { + const { start, end } = shape.handles + return vec.add(shape.point, vec.med(start.point, end.point)) }, hitTest(shape, point) { @@ -281,6 +295,9 @@ const arrow = registerShapeUtils({ }, onHandleChange(shape, handles) { + // const oldBounds = this.getRotatedBounds(shape) + // const prevCenter = getBoundsCenter(oldBounds) + for (let id in handles) { const handle = handles[id] @@ -313,6 +330,27 @@ const arrow = registerShapeUtils({ shape.handles.bend.point = getBendPoint(shape) + // const newBounds = this.getRotatedBounds(shape) + // const newCenter = getBoundsCenter(newBounds) + + // shape.point = vec.add(shape.point, vec.neg(vec.sub(newCenter, prevCenter))) + + return this + }, + + onSessionComplete(shape) { + const bounds = this.getBounds(shape) + + const offset = vec.sub([bounds.minX, bounds.minY], shape.point) + + this.translateTo(shape, vec.add(shape.point, offset)) + + const { start, end, bend } = shape.handles + + start.point = vec.sub(start.point, offset) + end.point = vec.sub(end.point, offset) + bend.point = vec.sub(bend.point, offset) + return this }, @@ -360,3 +398,44 @@ function getBendPoint(shape: ArrowShape) { ? midPoint : vec.add(midPoint, vec.mul(vec.per(u), bendDist)) } + +function getResizeOffset(a: Bounds, b: Bounds) { + const { minX: x0, minY: y0, width: w0, height: h0 } = a + const { minX: x1, minY: y1, width: w1, height: h1 } = b + + let delta: number[] + + if (h0 === h1 && w0 !== w1) { + if (x0 !== x1) { + // moving left edge, pin right edge + delta = vec.sub([x1, y1 + h1 / 2], [x0, y0 + h0 / 2]) + } else { + // moving right edge, pin left edge + delta = vec.sub([x1 + w1, y1 + h1 / 2], [x0 + w0, y0 + h0 / 2]) + } + } else if (h0 !== h1 && w0 === w1) { + if (y0 !== y1) { + // moving top edge, pin bottom edge + delta = vec.sub([x1 + w1 / 2, y1], [x0 + w0 / 2, y0]) + } else { + // moving bottom edge, pin top edge + delta = vec.sub([x1 + w1 / 2, y1 + h1], [x0 + w0 / 2, y0 + h0]) + } + } else if (x0 !== x1) { + if (y0 !== y1) { + // moving top left, pin bottom right + delta = vec.sub([x1, y1], [x0, y0]) + } else { + // moving bottom left, pin top right + delta = vec.sub([x1, y1 + h1], [x0, y0 + h0]) + } + } else if (y0 !== y1) { + // moving top right, pin bottom left + delta = vec.sub([x1 + w1, y1], [x0 + w0, y0]) + } else { + // moving bottom right, pin top left + delta = vec.sub([x1 + w1, y1 + h1], [x0 + w0, y0 + h0]) + } + + return delta +} diff --git a/lib/shape-utils/index.tsx b/lib/shape-utils/index.tsx index 113ddf9eb..f9eb1d0b9 100644 --- a/lib/shape-utils/index.tsx +++ b/lib/shape-utils/index.tsx @@ -147,6 +147,9 @@ export interface ShapeUtility { handle: Partial ): ShapeUtility + // Clean up changes when a session ends. + onSessionComplete(this: ShapeUtility, shape: Mutable): ShapeUtility + // Render a shape to JSX. render(this: ShapeUtility, shape: K): JSX.Element @@ -258,6 +261,10 @@ function getDefaultShapeUtil(): ShapeUtility { return this }, + onSessionComplete() { + return this + }, + getBounds(shape) { const [x, y] = shape.point return { diff --git a/state/commands/handle.ts b/state/commands/handle.ts index 087b4c22e..0f8d29819 100644 --- a/state/commands/handle.ts +++ b/state/commands/handle.ts @@ -24,26 +24,27 @@ export default function handleCommand( const page = getPage(data, currentPageId) const shape = page.shapes[initialShape.id] - getShapeUtils(shape).onHandleChange(shape, initialShape.handles) + getShapeUtils(shape) + .onHandleChange(shape, initialShape.handles) + .onSessionComplete(shape) - const bounds = getShapeUtils(shape).getBounds(shape) + // const bounds = getShapeUtils(shape).getBounds(shape) - const offset = vec.sub([bounds.minX, bounds.minY], shape.point) + // const offset = vec.sub([bounds.minX, bounds.minY], shape.point) - getShapeUtils(shape).translateTo(shape, vec.add(shape.point, offset)) + // getShapeUtils(shape).translateTo(shape, vec.add(shape.point, offset)) - const { start, end, bend } = page.shapes[initialShape.id].handles + // const { start, end, bend } = page.shapes[initialShape.id].handles - start.point = vec.sub(start.point, offset) - end.point = vec.sub(end.point, offset) - bend.point = vec.sub(bend.point, offset) + // start.point = vec.sub(start.point, offset) + // end.point = vec.sub(end.point, offset) + // bend.point = vec.sub(bend.point, offset) }, undo(data) { const { initialShape, currentPageId } = before - const shape = getPage(data, currentPageId).shapes[initialShape.id] - - getShapeUtils(shape).onHandleChange(shape, initialShape.handles) + const page = getPage(data, currentPageId) + page.shapes[initialShape.id] = initialShape }, }) ) diff --git a/state/commands/rotate.ts b/state/commands/rotate.ts index 916b9645d..823a6c025 100644 --- a/state/commands/rotate.ts +++ b/state/commands/rotate.ts @@ -20,10 +20,10 @@ export default function rotateCommand( for (let { id, point, rotation } of after.initialShapes) { const shape = shapes[id] - const utils = getShapeUtils(shape) - utils - .rotateTo(shape, rotation, rotation - shape.rotation) + getShapeUtils(shape) + .rotateBy(shape, rotation - shape.rotation) .translateTo(shape, point) + .onSessionComplete(shape) } data.boundsRotation = after.boundsRotation @@ -33,10 +33,10 @@ export default function rotateCommand( for (let { id, point, rotation } of before.initialShapes) { const shape = shapes[id] - const utils = getShapeUtils(shape) - utils - .rotateTo(shape, rotation, rotation - shape.rotation) + getShapeUtils(shape) + .rotateBy(shape, rotation - shape.rotation) .translateTo(shape, point) + .onSessionComplete(shape) } data.boundsRotation = before.boundsRotation diff --git a/state/data.ts b/state/data.ts index 299abf710..9d3702c80 100644 --- a/state/data.ts +++ b/state/data.ts @@ -115,31 +115,31 @@ export const defaultDocument: Data['document'] = { // }, // }), // Groups Testing - shapeA: shapeUtils[ShapeType.Rectangle].create({ - id: 'shapeA', - name: 'Shape A', - childIndex: 1, - point: [0, 0], - size: [200, 200], - parentId: 'groupA', - }), - shapeB: shapeUtils[ShapeType.Rectangle].create({ - id: 'shapeB', - name: 'Shape B', - childIndex: 2, - point: [220, 100], - size: [200, 200], - parentId: 'groupA', - }), - groupA: shapeUtils[ShapeType.Group].create({ - id: 'groupA', - name: 'Group A', - childIndex: 2, - point: [0, 0], - size: [420, 300], - parentId: 'page1', - children: ['shapeA', 'shapeB'], - }), + // shapeA: shapeUtils[ShapeType.Rectangle].create({ + // id: 'shapeA', + // name: 'Shape A', + // childIndex: 1, + // point: [0, 0], + // size: [200, 200], + // parentId: 'groupA', + // }), + // shapeB: shapeUtils[ShapeType.Rectangle].create({ + // id: 'shapeB', + // name: 'Shape B', + // childIndex: 2, + // point: [220, 100], + // size: [200, 200], + // parentId: 'groupA', + // }), + // groupA: shapeUtils[ShapeType.Group].create({ + // id: 'groupA', + // name: 'Group A', + // childIndex: 2, + // point: [0, 0], + // size: [420, 300], + // parentId: 'page1', + // children: ['shapeA', 'shapeB'], + // }), }, }, page2: { diff --git a/state/sessions/handle-session.ts b/state/sessions/handle-session.ts index 5d72e4036..175dad7a8 100644 --- a/state/sessions/handle-session.ts +++ b/state/sessions/handle-session.ts @@ -33,17 +33,21 @@ export default class HandleSession extends BaseSession { 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), + point: vec.add(handles[handleId].point, delta), // vec.rot(delta, shape.rotation)), }, }) } cancel(data: Data) { const { currentPageId, handleId, initialShape } = this.snapshot - const shape = getPage(data, currentPageId).shapes[initialShape.id] + getPage(data, currentPageId).shapes[initialShape.id] = initialShape } complete(data: Data) { diff --git a/state/state.ts b/state/state.ts index dea05f737..8be97ebe2 100644 --- a/state/state.ts +++ b/state/state.ts @@ -925,7 +925,7 @@ const state = createState({ payload: PointerInfo & { target: Corner | Edge } ) { const point = screenToWorld(inputs.pointer.origin, data) - // session = new Sessions.TransformSession(data, payload.target, point) + session = new Sessions.TransformSession(data, payload.target, point) session = data.selectedIds.size === 1 ? new Sessions.TransformSingleSession(data, payload.target, point) @@ -1442,9 +1442,16 @@ const state = createState({ return bounds } + const uniqueSelectedShapeIds: string[] = Array.from( + new Set( + Array.from(selectedIds.values()).flatMap((id) => + getDocumentBranch(data, id) + ) + ).values() + ) + const commonBounds = getCommonBounds( - ...shapes - .flatMap((shape) => getDocumentBranch(data, shape.id)) + ...uniqueSelectedShapeIds .map((id) => page.shapes[id]) .filter((shape) => shape.type !== ShapeType.Group) .map((shape) => { diff --git a/todo.md b/todo.md index b72331b2a..654cf518e 100644 --- a/todo.md +++ b/todo.md @@ -1,17 +1,21 @@ # Todo -## Done +## Groups -- fix select indicator placement for arrow +- fix drift when moving children of rotated group -## Todo +## Select - Restore select highlight, fix for children of rotated groups -- Transforming on rotated shapes -- Fix bounding box for rotated shapes -- Allow single-selected groups to transform their children correctly + +# Transforms + - (merge transform-session and transform-single-session) -- fix drift when moving children of rotated group -- shift dragging arrow handles should lock to directions -- arrow rotation with handles +- Allow single-selected groups to transform their children correctly - fix ellipse when scaleX < 0 or scaleY < 0 +- Transforming on rotated shapes + +## Arrows + +- shift dragging arrow handles should lock to directions +- fix undo/redo on rotated arrows