Improves transforms
This commit is contained in:
parent
da8f812090
commit
c3740cacdd
29 changed files with 700 additions and 72 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -123,6 +123,10 @@ const polyline = createShape<PolylineShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds, info) {
|
||||
return this.transform(shape, bounds, info)
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
72
state/commands/transform-single.ts
Normal file
72
state/commands/transform-single.ts
Normal 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,
|
||||
})
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
// },
|
||||
// }),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
247
state/sessions/transform-single-session.ts
Normal file
247
state/sessions/transform-single-session.ts
Normal 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
|
||||
>
|
|
@ -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(
|
||||
|
|
152
utils/utils.ts
152
utils/utils.ts
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue