improves arrow rotation

This commit is contained in:
Steve Ruiz 2021-06-05 15:29:49 +01:00
parent 554d55a9e3
commit 81141e7bb5
9 changed files with 179 additions and 86 deletions

View file

@ -30,7 +30,6 @@ export default function Handles() {
{Object.values(shape.handles).map((handle) => (
<Handle
key={handle.id}
shapeId={shape.id}
id={handle.id}
point={vec.add(handle.point, shape.point)}
/>
@ -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<SVGGElement>(null)
const events = useHandleEvents(id, rGroup)

View file

@ -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<ArrowShape>({
)
},
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<ArrowShape>({
},
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<ArrowShape>({
},
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<ArrowShape>({
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
}

View file

@ -147,6 +147,9 @@ export interface ShapeUtility<K extends Shape> {
handle: Partial<K['handles']>
): ShapeUtility<K>
// Clean up changes when a session ends.
onSessionComplete(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
// Render a shape to JSX.
render(this: ShapeUtility<K>, shape: K): JSX.Element
@ -258,6 +261,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
return this
},
onSessionComplete() {
return this
},
getBounds(shape) {
const [x, y] = shape.point
return {

View file

@ -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
},
})
)

View file

@ -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

View file

@ -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: {

View file

@ -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) {

View file

@ -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) => {

22
todo.md
View file

@ -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