Improves transforms

This commit is contained in:
Steve Ruiz 2021-05-19 10:35:00 +01:00
parent da8f812090
commit c3740cacdd
29 changed files with 700 additions and 72 deletions

View file

@ -6,7 +6,7 @@ import styled from "styles"
export default function BoundsBg() {
const rBounds = useRef<SVGRectElement>(null)
const bounds = useSelector((state) => state.values.selectedBounds)
const isSelecting = useSelector((s) => s.isIn("selecting"))
const rotation = useSelector((s) => {
if (s.data.selectedIds.size === 1) {
const { shapes } = s.data.document.pages[s.data.currentPageId]
@ -18,6 +18,7 @@ export default function BoundsBg() {
})
if (!bounds) return null
if (!isSelecting) return null
const { minX, minY, width, height } = bounds

View file

@ -7,9 +7,9 @@ import { lerp } from "utils/utils"
export default function Bounds() {
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
const isSelecting = useSelector((s) => s.isIn("selecting"))
const zoom = useSelector((s) => s.data.camera.zoom)
const bounds = useSelector((s) => s.values.selectedBounds)
const rotation = useSelector((s) => {
if (s.data.selectedIds.size === 1) {
const { shapes } = s.data.document.pages[s.data.currentPageId]
@ -21,6 +21,7 @@ export default function Bounds() {
})
if (!bounds) return null
if (!isSelecting) return null
let { minX, minY, maxX, maxY, width, height } = bounds

View file

@ -18,7 +18,7 @@ export default class Circle extends CodeShape<CircleShape> {
rotation: 0,
radius: 20,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
strokeWidth: 1,
},

View file

@ -17,7 +17,7 @@ export default class Dot extends CodeShape<DotShape> {
point: [0, 0],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
strokeWidth: 1,
},

View file

@ -19,7 +19,7 @@ export default class Ellipse extends CodeShape<EllipseShape> {
radiusY: 20,
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
strokeWidth: 1,
},

View file

@ -19,7 +19,7 @@ export default class Line extends CodeShape<LineShape> {
direction: [-0.5, 0.5],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
strokeWidth: 1,
},

View file

@ -19,7 +19,7 @@ export default class Ray extends CodeShape<RayShape> {
direction: [0, 1],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
strokeWidth: 1,
},

View file

@ -19,7 +19,7 @@ export default class Rectangle extends CodeShape<RectangleShape> {
size: [100, 100],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
strokeWidth: 1,
},

View file

@ -22,7 +22,7 @@ const circle = createShape<CircleShape>({
rotation: 0,
radius: 20,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
},
...props,
@ -157,6 +157,10 @@ const circle = createShape<CircleShape>({
return shape
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
canTransform: true,
})

View file

@ -22,7 +22,7 @@ const dot = createShape<DotShape>({
point: [0, 0],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
},
...props,
@ -89,6 +89,10 @@ const dot = createShape<DotShape>({
return shape
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
canTransform: false,
})

View file

@ -5,7 +5,12 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectEllipseBounds } from "utils/intersections"
import { pointInEllipse } from "utils/hitTests"
import { translateBounds } from "utils/utils"
import {
getBoundsFromPoints,
getRotatedCorners,
rotateBounds,
translateBounds,
} from "utils/utils"
const ellipse = createShape<EllipseShape>({
boundsCache: new WeakMap([]),
@ -23,7 +28,7 @@ const ellipse = createShape<EllipseShape>({
radiusY: 20,
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
},
...props,
@ -56,7 +61,7 @@ const ellipse = createShape<EllipseShape>({
},
getRotatedBounds(shape) {
return this.getBounds(shape)
return getBoundsFromPoints(getRotatedCorners(shape))
},
getCenter(shape) {
@ -68,7 +73,8 @@ const ellipse = createShape<EllipseShape>({
point,
vec.add(shape.point, [shape.radiusX, shape.radiusY]),
shape.radiusX,
shape.radiusY
shape.radiusY,
shape.rotation
)
},
@ -83,7 +89,8 @@ const ellipse = createShape<EllipseShape>({
vec.add(shape.point, [shape.radiusX, shape.radiusY]),
shape.radiusX,
shape.radiusY,
brushBounds
brushBounds,
shape.rotation
).length > 0
)
},
@ -109,6 +116,10 @@ const ellipse = createShape<EllipseShape>({
return shape
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
canTransform: true,
})

View file

@ -72,6 +72,23 @@ export interface ShapeUtility<K extends Shape> {
}
): K
transformSingle(
this: ShapeUtility<K>,
shape: K,
bounds: Bounds,
info: {
type: TransformEdge | TransformCorner
boundsRotation: number
initialShape: K
initialShapeBounds: BoundsSnapshot
initialBounds: Bounds
isFlippedX: boolean
isFlippedY: boolean
isSingle: boolean
anchor: TransformEdge | TransformCorner
}
): K
// Apply a scale to a shape.
scale(this: ShapeUtility<K>, shape: K, scale: number): K

View file

@ -22,7 +22,7 @@ const line = createShape<LineShape>({
direction: [0, 0],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
},
...props,
@ -97,6 +97,10 @@ const line = createShape<LineShape>({
return shape
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
canTransform: false,
})

View file

@ -123,6 +123,10 @@ const polyline = createShape<PolylineShape>({
return shape
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
canTransform: true,
})

View file

@ -22,7 +22,7 @@ const ray = createShape<RayShape>({
direction: [0, 1],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
strokeWidth: 1,
},

View file

@ -1,9 +1,19 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { RectangleShape, ShapeType } from "types"
import {
RectangleShape,
ShapeType,
TransformCorner,
TransformEdge,
} from "types"
import { createShape } from "./index"
import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
import { getBoundsFromPoints, rotateBounds, translateBounds } from "utils/utils"
import {
getBoundsFromPoints,
getRotatedCorners,
rotateBounds,
translateBounds,
} from "utils/utils"
const rectangle = createShape<RectangleShape>({
boundsCache: new WeakMap([]),
@ -20,7 +30,7 @@ const rectangle = createShape<RectangleShape>({
size: [1, 1],
rotation: 0,
style: {
fill: "rgba(142, 143, 142, 1.000)",
fill: "#c6cacb",
stroke: "#000",
},
...props,
@ -50,18 +60,9 @@ const rectangle = createShape<RectangleShape>({
},
getRotatedBounds(shape) {
const b = this.getBounds(shape)
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
// Rotate corners of the shape, then find the minimum among those points.
const rotatedCorners = [
[b.minX, b.minY],
[b.maxX, b.minY],
[b.maxX, b.maxY],
[b.minX, b.maxY],
].map((point) => vec.rotWith(point, center, shape.rotation))
return getBoundsFromPoints(rotatedCorners)
return getBoundsFromPoints(
getRotatedCorners(this.getBounds(shape), shape.rotation)
)
},
getCenter(shape) {
@ -74,15 +75,10 @@ const rectangle = createShape<RectangleShape>({
},
hitTestBounds(shape, brushBounds) {
const b = this.getBounds(shape)
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
const rotatedCorners = [
[b.minX, b.minY],
[b.maxX, b.minY],
[b.maxX, b.maxY],
[b.minX, b.maxY],
].map((point) => vec.rotWith(point, center, shape.rotation))
const rotatedCorners = getRotatedCorners(
this.getBounds(shape),
shape.rotation
)
return (
boundsContainPolygon(brushBounds, rotatedCorners) ||
@ -108,8 +104,6 @@ const rectangle = createShape<RectangleShape>({
shapeBounds,
{ initialShape, isSingle, initialShapeBounds, isFlippedX, isFlippedY }
) {
// TODO: Apply rotation to single-selection items
if (shape.rotation === 0 || isSingle) {
shape.size = [shapeBounds.width, shapeBounds.height]
shape.point = [shapeBounds.minX, shapeBounds.minY]
@ -145,6 +139,105 @@ const rectangle = createShape<RectangleShape>({
return shape
},
transformSingle(
shape,
bounds,
{ initialShape, initialShapeBounds, anchor, isFlippedY, isFlippedX }
) {
shape.size = [bounds.width, bounds.height]
shape.point = [bounds.minX, bounds.minY]
// const prevCorners = getRotatedCorners(
// initialShapeBounds,
// initialShape.rotation
// )
// let currCorners = getRotatedCorners(this.getBounds(shape), shape.rotation)
// if (isFlippedX) {
// let t = currCorners[3]
// currCorners[3] = currCorners[2]
// currCorners[2] = t
// t = currCorners[0]
// currCorners[0] = currCorners[1]
// currCorners[1] = t
// }
// if (isFlippedY) {
// let t = currCorners[3]
// currCorners[3] = currCorners[0]
// currCorners[0] = t
// t = currCorners[2]
// currCorners[2] = currCorners[1]
// currCorners[1] = t
// }
// switch (anchor) {
// case TransformCorner.TopLeft: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[2], prevCorners[2])
// )
// break
// }
// case TransformCorner.TopRight: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[3], prevCorners[3])
// )
// break
// }
// case TransformCorner.BottomRight: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[0], prevCorners[0])
// )
// break
// }
// case TransformCorner.BottomLeft: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[1], prevCorners[1])
// )
// break
// }
// case TransformEdge.Top: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[3], prevCorners[3])
// )
// break
// }
// case TransformEdge.Right: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[3], prevCorners[3])
// )
// break
// }
// case TransformEdge.Bottom: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[0], prevCorners[0])
// )
// break
// }
// case TransformEdge.Left: {
// shape.point = vec.sub(
// shape.point,
// vec.sub(currCorners[2], prevCorners[2])
// )
// break
// }
// }
// console.log(shape.point, shape.size)
return shape
},
canTransform: true,
})

View file

@ -2,7 +2,7 @@ import Command from "./command"
import history from "../history"
import { Data, Shape } from "types"
export default function createShape(data: Data, shape: Shape) {
export default function createShapeCommand(data: Data, shape: Shape) {
const { currentPageId } = data
history.execute(

View file

@ -3,7 +3,7 @@ import history from "../history"
import { DirectionSnapshot } from "state/sessions/direction-session"
import { Data, LineShape, RayShape } from "types"
export default function translateCommand(
export default function directCommand(
data: Data,
before: DirectionSnapshot,
after: DirectionSnapshot

View file

@ -3,7 +3,7 @@ import history from "../history"
import { CodeControl, Data, Shape } from "types"
import { current } from "immer"
export default function setGeneratedShapes(
export default function generateCommand(
data: Data,
currentPageId: string,
generatedShapes: Shape[]

View file

@ -1,5 +1,6 @@
import translate from "./translate"
import transform from "./transform"
import transformSingle from "./transform-single"
import generate from "./generate"
import createShape from "./create-shape"
import direct from "./direct"
@ -8,6 +9,7 @@ import rotate from "./rotate"
const commands = {
translate,
transform,
transformSingle,
generate,
createShape,
direct,

View file

@ -3,7 +3,7 @@ import history from "../history"
import { Data } from "types"
import { RotateSnapshot } from "state/sessions/rotate-session"
export default function translateCommand(
export default function rotateCommand(
data: Data,
before: RotateSnapshot,
after: RotateSnapshot

View file

@ -0,0 +1,72 @@
import Command from "./command"
import history from "../history"
import { Data, TransformCorner, TransformEdge } from "types"
import { getShapeUtils } from "lib/shapes"
import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
export default function transformSingleCommand(
data: Data,
before: TransformSingleSnapshot,
after: TransformSingleSnapshot,
anchor: TransformCorner | TransformEdge
) {
history.execute(
data,
new Command({
name: "translate_shapes",
category: "canvas",
do(data) {
const {
type,
initialShape,
initialShapeBounds,
currentPageId,
id,
boundsRotation,
} = after
const { shapes } = data.document.pages[currentPageId]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape,
initialShapeBounds,
initialBounds: initialShapeBounds,
boundsRotation,
isFlippedX: false,
isFlippedY: false,
isSingle: false,
anchor,
})
},
undo(data) {
const {
type,
initialShape,
initialShapeBounds,
currentPageId,
id,
boundsRotation,
} = before
const { shapes } = data.document.pages[currentPageId]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape,
initialShapeBounds,
initialBounds: initialShapeBounds,
boundsRotation,
isFlippedX: false,
isFlippedY: false,
isSingle: false,
anchor,
})
},
})
)
}

View file

@ -4,7 +4,7 @@ import { Data, TransformCorner, TransformEdge } from "types"
import { TransformSnapshot } from "state/sessions/transform-session"
import { getShapeUtils } from "lib/shapes"
export default function translateCommand(
export default function transformCommand(
data: Data,
before: TransformSnapshot,
after: TransformSnapshot,
@ -22,7 +22,6 @@ export default function translateCommand(
initialBounds,
currentPageId,
selectedIds,
isSingle,
boundsRotation,
} = after
@ -40,7 +39,7 @@ export default function translateCommand(
boundsRotation,
isFlippedX: false,
isFlippedY: false,
isSingle,
isSingle: false,
anchor,
})
})
@ -52,7 +51,6 @@ export default function translateCommand(
initialBounds,
currentPageId,
selectedIds,
isSingle,
boundsRotation,
} = before
@ -70,7 +68,7 @@ export default function translateCommand(
boundsRotation,
isFlippedX: false,
isFlippedY: false,
isSingle,
isSingle: false,
anchor: type,
})
})

View file

@ -17,7 +17,7 @@ export const defaultDocument: Data["document"] = {
direction: [0.5, 0.5],
style: {
fill: "#AAA",
stroke: "rgba(142, 143, 142, 1.000)",
stroke: "#c6cacb",
strokeWidth: 1,
},
}),
@ -28,7 +28,7 @@ export const defaultDocument: Data["document"] = {
// point: [400, 500],
// style: {
// fill: "#AAA",
// stroke: "rgba(142, 143, 142, 1.000)",
// stroke: "#c6cacb",
// strokeWidth: 1,
// },
// }),
@ -40,7 +40,7 @@ export const defaultDocument: Data["document"] = {
radius: 50,
style: {
fill: "#AAA",
stroke: "rgba(142, 143, 142, 1.000)",
stroke: "#c6cacb",
strokeWidth: 1,
},
}),
@ -53,7 +53,7 @@ export const defaultDocument: Data["document"] = {
radiusY: 30,
style: {
fill: "#AAA",
stroke: "rgba(142, 143, 142, 1.000)",
stroke: "#c6cacb",
strokeWidth: 1,
},
}),
@ -66,7 +66,7 @@ export const defaultDocument: Data["document"] = {
// radiusY: 30,
// style: {
// fill: "#AAA",
// stroke: "rgba(142, 143, 142, 1.000)",
// stroke: "#c6cacb",
// strokeWidth: 1,
// },
// }),
@ -82,7 +82,7 @@ export const defaultDocument: Data["document"] = {
// ],
// style: {
// fill: "none",
// stroke: "rgba(142, 143, 142, 1.000)",
// stroke: "#c6cacb",
// strokeWidth: 2,
// strokeLinecap: "round",
// strokeLinejoin: "round",
@ -96,7 +96,7 @@ export const defaultDocument: Data["document"] = {
size: [200, 200],
style: {
fill: "#AAA",
stroke: "rgba(142, 143, 142, 1.000)",
stroke: "#c6cacb",
strokeWidth: 1,
},
}),
@ -108,7 +108,7 @@ export const defaultDocument: Data["document"] = {
// direction: [0.2, 0.2],
// style: {
// fill: "#AAA",
// stroke: "rgba(142, 143, 142, 1.000)",
// stroke: "#c6cacb",
// strokeWidth: 1,
// },
// }),

View file

@ -2,6 +2,7 @@ import BaseSession from "./base-session"
import BrushSession from "./brush-session"
import TranslateSession from "./translate-session"
import TransformSession from "./transform-session"
import TransformSingleSession from "./transform-single-session"
import DirectionSession from "./direction-session"
import RotateSession from "./rotate-session"
@ -10,6 +11,7 @@ export {
BaseSession,
TranslateSession,
TransformSession,
TransformSingleSession,
DirectionSession,
RotateSession,
}

View file

@ -66,12 +66,25 @@ export default class TransformSession extends BaseSession {
initialBounds,
currentPageId,
selectedIds,
isSingle,
} = this.snapshot
const { shapes } = data.document.pages[currentPageId]
const delta = vec.vec(this.origin, point)
let delta = vec.vec(this.origin, point)
// if (isSingle) {
// const center = [
// initialBounds.minX + initialBounds.width / 2,
// initialBounds.minY + initialBounds.height / 2,
// ]
// const rotation = shapes[Array.from(selectedIds.values())[0]].rotation
// const rotatedOrigin = vec.rotWith(this.origin, center, -rotation)
// const rotatedPoint = vec.rotWith(point, center, -rotation)
// delta = vec.vec(rotatedOrigin, rotatedPoint)
// }
/*
Transforms
@ -173,7 +186,7 @@ export default class TransformSession extends BaseSession {
boundsRotation,
isFlippedX: this.isFlippedX,
isFlippedY: this.isFlippedY,
isSingle,
isSingle: false,
anchor: getTransformAnchor(
this.transformType,
this.isFlippedX,
@ -190,7 +203,6 @@ export default class TransformSession extends BaseSession {
initialBounds,
currentPageId,
selectedIds,
isSingle,
} = this.snapshot
const { shapes } = data.document.pages[currentPageId]
@ -208,7 +220,7 @@ export default class TransformSession extends BaseSession {
boundsRotation,
isFlippedX: false,
isFlippedY: false,
isSingle,
isSingle: false,
anchor: getTransformAnchor(this.transformType, false, false),
})
})
@ -255,7 +267,6 @@ export function getTransformSnapshot(
type: transformType,
initialBounds: bounds,
boundsRotation,
isSingle: selectedIds.size === 1,
selectedIds: new Set(selectedIds),
shapeBounds: Object.fromEntries(
Array.from(selectedIds.values()).map((id) => {

View file

@ -0,0 +1,247 @@
import { Data, TransformEdge, TransformCorner } from "types"
import * as vec from "utils/vec"
import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
import { getShapeUtils } from "lib/shapes"
import {
getTransformedBoundingBox,
getCommonBounds,
getRotatedCorners,
getTransformAnchor,
} from "utils/utils"
export default class TransformSingleSession extends BaseSession {
delta = [0, 0]
isFlippedX = false
isFlippedY = false
transformType: TransformEdge | TransformCorner
origin: number[]
center: number[]
snapshot: TransformSingleSnapshot
corners: {
a: number[]
b: number[]
}
rotatedCorners: number[][]
constructor(
data: Data,
transformType: TransformCorner | TransformEdge,
point: number[]
) {
super(data)
this.origin = point
this.transformType = transformType
this.snapshot = getTransformSingleSnapshot(data, transformType)
const { minX, minY, maxX, maxY } = this.snapshot.initialShapeBounds
this.center = [(minX + maxX) / 2, (minY + maxY) / 2]
this.corners = {
a: [minX, minY],
b: [maxX, maxY],
}
this.rotatedCorners = getRotatedCorners(
this.snapshot.initialShapeBounds,
this.snapshot.initialShape.rotation
)
}
update(data: Data, point: number[]) {
const {
corners: { a, b },
transformType,
} = this
const {
boundsRotation,
initialShapeBounds,
currentPageId,
initialShape,
id,
} = this.snapshot
const { shapes } = data.document.pages[currentPageId]
const shape = shapes[id]
const rotation = shape.rotation
// 1. Create a new bounding box.
// Counter rotate the delta and apply this to the original bounding box.
const delta = vec.vec(this.origin, point)
/*
Transforms
Corners a and b are the original top-left and bottom-right corners of the
bounding box. Depending on what the user is dragging, change one or both
points. To keep things smooth, calculate based by adding the delta (the
vector between the current point and its original point) to the original
bounding box values.
*/
const newBoundingBox = getTransformedBoundingBox(
initialShapeBounds,
transformType,
delta,
shape.rotation
)
// console.log(newBoundingBox)
switch (transformType) {
case TransformEdge.Top: {
a[1] = initialShapeBounds.minY + delta[1]
break
}
case TransformEdge.Right: {
b[0] = initialShapeBounds.maxX + delta[0]
break
}
case TransformEdge.Bottom: {
b[1] = initialShapeBounds.maxY + delta[1]
break
}
case TransformEdge.Left: {
a[0] = initialShapeBounds.minX + delta[0]
break
}
case TransformCorner.TopLeft: {
a[0] = initialShapeBounds.minX + delta[0]
a[1] = initialShapeBounds.minY + delta[1]
break
}
case TransformCorner.TopRight: {
a[1] = initialShapeBounds.minY + delta[1]
b[0] = initialShapeBounds.maxX + delta[0]
break
}
case TransformCorner.BottomRight: {
b[0] = initialShapeBounds.maxX + delta[0]
b[1] = initialShapeBounds.maxY + delta[1]
break
}
case TransformCorner.BottomLeft: {
a[0] = initialShapeBounds.minX + delta[0]
b[1] = initialShapeBounds.maxY + delta[1]
break
}
}
// Calculate new common (externior) bounding box
const newBounds = {
minX: Math.min(a[0], b[0]),
minY: Math.min(a[1], b[1]),
maxX: Math.max(a[0], b[0]),
maxY: Math.max(a[1], b[1]),
width: Math.abs(b[0] - a[0]),
height: Math.abs(b[1] - a[1]),
}
this.isFlippedX = b[0] < a[0]
this.isFlippedY = b[1] < a[1]
const anchor = this.transformType
// Pass the new data to the shape's transform utility for mutation.
// Most shapes should be able to transform using only the bounding box,
// however some shapes (e.g. those with internal points) will need more
// data here too.
getShapeUtils(shape).transformSingle(shape, newBoundingBox, {
type: this.transformType,
initialShape,
initialShapeBounds,
initialBounds: initialShapeBounds,
boundsRotation,
isFlippedX: this.isFlippedX,
isFlippedY: this.isFlippedY,
isSingle: true,
anchor,
})
}
cancel(data: Data) {
const {
id,
boundsRotation,
initialShape,
initialShapeBounds,
currentPageId,
isSingle,
} = this.snapshot
const { shapes } = data.document.pages[currentPageId]
// selectedIds.forEach((id) => {
// const shape = shapes[id]
// const { initialShape, initialShapeBounds } = shapeBounds[id]
// getShapeUtils(shape).transform(shape, initialShapeBounds, {
// type: this.transformType,
// initialShape,
// initialShapeBounds,
// initialBounds,
// boundsRotation,
// isFlippedX: false,
// isFlippedY: false,
// isSingle,
// anchor: getTransformAnchor(this.transformType, false, false),
// })
// })
}
complete(data: Data) {
commands.transformSingle(
data,
this.snapshot,
getTransformSingleSnapshot(data, this.transformType),
getTransformAnchor(this.transformType, false, false)
)
}
}
export function getTransformSingleSnapshot(
data: Data,
transformType: TransformEdge | TransformCorner
) {
const {
document: { pages },
selectedIds,
currentPageId,
} = current(data)
const pageShapes = pages[currentPageId].shapes
const id = Array.from(selectedIds)[0]
const shape = pageShapes[id]
const bounds = getShapeUtils(shape).getBounds(shape)
return {
id,
currentPageId,
type: transformType,
initialShape: shape,
initialShapeBounds: {
...bounds,
nx: 0,
ny: 0,
nmx: 1,
nmy: 1,
nw: 1,
nh: 1,
},
boundsRotation: shape.rotation,
isSingle: true,
}
}
export type TransformSingleSnapshot = ReturnType<
typeof getTransformSingleSnapshot
>

View file

@ -527,11 +527,18 @@ const state = createState({
data,
payload: PointerInfo & { target: TransformCorner | TransformEdge }
) {
session = new Sessions.TransformSession(
data,
payload.target,
screenToWorld(payload.point, data)
)
session =
data.selectedIds.size === 1
? new Sessions.TransformSingleSession(
data,
payload.target,
screenToWorld(payload.point, data)
)
: new Sessions.TransformSession(
data,
payload.target,
screenToWorld(payload.point, data)
)
},
startDrawTransformSession(data, payload: PointerInfo) {
session = new Sessions.TransformSession(

View file

@ -1,6 +1,7 @@
import Vector from "lib/code/vector"
import { getShapeUtils } from "lib/shapes"
import React from "react"
import { Data, Bounds, TransformEdge, TransformCorner } from "types"
import { Data, Bounds, TransformEdge, TransformCorner, Shape } from "types"
import * as svg from "./svg"
import * as vec from "./vec"
@ -1020,3 +1021,152 @@ export function rotateBounds(
height: bounds.height,
}
}
export function getRotatedCorners(b: Bounds, rotation: number) {
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
return [
[b.minX, b.minY],
[b.maxX, b.minY],
[b.maxX, b.maxY],
[b.minX, b.maxY],
].map((point) => vec.rotWith(point, center, rotation))
}
export function getTransformedBoundingBox(
bounds: Bounds,
handle: TransformCorner | TransformEdge,
delta: number[],
rotation = 0
) {
// Create top left and bottom right corners.
let [ax0, ay0] = [bounds.minX, bounds.minY]
let [ax1, ay1] = [bounds.maxX, bounds.maxY]
// Create a second set of corners for the result.
let [bx0, by0] = [bounds.minX, bounds.minY]
let [bx1, by1] = [bounds.maxX, bounds.maxY]
// Counter rotate the delta. This lets us make changes as if
// the (possibly rotated) boxes were axis aligned.
const [dx, dy] = vec.rot(delta, -rotation)
// Depending on the dragging handle (an edge or corner of
// the bounding box), find the anchor corner and use the delta
// to adjust the result's corners.
let anchor: TransformCorner | TransformEdge
switch (handle) {
case TransformEdge.Top: {
anchor = TransformCorner.BottomRight
by0 += dy
break
}
case TransformEdge.Right: {
anchor = TransformCorner.TopLeft
bx1 += dx
break
}
case TransformEdge.Bottom: {
anchor = TransformCorner.TopLeft
by1 += dy
break
}
case TransformEdge.Left: {
anchor = TransformCorner.BottomRight
bx0 += dx
break
}
case TransformCorner.TopLeft: {
anchor = TransformCorner.BottomRight
bx0 += dx
by0 += dy
break
}
case TransformCorner.TopRight: {
anchor = TransformCorner.BottomLeft
bx1 += dx
by0 += dy
break
}
case TransformCorner.BottomRight: {
anchor = TransformCorner.TopLeft
bx1 += dx
by1 += dy
break
}
case TransformCorner.BottomLeft: {
anchor = TransformCorner.TopRight
bx0 += dx
by1 += dy
break
}
}
// If the bounds are rotated, get a vector from the rotated anchor
// corner in the inital bounds to the rotated anchor corner in the
// result's bounds. Subtract this vector from the result's corners,
// so that the two anchor points (initial and result) will be equal.
if (rotation % (Math.PI * 2) !== 0) {
let cv = [0, 0]
const c0 = vec.med([ax0, ay0], [ax1, ay1])
const c1 = vec.med([bx0, by0], [bx1, by1])
switch (anchor) {
case TransformCorner.TopLeft: {
cv = vec.sub(
vec.rotWith([bx0, by0], c1, rotation),
vec.rotWith([ax0, ay0], c0, rotation)
)
break
}
case TransformCorner.TopRight: {
cv = vec.sub(
vec.rotWith([bx1, by0], c1, rotation),
vec.rotWith([ax1, ay0], c0, rotation)
)
break
}
case TransformCorner.BottomRight: {
cv = vec.sub(
vec.rotWith([bx1, by1], c1, rotation),
vec.rotWith([ax1, ay1], c0, rotation)
)
break
}
case TransformCorner.BottomLeft: {
cv = vec.sub(
vec.rotWith([bx0, by1], c1, rotation),
vec.rotWith([ax0, ay1], c0, rotation)
)
break
}
}
;[bx0, by0] = vec.sub([bx0, by0], cv)
;[bx1, by1] = vec.sub([bx1, by1], cv)
}
// If the axes are flipped (e.g. if the right edge has been dragged
// left past the initial left edge) then swap points on that axis.
if (bx1 < bx0) {
;[bx1, bx0] = [bx0, bx1]
}
if (by1 < by0) {
;[by1, by0] = [by0, by1]
}
return {
minX: bx0,
minY: by0,
maxX: bx1,
maxY: by1,
width: bx1 - bx0,
height: by1 - by0,
}
}