diff --git a/components/canvas/bounds-bg.tsx b/components/canvas/bounds-bg.tsx index d12eec3e0..e0d70cd4e 100644 --- a/components/canvas/bounds-bg.tsx +++ b/components/canvas/bounds-bg.tsx @@ -6,7 +6,7 @@ import styled from "styles" export default function BoundsBg() { const rBounds = useRef(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 diff --git a/components/canvas/bounds.tsx b/components/canvas/bounds.tsx index 7718d9817..49de78052 100644 --- a/components/canvas/bounds.tsx +++ b/components/canvas/bounds.tsx @@ -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 diff --git a/lib/code/circle.ts b/lib/code/circle.ts index e906c7547..547aad61d 100644 --- a/lib/code/circle.ts +++ b/lib/code/circle.ts @@ -18,7 +18,7 @@ export default class Circle extends CodeShape { rotation: 0, radius: 20, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", strokeWidth: 1, }, diff --git a/lib/code/dot.ts b/lib/code/dot.ts index 3bfd7464f..d241d3a2b 100644 --- a/lib/code/dot.ts +++ b/lib/code/dot.ts @@ -17,7 +17,7 @@ export default class Dot extends CodeShape { point: [0, 0], rotation: 0, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", strokeWidth: 1, }, diff --git a/lib/code/ellipse.ts b/lib/code/ellipse.ts index cd4a703da..860b6f307 100644 --- a/lib/code/ellipse.ts +++ b/lib/code/ellipse.ts @@ -19,7 +19,7 @@ export default class Ellipse extends CodeShape { radiusY: 20, rotation: 0, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", strokeWidth: 1, }, diff --git a/lib/code/line.ts b/lib/code/line.ts index 4550a57ac..db8642426 100644 --- a/lib/code/line.ts +++ b/lib/code/line.ts @@ -19,7 +19,7 @@ export default class Line extends CodeShape { direction: [-0.5, 0.5], rotation: 0, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", strokeWidth: 1, }, diff --git a/lib/code/ray.ts b/lib/code/ray.ts index ca6eed4f9..3a906a0a3 100644 --- a/lib/code/ray.ts +++ b/lib/code/ray.ts @@ -19,7 +19,7 @@ export default class Ray extends CodeShape { direction: [0, 1], rotation: 0, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", strokeWidth: 1, }, diff --git a/lib/code/rectangle.ts b/lib/code/rectangle.ts index 03b30dd4b..f2edc525d 100644 --- a/lib/code/rectangle.ts +++ b/lib/code/rectangle.ts @@ -19,7 +19,7 @@ export default class Rectangle extends CodeShape { size: [100, 100], rotation: 0, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", strokeWidth: 1, }, diff --git a/lib/shapes/circle.tsx b/lib/shapes/circle.tsx index 9d3495513..3b8406ae0 100644 --- a/lib/shapes/circle.tsx +++ b/lib/shapes/circle.tsx @@ -22,7 +22,7 @@ const circle = createShape({ rotation: 0, radius: 20, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", }, ...props, @@ -157,6 +157,10 @@ const circle = createShape({ return shape }, + transformSingle(shape, bounds, info) { + return this.transform(shape, bounds, info) + }, + canTransform: true, }) diff --git a/lib/shapes/dot.tsx b/lib/shapes/dot.tsx index b552550ce..c3223c20d 100644 --- a/lib/shapes/dot.tsx +++ b/lib/shapes/dot.tsx @@ -22,7 +22,7 @@ const dot = createShape({ 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({ return shape }, + transformSingle(shape, bounds, info) { + return this.transform(shape, bounds, info) + }, + canTransform: false, }) diff --git a/lib/shapes/ellipse.tsx b/lib/shapes/ellipse.tsx index e4fba1a0f..b7bb14ee6 100644 --- a/lib/shapes/ellipse.tsx +++ b/lib/shapes/ellipse.tsx @@ -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({ boundsCache: new WeakMap([]), @@ -23,7 +28,7 @@ const ellipse = createShape({ radiusY: 20, rotation: 0, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", }, ...props, @@ -56,7 +61,7 @@ const ellipse = createShape({ }, getRotatedBounds(shape) { - return this.getBounds(shape) + return getBoundsFromPoints(getRotatedCorners(shape)) }, getCenter(shape) { @@ -68,7 +73,8 @@ const ellipse = createShape({ point, vec.add(shape.point, [shape.radiusX, shape.radiusY]), shape.radiusX, - shape.radiusY + shape.radiusY, + shape.rotation ) }, @@ -83,7 +89,8 @@ const ellipse = createShape({ 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({ return shape }, + transformSingle(shape, bounds, info) { + return this.transform(shape, bounds, info) + }, + canTransform: true, }) diff --git a/lib/shapes/index.tsx b/lib/shapes/index.tsx index f997f0ac7..eb7cb3919 100644 --- a/lib/shapes/index.tsx +++ b/lib/shapes/index.tsx @@ -72,6 +72,23 @@ export interface ShapeUtility { } ): K + transformSingle( + this: ShapeUtility, + 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, shape: K, scale: number): K diff --git a/lib/shapes/line.tsx b/lib/shapes/line.tsx index 414f57417..da50e8916 100644 --- a/lib/shapes/line.tsx +++ b/lib/shapes/line.tsx @@ -22,7 +22,7 @@ const line = createShape({ 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({ return shape }, + transformSingle(shape, bounds, info) { + return this.transform(shape, bounds, info) + }, + canTransform: false, }) diff --git a/lib/shapes/polyline.tsx b/lib/shapes/polyline.tsx index 247c87c67..1508df448 100644 --- a/lib/shapes/polyline.tsx +++ b/lib/shapes/polyline.tsx @@ -123,6 +123,10 @@ const polyline = createShape({ return shape }, + transformSingle(shape, bounds, info) { + return this.transform(shape, bounds, info) + }, + canTransform: true, }) diff --git a/lib/shapes/ray.tsx b/lib/shapes/ray.tsx index cf42fd108..417bbb406 100644 --- a/lib/shapes/ray.tsx +++ b/lib/shapes/ray.tsx @@ -22,7 +22,7 @@ const ray = createShape({ direction: [0, 1], rotation: 0, style: { - fill: "rgba(142, 143, 142, 1.000)", + fill: "#c6cacb", stroke: "#000", strokeWidth: 1, }, diff --git a/lib/shapes/rectangle.tsx b/lib/shapes/rectangle.tsx index 265a1866e..f13b209b0 100644 --- a/lib/shapes/rectangle.tsx +++ b/lib/shapes/rectangle.tsx @@ -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({ boundsCache: new WeakMap([]), @@ -20,7 +30,7 @@ const rectangle = createShape({ 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({ }, 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({ }, 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({ 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({ 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, }) diff --git a/state/commands/create-shape.ts b/state/commands/create-shape.ts index 039a83ab0..4646017aa 100644 --- a/state/commands/create-shape.ts +++ b/state/commands/create-shape.ts @@ -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( diff --git a/state/commands/direct.ts b/state/commands/direct.ts index f71ce8176..f51d35921 100644 --- a/state/commands/direct.ts +++ b/state/commands/direct.ts @@ -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 diff --git a/state/commands/generate.ts b/state/commands/generate.ts index cc67679ad..c73848410 100644 --- a/state/commands/generate.ts +++ b/state/commands/generate.ts @@ -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[] diff --git a/state/commands/index.ts b/state/commands/index.ts index c0d702587..98b2f62a3 100644 --- a/state/commands/index.ts +++ b/state/commands/index.ts @@ -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, diff --git a/state/commands/rotate.ts b/state/commands/rotate.ts index 9bd66814a..3d3cc074c 100644 --- a/state/commands/rotate.ts +++ b/state/commands/rotate.ts @@ -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 diff --git a/state/commands/transform-single.ts b/state/commands/transform-single.ts new file mode 100644 index 000000000..d7852d1aa --- /dev/null +++ b/state/commands/transform-single.ts @@ -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, + }) + }, + }) + ) +} diff --git a/state/commands/transform.ts b/state/commands/transform.ts index ebe48d5c2..df840d60f 100644 --- a/state/commands/transform.ts +++ b/state/commands/transform.ts @@ -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, }) }) diff --git a/state/data.ts b/state/data.ts index af2873603..e518e23d6 100644 --- a/state/data.ts +++ b/state/data.ts @@ -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, // }, // }), diff --git a/state/sessions/index.ts b/state/sessions/index.ts index 8447985c8..ad62e56d0 100644 --- a/state/sessions/index.ts +++ b/state/sessions/index.ts @@ -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, } diff --git a/state/sessions/transform-session.ts b/state/sessions/transform-session.ts index 886f2238e..67c7fe8a1 100644 --- a/state/sessions/transform-session.ts +++ b/state/sessions/transform-session.ts @@ -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) => { diff --git a/state/sessions/transform-single-session.ts b/state/sessions/transform-single-session.ts new file mode 100644 index 000000000..563d17874 --- /dev/null +++ b/state/sessions/transform-single-session.ts @@ -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 +> diff --git a/state/state.ts b/state/state.ts index be8d568de..59006b29a 100644 --- a/state/state.ts +++ b/state/state.ts @@ -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( diff --git a/utils/utils.ts b/utils/utils.ts index e6b782900..7b750e235 100644 --- a/utils/utils.ts +++ b/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, + } +}