diff --git a/lib/code/generate.ts b/lib/code/generate.ts index 18e4163a7..f6bd0b08e 100644 --- a/lib/code/generate.ts +++ b/lib/code/generate.ts @@ -39,10 +39,9 @@ export function generateFromCode(code: string) { new Function(...Object.keys(scope), `${code}`)(...Object.values(scope)) - const generatedShapes = Array.from(codeShapes.values()).map((instance) => { - instance.shape.isGenerated = true - return instance.shape - }) + const generatedShapes = Array.from(codeShapes.values()).map( + (instance) => instance.shape + ) const generatedControls = Array.from(codeControls.values()) @@ -73,10 +72,9 @@ export function updateFromCode( new Function(...Object.keys(scope), `${code}`)(...Object.values(scope)) - const generatedShapes = Array.from(codeShapes.values()).map((instance) => { - instance.shape.isGenerated = true - return instance.shape - }) + const generatedShapes = Array.from(codeShapes.values()).map( + (instance) => instance.shape + ) return { shapes: generatedShapes } } diff --git a/lib/code/index.ts b/lib/code/index.ts index f67aefd75..c8deb291f 100644 --- a/lib/code/index.ts +++ b/lib/code/index.ts @@ -1,24 +1,23 @@ import { Shape } from "types" -import { getShapeUtils } from "lib/shape-utils" +import { getShapeUtils, ShapeUtility } from "lib/shape-utils" import * as vec from "utils/vec" import Vector from "./vector" import { vectorToPoint } from "utils/utils" export const codeShapes = new Set>([]) -type WithVectors = { - [key in keyof T]: number[] extends T[key] ? Vector : T[key] -} - /** * A base class for code shapes. Note that creating a shape adds it to the * shape map, while deleting it removes it from the collected shapes set */ export default class CodeShape { private _shape: T + private utils: ShapeUtility constructor(props: T) { this._shape = props + this.utils = getShapeUtils(this.shape) + codeShapes.add(this) } @@ -27,27 +26,31 @@ export default class CodeShape { } moveTo(point: Vector) { - this.shape.point = vectorToPoint(point) + this.utils.translate(this._shape, vectorToPoint(point)) + return this } translate(delta: Vector) { - this.shape.point = vec.add(this._shape.point, vectorToPoint(delta)) + this.utils.translate( + this._shape, + vec.add(this._shape.point, vectorToPoint(delta)) + ) + return this } rotate(rotation: number) { - this.shape.rotation = rotation - } - - scale(scale: number) { - return getShapeUtils(this.shape).scale(this.shape, scale) + this.utils.rotate(this._shape, rotation) + return this } getBounds() { - return getShapeUtils(this.shape).getBounds(this.shape) + this.utils.getBounds(this.shape) + return this } hitTest(point: Vector) { - return getShapeUtils(this.shape).hitTest(this.shape, vectorToPoint(point)) + this.utils.hitTest(this.shape, vectorToPoint(point)) + return this } get shape() { diff --git a/lib/shape-utils/circle.tsx b/lib/shape-utils/circle.tsx index e6c4e9aac..4b16533aa 100644 --- a/lib/shape-utils/circle.tsx +++ b/lib/shape-utils/circle.tsx @@ -81,17 +81,13 @@ const circle = registerShapeUtils({ ) }, - rotate(shape) { - return shape - }, - translate(shape, delta) { shape.point = vec.add(shape.point, delta) - return shape + return this }, - scale(shape, scale) { - return shape + rotate(shape) { + return this }, transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) { @@ -107,13 +103,23 @@ const circle = registerShapeUtils({ (scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]), ] - return shape + return this }, transformSingle(shape, bounds, info) { shape.radius = Math.min(bounds.width, bounds.height) / 2 shape.point = [bounds.minX, bounds.minY] - return shape + return this + }, + + setParent(shape, parentId) { + shape.parentId = parentId + return this + }, + + setChildIndex(shape, childIndex) { + shape.childIndex = childIndex + return this }, canTransform: true, diff --git a/lib/shape-utils/dot.tsx b/lib/shape-utils/dot.tsx index 63790abf3..d97fb8672 100644 --- a/lib/shape-utils/dot.tsx +++ b/lib/shape-utils/dot.tsx @@ -70,26 +70,33 @@ const dot = registerShapeUtils({ }, rotate(shape) { - return shape - }, - - scale(shape, scale: number) { - return shape + return this }, translate(shape, delta) { shape.point = vec.add(shape.point, delta) - return shape + return this }, transform(shape, bounds) { shape.point = [bounds.minX, bounds.minY] - return shape + return this }, transformSingle(shape, bounds, info) { - return this.transform(shape, bounds, info) + this.transform(shape, bounds, info) + return this + }, + + setParent(shape, parentId) { + shape.parentId = parentId + return this + }, + + setChildIndex(shape, childIndex) { + shape.childIndex = childIndex + return this }, canTransform: false, diff --git a/lib/shape-utils/ellipse.tsx b/lib/shape-utils/ellipse.tsx index 93016b153..728f844a7 100644 --- a/lib/shape-utils/ellipse.tsx +++ b/lib/shape-utils/ellipse.tsx @@ -100,16 +100,12 @@ const ellipse = registerShapeUtils({ }, rotate(shape) { - return shape + return this }, translate(shape, delta) { shape.point = vec.add(shape.point, delta) - return shape - }, - - scale(shape, scale: number) { - return shape + return this }, transform(shape, bounds, { scaleX, scaleY, initialShape }) { @@ -122,13 +118,23 @@ const ellipse = registerShapeUtils({ ? -initialShape.rotation : initialShape.rotation - return shape + return this }, transformSingle(shape, bounds, info) { return this.transform(shape, bounds, info) }, + setParent(shape, parentId) { + shape.parentId = parentId + return this + }, + + setChildIndex(shape, childIndex) { + shape.childIndex = childIndex + return this + }, + canTransform: true, canChangeAspectRatio: true, }) diff --git a/lib/shape-utils/index.tsx b/lib/shape-utils/index.tsx index 86a4adf66..fa7c8728b 100644 --- a/lib/shape-utils/index.tsx +++ b/lib/shape-utils/index.tsx @@ -6,6 +6,7 @@ import { ShapeType, Corner, Edge, + ShapeByType, } from "types" import circle from "./circle" import dot from "./dot" @@ -26,13 +27,66 @@ Operations throughout the app will call these utility methods when performing tests (such as hit tests) or mutations, such as translations. */ -export interface ShapeUtility { +export interface ShapeUtility> { // A cache for the computed bounds of this kind of shape. boundsCache: WeakMap + // Whether to show transform controls when this shape is selected. + canTransform: boolean + + // Whether the shape's aspect ratio can change + canChangeAspectRatio: boolean + // Create a new shape. create(props: Partial): K + // Apply a translation to a shape. + translate(this: ShapeUtility, shape: K, delta: number[]): ShapeUtility + + // Apply a rotation to a shape. + rotate(this: ShapeUtility, shape: K, rotation: number): ShapeUtility + + // Transform to fit a new bounding box when more than one shape is selected. + transform( + this: ShapeUtility, + shape: K, + bounds: Bounds, + info: { + type: Edge | Corner + initialShape: K + scaleX: number + scaleY: number + transformOrigin: number[] + } + ): ShapeUtility + + // Transform a single shape to fit a new bounding box. + transformSingle( + this: ShapeUtility, + shape: K, + bounds: Bounds, + info: { + type: Edge | Corner + initialShape: K + scaleX: number + scaleY: number + transformOrigin: number[] + } + ): ShapeUtility + + // Move a shape to a new parent. + setParent(this: ShapeUtility, shape: K, parentId: string): ShapeUtility + + // Change the child index of a shape + setChildIndex( + this: ShapeUtility, + shape: K, + childIndex: number + ): ShapeUtility + + // Render a shape to JSX. + render(this: ShapeUtility, shape: K): JSX.Element + // Get the bounds of the a shape. getBounds(this: ShapeUtility, shape: K): Bounds @@ -47,55 +101,10 @@ export interface ShapeUtility { // Test whether bounds collide with or contain a shape. hitTestBounds(this: ShapeUtility, shape: K, bounds: Bounds): boolean - - // Apply a rotation to a shape. - rotate(this: ShapeUtility, shape: K): K - - // Apply a translation to a shape. - translate(this: ShapeUtility, shape: K, delta: number[]): K - - // Transform to fit a new bounding box. - transform( - this: ShapeUtility, - shape: K, - bounds: Bounds, - info: { - type: Edge | Corner - initialShape: K - scaleX: number - scaleY: number - transformOrigin: number[] - } - ): K - - transformSingle( - this: ShapeUtility, - shape: K, - bounds: Bounds, - info: { - type: Edge | Corner - initialShape: K - scaleX: number - scaleY: number - transformOrigin: number[] - } - ): K - - // Apply a scale to a shape. - scale(this: ShapeUtility, shape: K, scale: number): K - - // Render a shape to JSX. - render(this: ShapeUtility, shape: K): JSX.Element - - // Whether to show transform controls when this shape is selected. - canTransform: boolean - - // Whether the shape's aspect ratio can change - canChangeAspectRatio: boolean } // A mapping of shape types to shape utilities. -const shapeUtilityMap: { [key in ShapeType]: ShapeUtility } = { +const shapeUtilityMap: Record> = { [ShapeType.Circle]: circle, [ShapeType.Dot]: dot, [ShapeType.Polyline]: polyline, @@ -110,8 +119,8 @@ const shapeUtilityMap: { [key in ShapeType]: ShapeUtility } = { * @param shape * @returns */ -export function getShapeUtils(shape: Shape): ShapeUtility { - return shapeUtilityMap[shape.type] +export function getShapeUtils(shape: T): ShapeUtility { + return shapeUtilityMap[shape.type] as ShapeUtility } /** @@ -125,4 +134,11 @@ export function registerShapeUtils( return Object.freeze(shape) } +export function createShape( + type: T, + props: Partial> +) { + return shapeUtilityMap[type].create(props) as ShapeByType +} + export default shapeUtilityMap diff --git a/lib/shape-utils/line.tsx b/lib/shape-utils/line.tsx index 01702f21f..1ea1bd131 100644 --- a/lib/shape-utils/line.tsx +++ b/lib/shape-utils/line.tsx @@ -79,28 +79,34 @@ const line = registerShapeUtils({ }, rotate(shape) { - return shape + return this }, translate(shape, delta) { shape.point = vec.add(shape.point, delta) - return shape - }, - - scale(shape, scale: number) { - return shape + return this }, transform(shape, bounds) { shape.point = [bounds.minX, bounds.minY] - return shape + return this }, transformSingle(shape, bounds, info) { return this.transform(shape, bounds, info) }, + setParent(shape, parentId) { + shape.parentId = parentId + return this + }, + + setChildIndex(shape, childIndex) { + shape.childIndex = childIndex + return this + }, + canTransform: false, canChangeAspectRatio: false, }) diff --git a/lib/shape-utils/polyline.tsx b/lib/shape-utils/polyline.tsx index a1c0838ca..ff9ef490a 100644 --- a/lib/shape-utils/polyline.tsx +++ b/lib/shape-utils/polyline.tsx @@ -87,16 +87,12 @@ const polyline = registerShapeUtils({ }, rotate(shape) { - return shape + return this }, translate(shape, delta) { shape.point = vec.add(shape.point, delta) - return shape - }, - - scale(shape, scale: number) { - return shape + return this }, transform(shape, bounds, { initialShape, scaleX, scaleY }) { @@ -117,11 +113,22 @@ const polyline = registerShapeUtils({ }) shape.point = [bounds.minX, bounds.minY] - return shape + return this }, transformSingle(shape, bounds, info) { - return this.transform(shape, bounds, info) + this.transform(shape, bounds, info) + return this + }, + + setParent(shape, parentId) { + shape.parentId = parentId + return this + }, + + setChildIndex(shape, childIndex) { + shape.childIndex = childIndex + return this }, canTransform: true, diff --git a/lib/shape-utils/ray.tsx b/lib/shape-utils/ray.tsx index 1637c7bb6..d9e9562a6 100644 --- a/lib/shape-utils/ray.tsx +++ b/lib/shape-utils/ray.tsx @@ -79,28 +79,34 @@ const ray = registerShapeUtils({ }, rotate(shape) { - return shape + return this }, translate(shape, delta) { shape.point = vec.add(shape.point, delta) - return shape - }, - - scale(shape, scale: number) { - return shape + return this }, transform(shape, bounds) { shape.point = [bounds.minX, bounds.minY] - return shape + return this }, transformSingle(shape, bounds, info) { return this.transform(shape, bounds, info) }, + setParent(shape, parentId) { + shape.parentId = parentId + return this + }, + + setChildIndex(shape, childIndex) { + shape.childIndex = childIndex + return this + }, + canTransform: false, canChangeAspectRatio: false, }) diff --git a/lib/shape-utils/rectangle.tsx b/lib/shape-utils/rectangle.tsx index db05bc8f1..e8ed9f5c4 100644 --- a/lib/shape-utils/rectangle.tsx +++ b/lib/shape-utils/rectangle.tsx @@ -96,16 +96,12 @@ const rectangle = registerShapeUtils({ }, rotate(shape) { - return shape + return this }, translate(shape, delta) { shape.point = vec.add(shape.point, delta) - return shape - }, - - scale(shape, scale) { - return shape + return this }, transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) { @@ -133,13 +129,23 @@ const rectangle = registerShapeUtils({ : initialShape.rotation } - return shape + return this }, transformSingle(shape, bounds) { shape.size = [bounds.width, bounds.height] shape.point = [bounds.minX, bounds.minY] - return shape + return this + }, + + setParent(shape, parentId) { + shape.parentId = parentId + return this + }, + + setChildIndex(shape, childIndex) { + shape.childIndex = childIndex + return this }, canTransform: true, diff --git a/state/commands/move.ts b/state/commands/move.ts index 07ac100ac..b19dfc572 100644 --- a/state/commands/move.ts +++ b/state/commands/move.ts @@ -2,6 +2,7 @@ import Command from "./command" import history from "../history" import { Data, MoveType, Shape } from "types" import { forceIntegerChildIndices, getChildren, getPage } from "utils/utils" +import { getShapeUtils } from "lib/shape-utils" export default function moveCommand(data: Data, type: MoveType) { const { currentPageId } = data @@ -75,7 +76,8 @@ export default function moveCommand(data: Data, type: MoveType) { const page = getPage(data) for (let id of selectedIds) { - page.shapes[id].childIndex = initialIndices[id] + const shape = page.shapes[id] + getShapeUtils(shape).setChildIndex(shape, initialIndices[id]) } }, }) @@ -93,7 +95,9 @@ function moveToFront(shapes: Shape[], siblings: Shape[]) { const startIndex = Math.ceil(diff[0].childIndex) + 1 - shapes.forEach((shape, i) => (shape.childIndex = startIndex + i)) + shapes.forEach((shape, i) => + getShapeUtils(shape).setChildIndex(shape, startIndex + i) + ) } function moveToBack(shapes: Shape[], siblings: Shape[]) { @@ -109,7 +113,9 @@ function moveToBack(shapes: Shape[], siblings: Shape[]) { const step = startIndex / (shapes.length + 1) - shapes.forEach((shape, i) => (shape.childIndex = startIndex - (i + 1) * step)) + shapes.forEach((shape, i) => + getShapeUtils(shape).setChildIndex(shape, startIndex - (i + 1) * step) + ) } function moveForward(shape: Shape, siblings: Shape[], visited: Set) { @@ -132,7 +138,7 @@ function moveForward(shape: Shape, siblings: Shape[], visited: Set) { : Math.ceil(nextSibling.childIndex + 1) } - shape.childIndex = nextIndex + getShapeUtils(shape).setChildIndex(shape, nextIndex) siblings.sort((a, b) => a.childIndex - b.childIndex) } @@ -158,7 +164,7 @@ function moveBackward(shape: Shape, siblings: Shape[], visited: Set) { : nextSibling.childIndex / 2 } - shape.childIndex = nextIndex + getShapeUtils(shape).setChildIndex(shape, nextIndex) siblings.sort((a, b) => a.childIndex - b.childIndex) } diff --git a/state/commands/rotate.ts b/state/commands/rotate.ts index dd10be7a4..40c9b4cdc 100644 --- a/state/commands/rotate.ts +++ b/state/commands/rotate.ts @@ -3,6 +3,7 @@ import history from "../history" import { Data } from "types" import { RotateSnapshot } from "state/sessions/rotate-session" import { getPage } from "utils/utils" +import { getShapeUtils } from "lib/shape-utils" export default function rotateCommand( data: Data, @@ -19,8 +20,9 @@ export default function rotateCommand( for (let { id, point, rotation } of after.shapes) { const shape = shapes[id] - shape.rotation = rotation - shape.point = point + const utils = getShapeUtils(shape) + utils.rotate(shape, rotation) + utils.translate(shape, point) } data.boundsRotation = after.boundsRotation @@ -30,8 +32,9 @@ export default function rotateCommand( for (let { id, point, rotation } of before.shapes) { const shape = shapes[id] - shape.rotation = rotation - shape.point = point + const utils = getShapeUtils(shape) + utils.rotate(shape, rotation) + utils.translate(shape, point) } data.boundsRotation = before.boundsRotation diff --git a/state/commands/translate.ts b/state/commands/translate.ts index 7a05f3029..fceb094e4 100644 --- a/state/commands/translate.ts +++ b/state/commands/translate.ts @@ -3,6 +3,7 @@ import history from "../history" import { TranslateSnapshot } from "state/sessions/translate-session" import { Data } from "types" import { getPage } from "utils/utils" +import { getShapeUtils } from "lib/shape-utils" export default function translateCommand( data: Data, @@ -32,7 +33,8 @@ export default function translateCommand( } for (const { id, point } of initialShapes) { - shapes[id].point = point + const shape = shapes[id] + getShapeUtils(shape).translate(shape, point) data.selectedIds.add(id) } }, @@ -49,7 +51,8 @@ export default function translateCommand( } for (const { id, point } of initialShapes) { - shapes[id].point = point + const shape = shapes[id] + getShapeUtils(shape).translate(shape, point) data.selectedIds.add(id) } }, diff --git a/state/sessions/rotate-session.ts b/state/sessions/rotate-session.ts index 10b03cad3..df5c9b21c 100644 --- a/state/sessions/rotate-session.ts +++ b/state/sessions/rotate-session.ts @@ -11,6 +11,7 @@ import { getSelectedShapes, getShapeBounds, } from "utils/utils" +import { getShapeUtils } from "lib/shape-utils" const PI2 = Math.PI * 2 @@ -42,9 +43,13 @@ export default class RotateSession extends BaseSession { for (let { id, center, offset, rotation } of shapes) { const shape = page.shapes[id] - shape.rotation = (PI2 + (rotation + rot)) % PI2 - const newCenter = vec.rotWith(center, boundsCenter, rot % PI2) - shape.point = vec.sub(newCenter, offset) + + getShapeUtils(shape) + .rotate(shape, (PI2 + (rotation + rot)) % PI2) + .translate( + shape, + vec.sub(vec.rotWith(center, boundsCenter, rot % PI2), offset) + ) } } @@ -53,8 +58,7 @@ export default class RotateSession extends BaseSession { for (let { id, point, rotation } of this.snapshot.shapes) { const shape = page.shapes[id] - shape.rotation = rotation - shape.point = point + getShapeUtils(shape).rotate(shape, rotation).translate(shape, point) } } diff --git a/state/sessions/translate-session.ts b/state/sessions/translate-session.ts index e802f9647..2292ade39 100644 --- a/state/sessions/translate-session.ts +++ b/state/sessions/translate-session.ts @@ -5,6 +5,7 @@ import commands from "state/commands" import { current } from "immer" import { v4 as uuid } from "uuid" import { getChildIndexAbove, getPage, getSelectedShapes } from "utils/utils" +import { getShapeUtils } from "lib/shape-utils" export default class TranslateSession extends BaseSession { delta = [0, 0] @@ -38,7 +39,8 @@ export default class TranslateSession extends BaseSession { data.selectedIds.clear() for (const { id, point } of initialShapes) { - shapes[id].point = point + const shape = shapes[id] + getShapeUtils(shape).translate(shape, point) } for (const clone of clones) { @@ -48,7 +50,8 @@ export default class TranslateSession extends BaseSession { } for (const { id, point } of clones) { - shapes[id].point = vec.add(point, delta) + const shape = shapes[id] + getShapeUtils(shape).translate(shape, vec.add(point, delta)) } } else { if (this.isCloning) { @@ -65,7 +68,8 @@ export default class TranslateSession extends BaseSession { } for (const { id, point } of initialShapes) { - shapes[id].point = vec.add(point, delta) + const shape = shapes[id] + getShapeUtils(shape).translate(shape, vec.add(point, delta)) } } } @@ -75,7 +79,8 @@ export default class TranslateSession extends BaseSession { const { shapes } = getPage(data, currentPageId) for (const { id, point } of initialShapes) { - shapes[id].point = point + const shape = shapes[id] + getShapeUtils(shape).translate(shape, point) } for (const { id } of clones) { diff --git a/state/state.ts b/state/state.ts index 0034f4f10..5d05d02a8 100644 --- a/state/state.ts +++ b/state/state.ts @@ -525,12 +525,16 @@ const state = createState({ }, actions: { /* --------------------- Shapes --------------------- */ - createShape(data, payload: PointerInfo, shape: Shape) { + createShape(data, payload, shape: Shape) { const siblings = getChildren(data, shape.parentId) - shape.childIndex = - siblings.length > 0 ? siblings[siblings.length - 1].childIndex + 1 : 1 + const childIndex = siblings.length + ? siblings[siblings.length - 1].childIndex + 1 + : 1 + + getShapeUtils(shape).setChildIndex(shape, childIndex) getPage(data).shapes[shape.id] = shape + data.selectedIds.clear() data.selectedIds.add(shape.id) }, @@ -608,19 +612,11 @@ const state = createState({ data, payload: PointerInfo & { target: Corner | Edge } ) { + const point = screenToWorld(inputs.pointer.origin, data) session = data.selectedIds.size === 1 - ? new Sessions.TransformSingleSession( - data, - payload.target, - screenToWorld(payload.point, data), - false - ) - : new Sessions.TransformSession( - data, - payload.target, - screenToWorld(payload.point, data) - ) + ? new Sessions.TransformSingleSession(data, payload.target, point) + : new Sessions.TransformSession(data, payload.target, point) }, startDrawTransformSession(data, payload: PointerInfo) { session = new Sessions.TransformSingleSession( @@ -651,7 +647,7 @@ const state = createState({ startDirectionSession(data, payload: PointerInfo) { session = new Sessions.DirectionSession( data, - screenToWorld(payload.point, data) + screenToWorld(inputs.pointer.origin, data) ) }, updateDirectionSession(data, payload: PointerInfo) { diff --git a/types.ts b/types.ts index 12486d3f3..fe4d4d027 100644 --- a/types.ts +++ b/types.ts @@ -106,7 +106,7 @@ export interface RectangleShape extends BaseShape { size: number[] } -export type Shape = +export type Shape = Readonly< | DotShape | CircleShape | EllipseShape @@ -114,8 +114,9 @@ export type Shape = | RayShape | PolylineShape | RectangleShape +> -export interface Shapes extends Record { +export interface Shapes { [ShapeType.Dot]: DotShape [ShapeType.Circle]: CircleShape [ShapeType.Ellipse]: EllipseShape @@ -125,6 +126,8 @@ export interface Shapes extends Record { [ShapeType.Rectangle]: RectangleShape } +export type ShapeByType = Shapes[T] + export interface CodeFile { id: string name: string diff --git a/utils/utils.ts b/utils/utils.ts index 9fe6e5e95..30aa63b2d 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1479,7 +1479,8 @@ export function getChildIndexBelow( export function forceIntegerChildIndices(shapes: Shape[]) { for (let i = 0; i < shapes.length; i++) { - shapes[i].childIndex = i + 1 + const shape = shapes[i] + getShapeUtils(shape).setChildIndex(shape, i + 1) } } export function setZoomCSS(zoom: number) {