refactors bounds, improves transforming rotating shapes

This commit is contained in:
Steve Ruiz 2021-05-22 16:45:24 +01:00
parent fb0bb47c19
commit b752782753
33 changed files with 690 additions and 632 deletions

View file

@ -1,47 +0,0 @@
import { useRef } from "react"
import state, { useSelector } from "state"
import inputs from "state/inputs"
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]
const selected = Array.from(s.data.selectedIds.values())[0]
return shapes[selected].rotation
} else {
return 0
}
})
if (!bounds) return null
if (!isSelecting) return null
const { minX, minY, width, height } = bounds
return (
<StyledBoundsBg
ref={rBounds}
x={minX}
y={minY}
width={Math.max(1, width)}
height={Math.max(1, height)}
onPointerDown={(e) => {
if (e.buttons !== 1) return
e.stopPropagation()
rBounds.current.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
}}
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
minY + height / 2
})`}
/>
)
}
const StyledBoundsBg = styled("rect", {
fill: "$boundsBg",
})

View file

@ -1,285 +0,0 @@
import state, { useSelector } from "state"
import styled from "styles"
import inputs from "state/inputs"
import { useRef } from "react"
import { TransformCorner, TransformEdge } from "types"
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]
const selected = Array.from(s.data.selectedIds.values())[0]
return shapes[selected].rotation
} else {
return 0
}
})
if (!bounds) return null
if (!isSelecting) return null
let { minX, minY, maxX, maxY, width, height } = bounds
const p = 4 / zoom
const cp = p * 2
return (
<g
pointerEvents={isBrushing ? "none" : "all"}
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
minY + height / 2
})`}
>
<StyledBounds
x={minX}
y={minY}
width={width}
height={height}
pointerEvents="none"
/>
<EdgeHorizontal
x={minX + p}
y={minY}
width={Math.max(0, width - p * 2)}
height={p}
edge={TransformEdge.Top}
/>
<EdgeVertical
x={maxX}
y={minY + p}
width={p}
height={Math.max(0, height - p * 2)}
edge={TransformEdge.Right}
/>
<EdgeHorizontal
x={minX + p}
y={maxY}
width={Math.max(0, width - p * 2)}
height={p}
edge={TransformEdge.Bottom}
/>
<EdgeVertical
x={minX}
y={minY + p}
width={p}
height={Math.max(0, height - p * 2)}
edge={TransformEdge.Left}
/>
<Corner
x={minX}
y={minY}
width={cp}
height={cp}
corner={TransformCorner.TopLeft}
/>
<Corner
x={maxX}
y={minY}
width={cp}
height={cp}
corner={TransformCorner.TopRight}
/>
<Corner
x={maxX}
y={maxY}
width={cp}
height={cp}
corner={TransformCorner.BottomRight}
/>
<Corner
x={minX}
y={maxY}
width={cp}
height={cp}
corner={TransformCorner.BottomLeft}
/>
<RotateHandle x={minX + width / 2} y={minY - cp * 2} r={cp / 2} />
</g>
)
}
function RotateHandle({ x, y, r }: { x: number; y: number; r: number }) {
const rRotateHandle = useRef<SVGCircleElement>(null)
return (
<StyledRotateHandle
ref={rRotateHandle}
cx={x}
cy={y}
r={r}
onPointerDown={(e) => {
e.stopPropagation()
rRotateHandle.current.setPointerCapture(e.pointerId)
state.send("POINTED_ROTATE_HANDLE", inputs.pointerDown(e, "rotate"))
}}
onPointerUp={(e) => {
e.stopPropagation()
rRotateHandle.current.releasePointerCapture(e.pointerId)
rRotateHandle.current.replaceWith(rRotateHandle.current)
state.send("STOPPED_POINTING", inputs.pointerDown(e, "rotate"))
}}
/>
)
}
function Corner({
x,
y,
width,
height,
corner,
}: {
x: number
y: number
width: number
height: number
corner: TransformCorner
}) {
const rCorner = useRef<SVGRectElement>(null)
return (
<g>
<StyledCorner
ref={rCorner}
x={x + width * -0.5}
y={y + height * -0.5}
width={width}
height={height}
corner={corner}
onPointerDown={(e) => {
e.stopPropagation()
rCorner.current.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
}}
onPointerUp={(e) => {
e.stopPropagation()
rCorner.current.releasePointerCapture(e.pointerId)
rCorner.current.replaceWith(rCorner.current)
state.send("STOPPED_POINTING", inputs.pointerDown(e, corner))
}}
/>
</g>
)
}
function EdgeHorizontal({
x,
y,
width,
height,
edge,
}: {
x: number
y: number
width: number
height: number
edge: TransformEdge.Top | TransformEdge.Bottom
}) {
const rEdge = useRef<SVGRectElement>(null)
return (
<StyledEdge
ref={rEdge}
x={x}
y={y - height / 2}
width={width}
height={height}
onPointerDown={(e) => {
e.stopPropagation()
rEdge.current.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
}}
onPointerUp={(e) => {
e.stopPropagation()
e.preventDefault()
state.send("STOPPED_POINTING", inputs.pointerUp(e))
rEdge.current.releasePointerCapture(e.pointerId)
rEdge.current.replaceWith(rEdge.current)
}}
edge={edge}
/>
)
}
function EdgeVertical({
x,
y,
width,
height,
edge,
}: {
x: number
y: number
width: number
height: number
edge: TransformEdge.Right | TransformEdge.Left
}) {
const rEdge = useRef<SVGRectElement>(null)
return (
<StyledEdge
ref={rEdge}
x={x - width / 2}
y={y}
width={width}
height={height}
onPointerDown={(e) => {
e.stopPropagation()
state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
rEdge.current.setPointerCapture(e.pointerId)
}}
onPointerUp={(e) => {
e.stopPropagation()
state.send("STOPPED_POINTING", inputs.pointerUp(e))
rEdge.current.releasePointerCapture(e.pointerId)
rEdge.current.replaceWith(rEdge.current)
}}
edge={edge}
/>
)
}
const StyledEdge = styled("rect", {
stroke: "none",
fill: "none",
variants: {
edge: {
bottom_edge: { cursor: "ns-resize" },
right_edge: { cursor: "ew-resize" },
top_edge: { cursor: "ns-resize" },
left_edge: { cursor: "ew-resize" },
},
},
})
const StyledCorner = styled("rect", {
stroke: "$bounds",
fill: "#fff",
zStrokeWidth: 2,
variants: {
corner: {
top_left_corner: { cursor: "nwse-resize" },
top_right_corner: { cursor: "nesw-resize" },
bottom_right_corner: { cursor: "nwse-resize" },
bottom_left_corner: { cursor: "nesw-resize" },
},
},
})
const StyledRotateHandle = styled("circle", {
stroke: "$bounds",
fill: "#fff",
zStrokeWidth: 2,
cursor: "grab",
})
const StyledBounds = styled("rect", {
fill: "none",
stroke: "$bounds",
zStrokeWidth: 2,
})

View file

@ -0,0 +1,46 @@
import * as React from "react"
import { Edge, Corner } from "types"
import { useSelector } from "state"
import { getSelectedShapes, isMobile } from "utils/utils"
import CenterHandle from "./center-handle"
import CornerHandle from "./corner-handle"
import EdgeHandle from "./edge-handle"
import RotateHandle from "./rotate-handle"
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(({ data }) =>
data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
)
if (!bounds) return null
if (!isSelecting) return null
const size = (isMobile().any ? 16 : 8) / zoom // Touch target size
return (
<g
pointerEvents={isBrushing ? "none" : "all"}
transform={`
rotate(${rotation * (180 / Math.PI)},
${(bounds.minX + bounds.maxX) / 2},
${(bounds.minY + bounds.maxY) / 2})
translate(${bounds.minX},${bounds.minY})`}
>
<CenterHandle bounds={bounds} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Bottom} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Left} />
<CornerHandle size={size} bounds={bounds} corner={Corner.TopLeft} />
<CornerHandle size={size} bounds={bounds} corner={Corner.TopRight} />
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomRight} />
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomLeft} />
<RotateHandle size={size} bounds={bounds} />
</g>
)
}

View file

@ -0,0 +1,58 @@
import { useCallback, useRef } from "react"
import state, { useSelector } from "state"
import inputs from "state/inputs"
import styled from "styles"
import { getPage } from "utils/utils"
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
if (e.buttons !== 1) return
e.stopPropagation()
e.currentTarget.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
}
function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
if (e.buttons !== 1) return
e.stopPropagation()
e.currentTarget.releasePointerCapture(e.pointerId)
state.send("STOPPED_POINTING", inputs.pointerUp(e))
}
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 } = getPage(s.data)
const selected = Array.from(s.data.selectedIds.values())[0]
return shapes[selected].rotation
} else {
return 0
}
})
if (!bounds) return null
if (!isSelecting) return null
const { width, height } = bounds
return (
<StyledBoundsBg
ref={rBounds}
width={Math.max(1, width)}
height={Math.max(1, height)}
transform={`
rotate(${rotation * (180 / Math.PI)},
${(bounds.minX + bounds.maxX) / 2},
${(bounds.minY + bounds.maxY) / 2})
translate(${bounds.minX},${bounds.minY})`}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
/>
)
}
const StyledBoundsBg = styled("rect", {
fill: "$boundsBg",
})

View file

@ -0,0 +1,18 @@
import styled from "styles"
import { Bounds } from "types"
export default function CenterHandle({ bounds }: { bounds: Bounds }) {
return (
<StyledBounds
width={bounds.width}
height={bounds.height}
pointerEvents="none"
/>
)
}
const StyledBounds = styled("rect", {
fill: "none",
stroke: "$bounds",
zStrokeWidth: 2,
})

View file

@ -0,0 +1,43 @@
import useHandleEvents from "hooks/useBoundsHandleEvents"
import styled from "styles"
import { Corner, Bounds } from "types"
export default function CornerHandle({
size,
corner,
bounds,
}: {
size: number
bounds: Bounds
corner: Corner
}) {
const events = useHandleEvents(corner)
const isTop = corner === Corner.TopLeft || corner === Corner.TopRight
const isLeft = corner === Corner.TopLeft || corner === Corner.BottomLeft
return (
<StyledCorner
corner={corner}
x={(isLeft ? 0 : bounds.width) - size / 2}
y={(isTop ? 0 : bounds.height) - size / 2}
width={size}
height={size}
{...events}
/>
)
}
const StyledCorner = styled("rect", {
stroke: "$bounds",
fill: "#fff",
zStrokeWidth: 2,
variants: {
corner: {
[Corner.TopLeft]: { cursor: "nwse-resize" },
[Corner.TopRight]: { cursor: "nesw-resize" },
[Corner.BottomRight]: { cursor: "nwse-resize" },
[Corner.BottomLeft]: { cursor: "nesw-resize" },
},
},
})

View file

@ -0,0 +1,42 @@
import useHandleEvents from "hooks/useBoundsHandleEvents"
import styled from "styles"
import { Edge, Bounds } from "types"
export default function EdgeHandle({
size,
bounds,
edge,
}: {
size: number
bounds: Bounds
edge: Edge
}) {
const events = useHandleEvents(edge)
const isHorizontal = edge === Edge.Top || edge === Edge.Bottom
const isFarEdge = edge === Edge.Right || edge === Edge.Bottom
return (
<StyledEdge
edge={edge}
x={isHorizontal ? size / 2 : (isFarEdge ? bounds.width : 0) - size / 2}
y={isHorizontal ? (isFarEdge ? bounds.height : 0) - size / 2 : size / 2}
width={isHorizontal ? Math.max(0, bounds.width - size) : size}
height={isHorizontal ? size : Math.max(0, bounds.height - size)}
{...events}
/>
)
}
const StyledEdge = styled("rect", {
stroke: "none",
fill: "none",
variants: {
edge: {
[Edge.Top]: { cursor: "ns-resize" },
[Edge.Right]: { cursor: "ew-resize" },
[Edge.Bottom]: { cursor: "ns-resize" },
[Edge.Left]: { cursor: "ew-resize" },
},
},
})

View file

@ -0,0 +1,30 @@
import useHandleEvents from "hooks/useBoundsHandleEvents"
import styled from "styles"
import { Bounds } from "types"
export default function Rotate({
bounds,
size,
}: {
bounds: Bounds
size: number
}) {
const events = useHandleEvents("rotate")
return (
<StyledRotateHandle
cursor="grab"
cx={bounds.width / 2}
cy={size * -2}
r={size / 2}
{...events}
/>
)
}
const StyledRotateHandle = styled("circle", {
stroke: "$bounds",
fill: "#fff",
zStrokeWidth: 2,
cursor: "grab",
})

View file

@ -5,8 +5,8 @@ import useCamera from "hooks/useCamera"
import Page from "./page" import Page from "./page"
import Brush from "./brush" import Brush from "./brush"
import state from "state" import state from "state"
import Bounds from "./bounds" import Bounds from "./bounds/bounding-box"
import BoundsBg from "./bounds-bg" import BoundsBg from "./bounds/bounds-bg"
import inputs from "state/inputs" import inputs from "state/inputs"
export default function Canvas() { export default function Canvas() {

View file

@ -1,5 +1,5 @@
import { useSelector } from "state" import { useSelector } from "state"
import { deepCompareArrays } from "utils/utils" import { deepCompareArrays, getPage } from "utils/utils"
import Shape from "./shape" import Shape from "./shape"
/* /*
@ -10,7 +10,7 @@ here; and still cheaper than any other pattern I've found.
export default function Page() { export default function Page() {
const currentPageShapeIds = useSelector( const currentPageShapeIds = useSelector(
({ data }) => Object.keys(data.document.pages[data.currentPageId].shapes), ({ data }) => Object.keys(getPage(data).shapes),
deepCompareArrays deepCompareArrays
) )

View file

@ -3,6 +3,7 @@ import state, { useSelector } from "state"
import inputs from "state/inputs" import inputs from "state/inputs"
import { getShapeUtils } from "lib/shape-utils" import { getShapeUtils } from "lib/shape-utils"
import styled from "styles" import styled from "styles"
import { getPage } from "utils/utils"
function Shape({ id }: { id: string }) { function Shape({ id }: { id: string }) {
const rGroup = useRef<SVGGElement>(null) const rGroup = useRef<SVGGElement>(null)
@ -11,9 +12,7 @@ function Shape({ id }: { id: string }) {
const isSelected = useSelector((state) => state.values.selectedIds.has(id)) const isSelected = useSelector((state) => state.values.selectedIds.has(id))
const shape = useSelector( const shape = useSelector(({ data }) => getPage(data).shapes[id])
({ data }) => data.document.pages[data.currentPageId].shapes[id]
)
const handlePointerDown = useCallback( const handlePointerDown = useCallback(
(e: React.PointerEvent) => { (e: React.PointerEvent) => {

View file

@ -0,0 +1,38 @@
import { useCallback, useRef } from "react"
import inputs from "state/inputs"
import { Edge, Corner } from "types"
import state from "../state"
export default function useBoundsHandleEvents(
handle: Edge | Corner | "rotate"
) {
const onPointerDown = useCallback(
(e) => {
if (e.buttons !== 1) return
e.stopPropagation()
e.currentTarget.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS_HANDLE", inputs.pointerDown(e, handle))
},
[handle]
)
const onPointerMove = useCallback(
(e) => {
if (e.buttons !== 1) return
e.stopPropagation()
state.send("MOVED_POINTER", inputs.pointerMove(e))
},
[handle]
)
const onPointerUp = useCallback((e) => {
if (e.buttons !== 1) return
e.stopPropagation()
e.currentTarget.releasePointerCapture(e.pointerId)
e.currentTarget.replaceWith(e.currentTarget)
state.send("STOPPED_POINTING", inputs.pointerUp(e))
}, [])
return { onPointerDown, onPointerMove, onPointerUp }
}

View file

@ -1,6 +1,6 @@
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { CircleShape, ShapeType, TransformCorner, TransformEdge } from "types" import { CircleShape, ShapeType, Corner, Edge } from "types"
import { registerShapeUtils } from "./index" import { registerShapeUtils } from "./index"
import { boundsContained } from "utils/bounds" import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections" import { intersectCircleBounds } from "utils/intersections"
@ -99,7 +99,7 @@ const circle = registerShapeUtils<CircleShape>({
// Set the new corner or position depending on the anchor // Set the new corner or position depending on the anchor
switch (anchor) { switch (anchor) {
case TransformCorner.TopLeft: { case Corner.TopLeft: {
shape.radius = Math.min(bounds.width, bounds.height) / 2 shape.radius = Math.min(bounds.width, bounds.height) / 2
shape.point = [ shape.point = [
bounds.maxX - shape.radius * 2, bounds.maxX - shape.radius * 2,
@ -107,12 +107,12 @@ const circle = registerShapeUtils<CircleShape>({
] ]
break break
} }
case TransformCorner.TopRight: { case Corner.TopRight: {
shape.radius = Math.min(bounds.width, bounds.height) / 2 shape.radius = Math.min(bounds.width, bounds.height) / 2
shape.point = [bounds.minX, bounds.maxY - shape.radius * 2] shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
break break
} }
case TransformCorner.BottomRight: { case Corner.BottomRight: {
shape.radius = Math.min(bounds.width, bounds.height) / 2 shape.radius = Math.min(bounds.width, bounds.height) / 2
shape.point = [ shape.point = [
bounds.maxX - shape.radius * 2, bounds.maxX - shape.radius * 2,
@ -121,12 +121,12 @@ const circle = registerShapeUtils<CircleShape>({
break break
break break
} }
case TransformCorner.BottomLeft: { case Corner.BottomLeft: {
shape.radius = Math.min(bounds.width, bounds.height) / 2 shape.radius = Math.min(bounds.width, bounds.height) / 2
shape.point = [bounds.maxX - shape.radius * 2, bounds.minY] shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
break break
} }
case TransformEdge.Top: { case Edge.Top: {
shape.radius = bounds.height / 2 shape.radius = bounds.height / 2
shape.point = [ shape.point = [
bounds.minX + (bounds.width / 2 - shape.radius), bounds.minX + (bounds.width / 2 - shape.radius),
@ -134,7 +134,7 @@ const circle = registerShapeUtils<CircleShape>({
] ]
break break
} }
case TransformEdge.Right: { case Edge.Right: {
shape.radius = bounds.width / 2 shape.radius = bounds.width / 2
shape.point = [ shape.point = [
bounds.maxX - shape.radius * 2, bounds.maxX - shape.radius * 2,
@ -142,7 +142,7 @@ const circle = registerShapeUtils<CircleShape>({
] ]
break break
} }
case TransformEdge.Bottom: { case Edge.Bottom: {
shape.radius = bounds.height / 2 shape.radius = bounds.height / 2
shape.point = [ shape.point = [
bounds.minX + (bounds.width / 2 - shape.radius), bounds.minX + (bounds.width / 2 - shape.radius),
@ -150,7 +150,7 @@ const circle = registerShapeUtils<CircleShape>({
] ]
break break
} }
case TransformEdge.Left: { case Edge.Left: {
shape.radius = bounds.width / 2 shape.radius = bounds.width / 2
shape.point = [ shape.point = [
bounds.minX, bounds.minX,

View file

@ -4,8 +4,8 @@ import {
Shape, Shape,
Shapes, Shapes,
ShapeType, ShapeType,
TransformCorner, Corner,
TransformEdge, Edge,
} from "types" } from "types"
import circle from "./circle" import circle from "./circle"
import dot from "./dot" import dot from "./dot"
@ -60,10 +60,11 @@ export interface ShapeUtility<K extends Shape> {
shape: K, shape: K,
bounds: Bounds, bounds: Bounds,
info: { info: {
type: TransformEdge | TransformCorner | "center" type: Edge | Corner | "center"
initialShape: K initialShape: K
scaleX: number scaleX: number
scaleY: number scaleY: number
transformOrigin: number[]
} }
): K ): K
@ -72,10 +73,11 @@ export interface ShapeUtility<K extends Shape> {
shape: K, shape: K,
bounds: Bounds, bounds: Bounds,
info: { info: {
type: TransformEdge | TransformCorner | "center" type: Edge | Corner | "center"
initialShape: K initialShape: K
scaleX: number scaleX: number
scaleY: number scaleY: number
transformOrigin: number[]
} }
): K ): K

View file

@ -1,16 +1,13 @@
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { import { RectangleShape, ShapeType, Corner, Edge } from "types"
RectangleShape,
ShapeType,
TransformCorner,
TransformEdge,
} from "types"
import { registerShapeUtils } from "./index" import { registerShapeUtils } from "./index"
import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds" import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
import { import {
getBoundsFromPoints, getBoundsFromPoints,
getRotatedCorners, getRotatedCorners,
getRotatedSize,
lerp,
rotateBounds, rotateBounds,
translateBounds, translateBounds,
} from "utils/utils" } from "utils/utils"
@ -99,27 +96,33 @@ const rectangle = registerShapeUtils<RectangleShape>({
return shape return shape
}, },
transform(shape, bounds, { initialShape, scaleX, scaleY }) { transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
if (shape.rotation === 0) { if (shape.rotation === 0) {
shape.size = [bounds.width, bounds.height] shape.size = [bounds.width, bounds.height]
shape.point = [bounds.minX, bounds.minY] shape.point = [bounds.minX, bounds.minY]
} else { } else {
// Center shape in resized bounds // Size
shape.size = vec.mul( shape.size = vec.mul(
initialShape.size, initialShape.size,
Math.min(Math.abs(scaleX), Math.abs(scaleY)) Math.min(Math.abs(scaleX), Math.abs(scaleY))
) )
shape.point = vec.sub( // Point
vec.med([bounds.minX, bounds.minY], [bounds.maxX, bounds.maxY]), shape.point = [
vec.div(shape.size, 2) bounds.minX +
) (bounds.width - shape.size[0]) *
} (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
bounds.minY +
(bounds.height - shape.size[1]) *
(scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
]
// Set rotation for flipped shapes // Rotation
shape.rotation = initialShape.rotation shape.rotation =
if (scaleX < 0) shape.rotation *= -1 (scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
if (scaleY < 0) shape.rotation *= -1 ? -initialShape.rotation
: initialShape.rotation
}
return shape return shape
}, },

View file

@ -13,6 +13,7 @@
"@stitches/react": "^0.1.9", "@stitches/react": "^0.1.9",
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"framer-motion": "^4.1.16", "framer-motion": "^4.1.16",
"ismobilejs": "^1.1.1",
"next": "10.2.0", "next": "10.2.0",
"perfect-freehand": "^0.4.7", "perfect-freehand": "^0.4.7",
"prettier": "^2.3.0", "prettier": "^2.3.0",

View file

@ -1,6 +1,7 @@
import Command from "./command" import Command from "./command"
import history from "../history" import history from "../history"
import { Data, Shape } from "types" import { Data, Shape } from "types"
import { getPage } from "utils/utils"
export default function registerShapeUtilsCommand(data: Data, shape: Shape) { export default function registerShapeUtilsCommand(data: Data, shape: Shape) {
const { currentPageId } = data const { currentPageId } = data
@ -11,17 +12,17 @@ export default function registerShapeUtilsCommand(data: Data, shape: Shape) {
name: "translate_shapes", name: "translate_shapes",
category: "canvas", category: "canvas",
do(data) { do(data) {
const { shapes } = data.document.pages[currentPageId] const page = getPage(data)
shapes[shape.id] = shape page.shapes[shape.id] = shape
data.selectedIds.clear() data.selectedIds.clear()
data.pointedId = undefined data.pointedId = undefined
data.hoveredId = undefined data.hoveredId = undefined
}, },
undo(data) { undo(data) {
const { shapes } = data.document.pages[currentPageId] const page = getPage(data)
delete shapes[shape.id] delete page.shapes[shape.id]
data.selectedIds.clear() data.selectedIds.clear()
data.pointedId = undefined data.pointedId = undefined

View file

@ -2,6 +2,7 @@ import Command from "./command"
import history from "../history" import history from "../history"
import { DirectionSnapshot } from "state/sessions/direction-session" import { DirectionSnapshot } from "state/sessions/direction-session"
import { Data, LineShape, RayShape } from "types" import { Data, LineShape, RayShape } from "types"
import { getPage } from "utils/utils"
export default function directCommand( export default function directCommand(
data: Data, data: Data,
@ -14,7 +15,7 @@ export default function directCommand(
name: "set_direction", name: "set_direction",
category: "canvas", category: "canvas",
do(data) { do(data) {
const { shapes } = data.document.pages[after.currentPageId] const { shapes } = getPage(data)
for (let { id, direction } of after.shapes) { for (let { id, direction } of after.shapes) {
const shape = shapes[id] as RayShape | LineShape const shape = shapes[id] as RayShape | LineShape
@ -23,7 +24,7 @@ export default function directCommand(
} }
}, },
undo(data) { undo(data) {
const { shapes } = data.document.pages[before.currentPageId] const { shapes } = getPage(data, before.currentPageId)
for (let { id, direction } of after.shapes) { for (let { id, direction } of after.shapes) {
const shape = shapes[id] as RayShape | LineShape const shape = shapes[id] as RayShape | LineShape

View file

@ -2,6 +2,7 @@ import Command from "./command"
import history from "../history" import history from "../history"
import { CodeControl, Data, Shape } from "types" import { CodeControl, Data, Shape } from "types"
import { current } from "immer" import { current } from "immer"
import { getPage } from "utils/utils"
export default function generateCommand( export default function generateCommand(
data: Data, data: Data,
@ -9,12 +10,13 @@ export default function generateCommand(
generatedShapes: Shape[] generatedShapes: Shape[]
) { ) {
const cData = current(data) const cData = current(data)
const page = getPage(cData)
const prevGeneratedShapes = Object.values( const currentShapes = page.shapes
cData.document.pages[currentPageId].shapes
).filter((shape) => shape.isGenerated)
const currentShapes = data.document.pages[currentPageId].shapes const prevGeneratedShapes = Object.values(currentShapes).filter(
(shape) => shape.isGenerated
)
// Remove previous generated shapes // Remove previous generated shapes
for (let id in currentShapes) { for (let id in currentShapes) {
@ -34,7 +36,7 @@ export default function generateCommand(
name: "translate_shapes", name: "translate_shapes",
category: "canvas", category: "canvas",
do(data) { do(data) {
const { shapes } = data.document.pages[currentPageId] const { shapes } = getPage(data)
data.selectedIds.clear() data.selectedIds.clear()
@ -51,7 +53,7 @@ export default function generateCommand(
} }
}, },
undo(data) { undo(data) {
const { shapes } = data.document.pages[currentPageId] const { shapes } = getPage(data)
// Remove generated shapes // Remove generated shapes
for (let id in shapes) { for (let id in shapes) {

View file

@ -2,6 +2,7 @@ import Command from "./command"
import history from "../history" import history from "../history"
import { Data } from "types" import { Data } from "types"
import { RotateSnapshot } from "state/sessions/rotate-session" import { RotateSnapshot } from "state/sessions/rotate-session"
import { getPage } from "utils/utils"
export default function rotateCommand( export default function rotateCommand(
data: Data, data: Data,
@ -14,7 +15,7 @@ export default function rotateCommand(
name: "translate_shapes", name: "translate_shapes",
category: "canvas", category: "canvas",
do(data) { do(data) {
const { shapes } = data.document.pages[after.currentPageId] const { shapes } = getPage(data)
for (let { id, point, rotation } of after.shapes) { for (let { id, point, rotation } of after.shapes) {
const shape = shapes[id] const shape = shapes[id]
@ -25,7 +26,7 @@ export default function rotateCommand(
data.boundsRotation = after.boundsRotation data.boundsRotation = after.boundsRotation
}, },
undo(data) { undo(data) {
const { shapes } = data.document.pages[before.currentPageId] const { shapes } = getPage(data, before.currentPageId)
for (let { id, point, rotation } of before.shapes) { for (let { id, point, rotation } of before.shapes) {
const shape = shapes[id] const shape = shapes[id]

View file

@ -1,9 +1,10 @@
import Command from "./command" import Command from "./command"
import history from "../history" import history from "../history"
import { Data, TransformCorner, TransformEdge } from "types" import { Data, Corner, Edge } from "types"
import { getShapeUtils } from "lib/shape-utils" import { getShapeUtils } from "lib/shape-utils"
import { current } from "immer" import { current } from "immer"
import { TransformSingleSnapshot } from "state/sessions/transform-single-session" import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
import { getPage } from "utils/utils"
export default function transformSingleCommand( export default function transformSingleCommand(
data: Data, data: Data,
@ -13,8 +14,7 @@ export default function transformSingleCommand(
scaleY: number, scaleY: number,
isCreating: boolean isCreating: boolean
) { ) {
const shape = const shape = getPage(data, after.currentPageId).shapes[after.id]
current(data).document.pages[after.currentPageId].shapes[after.id]
history.execute( history.execute(
data, data,
@ -23,32 +23,36 @@ export default function transformSingleCommand(
category: "canvas", category: "canvas",
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const { id, currentPageId, type, initialShape, initialShapeBounds } = const { id, type, initialShape, initialShapeBounds } = after
after
const { shapes } = getPage(data, after.currentPageId)
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(id) data.selectedIds.add(id)
if (isCreating) { if (isCreating) {
data.document.pages[currentPageId].shapes[id] = shape shapes[id] = shape
} else { } else {
getShapeUtils(shape).transformSingle(shape, initialShapeBounds, { getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
type, type,
initialShape, initialShape,
scaleX, scaleX,
scaleY, scaleY,
transformOrigin: [0.5, 0.5],
}) })
} }
}, },
undo(data) { undo(data) {
const { id, currentPageId, type, initialShapeBounds } = before const { id, type, initialShapeBounds } = before
const { shapes } = getPage(data, before.currentPageId)
data.selectedIds.clear() data.selectedIds.clear()
if (isCreating) { if (isCreating) {
delete data.document.pages[currentPageId].shapes[id] delete shapes[id]
} else { } else {
const shape = data.document.pages[currentPageId].shapes[id] const shape = shapes[id]
data.selectedIds.add(id) data.selectedIds.add(id)
getShapeUtils(shape).transform(shape, initialShapeBounds, { getShapeUtils(shape).transform(shape, initialShapeBounds, {
@ -56,6 +60,7 @@ export default function transformSingleCommand(
initialShape: after.initialShape, initialShape: after.initialShape,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
transformOrigin: [0.5, 0.5],
}) })
} }
}, },

View file

@ -1,8 +1,9 @@
import Command from "./command" import Command from "./command"
import history from "../history" import history from "../history"
import { Data, TransformCorner, TransformEdge } from "types" import { Data, Corner, Edge } from "types"
import { TransformSnapshot } from "state/sessions/transform-session" import { TransformSnapshot } from "state/sessions/transform-session"
import { getShapeUtils } from "lib/shape-utils" import { getShapeUtils } from "lib/shape-utils"
import { getPage } from "utils/utils"
export default function transformCommand( export default function transformCommand(
data: Data, data: Data,
@ -17,32 +18,40 @@ export default function transformCommand(
name: "translate_shapes", name: "translate_shapes",
category: "canvas", category: "canvas",
do(data) { do(data) {
const { type, currentPageId, selectedIds } = after const { type, selectedIds } = after
const { shapes } = getPage(data)
selectedIds.forEach((id) => { selectedIds.forEach((id) => {
const { initialShape, initialShapeBounds } = after.shapeBounds[id] const { initialShape, initialShapeBounds, transformOrigin } =
const shape = data.document.pages[currentPageId].shapes[id] after.shapeBounds[id]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, { getShapeUtils(shape).transform(shape, initialShapeBounds, {
type, type,
initialShape, initialShape,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
transformOrigin,
}) })
}) })
}, },
undo(data) { undo(data) {
const { type, currentPageId, selectedIds } = before const { type, selectedIds } = before
const { shapes } = getPage(data)
selectedIds.forEach((id) => { selectedIds.forEach((id) => {
const { initialShape, initialShapeBounds } = before.shapeBounds[id] const { initialShape, initialShapeBounds, transformOrigin } =
const shape = data.document.pages[currentPageId].shapes[id] before.shapeBounds[id]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, { getShapeUtils(shape).transform(shape, initialShapeBounds, {
type, type,
initialShape, initialShape,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
transformOrigin,
}) })
}) })
}, },

View file

@ -2,6 +2,7 @@ import Command from "./command"
import history from "../history" import history from "../history"
import { TranslateSnapshot } from "state/sessions/translate-session" import { TranslateSnapshot } from "state/sessions/translate-session"
import { Data } from "types" import { Data } from "types"
import { getPage } from "utils/utils"
export default function translateCommand( export default function translateCommand(
data: Data, data: Data,
@ -18,8 +19,8 @@ export default function translateCommand(
do(data, initial) { do(data, initial) {
if (initial) return if (initial) return
const { shapes } = data.document.pages[after.currentPageId] const { initialShapes, currentPageId } = after
const { initialShapes } = after const { shapes } = getPage(data, currentPageId)
const { clones } = before // ! const { clones } = before // !
data.selectedIds.clear() data.selectedIds.clear()
@ -36,8 +37,8 @@ export default function translateCommand(
} }
}, },
undo(data) { undo(data) {
const { shapes } = data.document.pages[before.currentPageId] const { initialShapes, clones, currentPageId } = before
const { initialShapes, clones } = before const { shapes } = getPage(data, currentPageId)
data.selectedIds.clear() data.selectedIds.clear()

View file

@ -1,15 +1,10 @@
import { current } from "immer" import { current } from "immer"
import { ShapeUtil, Bounds, Data, Shapes } from "types" import { Bounds, Data } from "types"
import BaseSession from "./base-session" import BaseSession from "./base-session"
import shapes, { getShapeUtils } from "lib/shape-utils" import { getShapeUtils } from "lib/shape-utils"
import { getBoundsFromPoints } from "utils/utils" import { getBoundsFromPoints, getShapes } from "utils/utils"
import * as vec from "utils/vec" import * as vec from "utils/vec"
interface BrushSnapshot {
selectedIds: Set<string>
shapes: { id: string; test: (bounds: Bounds) => boolean }[]
}
export default class BrushSession extends BaseSession { export default class BrushSession extends BaseSession {
origin: number[] origin: number[]
snapshot: BrushSnapshot snapshot: BrushSnapshot
@ -19,7 +14,7 @@ export default class BrushSession extends BaseSession {
this.origin = vec.round(point) this.origin = vec.round(point)
this.snapshot = BrushSession.getSnapshot(data) this.snapshot = getBrushSnapshot(data)
} }
update = (data: Data, point: number[]) => { update = (data: Data, point: number[]) => {
@ -27,7 +22,8 @@ export default class BrushSession extends BaseSession {
const brushBounds = getBoundsFromPoints([origin, point]) const brushBounds = getBoundsFromPoints([origin, point])
for (let { test, id } of snapshot.shapes) { for (let id in snapshot.shapeHitTests) {
const test = snapshot.shapeHitTests[id]
if (test(brushBounds)) { if (test(brushBounds)) {
data.selectedIds.add(id) data.selectedIds.add(id)
} else if (data.selectedIds.has(id)) { } else if (data.selectedIds.has(id)) {
@ -46,30 +42,23 @@ export default class BrushSession extends BaseSession {
complete = (data: Data) => { complete = (data: Data) => {
data.brush = undefined data.brush = undefined
} }
}
/** /**
* Get a snapshot of the current selected ids, for each shape that is * Get a snapshot of the current selected ids, for each shape that is
* not already selected, the shape's id and a test to see whether the * not already selected, the shape's id and a test to see whether the
* brush will intersect that shape. For tests, start broad -> fine. * brush will intersect that shape. For tests, start broad -> fine.
* @param data
* @returns
*/ */
static getSnapshot(data: Data): BrushSnapshot { export function getBrushSnapshot(data: Data) {
const {
selectedIds,
document: { pages },
currentPageId,
} = current(data)
return { return {
selectedIds: new Set(data.selectedIds), selectedIds: new Set(data.selectedIds),
shapes: Object.values(pages[currentPageId].shapes) shapeHitTests: Object.fromEntries(
.filter((shape) => !selectedIds.has(shape.id)) getShapes(current(data)).map((shape) => [
.map((shape) => ({ shape.id,
id: shape.id, (bounds: Bounds) => getShapeUtils(shape).hitTestBounds(shape, bounds),
test: (brushBounds: Bounds): boolean => ])
getShapeUtils(shape).hitTestBounds(shape, brushBounds), ),
})),
}
} }
} }
export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>

View file

@ -3,6 +3,7 @@ import * as vec from "utils/vec"
import BaseSession from "./base-session" import BaseSession from "./base-session"
import commands from "state/commands" import commands from "state/commands"
import { current } from "immer" import { current } from "immer"
import { getPage } from "utils/utils"
export default class DirectionSession extends BaseSession { export default class DirectionSession extends BaseSession {
delta = [0, 0] delta = [0, 0]
@ -16,26 +17,22 @@ export default class DirectionSession extends BaseSession {
} }
update(data: Data, point: number[]) { update(data: Data, point: number[]) {
const { currentPageId, shapes } = this.snapshot const { shapes } = this.snapshot
const { document } = data
const page = getPage(data)
for (let { id } of shapes) { for (let { id } of shapes) {
const shape = document.pages[currentPageId].shapes[id] as const shape = page.shapes[id] as RayShape | LineShape
| RayShape
| LineShape
shape.direction = vec.uni(vec.vec(shape.point, point)) shape.direction = vec.uni(vec.vec(shape.point, point))
} }
} }
cancel(data: Data) { cancel(data: Data) {
const { document } = data const page = getPage(data, this.snapshot.currentPageId)
for (let { id, direction } of this.snapshot.shapes) { for (let { id, direction } of this.snapshot.shapes) {
const shape = document.pages[this.snapshot.currentPageId].shapes[id] as const shape = page.shapes[id] as RayShape | LineShape
| RayShape
| LineShape
shape.direction = direction shape.direction = direction
} }
} }
@ -46,12 +43,7 @@ export default class DirectionSession extends BaseSession {
} }
export function getDirectionSnapshot(data: Data) { export function getDirectionSnapshot(data: Data) {
const { const { shapes } = getPage(current(data))
document: { pages },
currentPageId,
} = current(data)
const { shapes } = pages[currentPageId]
let snapshapes: { id: string; direction: number[] }[] = [] let snapshapes: { id: string; direction: number[] }[] = []
@ -63,7 +55,7 @@ export function getDirectionSnapshot(data: Data) {
}) })
return { return {
currentPageId, currentPageId: data.currentPageId,
shapes: snapshapes, shapes: snapshapes,
} }
} }

View file

@ -3,8 +3,15 @@ import * as vec from "utils/vec"
import BaseSession from "./base-session" import BaseSession from "./base-session"
import commands from "state/commands" import commands from "state/commands"
import { current } from "immer" import { current } from "immer"
import { getCommonBounds } from "utils/utils" import {
import { getShapeUtils } from "lib/shape-utils" getBoundsCenter,
getCommonBounds,
getPage,
getSelectedShapes,
getShapeBounds,
} from "utils/utils"
const PI2 = Math.PI * 2
export default class RotateSession extends BaseSession { export default class RotateSession extends BaseSession {
delta = [0, 0] delta = [0, 0]
@ -17,33 +24,34 @@ export default class RotateSession extends BaseSession {
this.snapshot = getRotateSnapshot(data) this.snapshot = getRotateSnapshot(data)
} }
update(data: Data, point: number[]) { update(data: Data, point: number[], isLocked: boolean) {
const { currentPageId, boundsCenter, shapes } = this.snapshot const { boundsCenter, shapes } = this.snapshot
const { document } = data
const page = getPage(data)
const a1 = vec.angle(boundsCenter, this.origin) const a1 = vec.angle(boundsCenter, this.origin)
const a2 = vec.angle(boundsCenter, point) const a2 = vec.angle(boundsCenter, point)
data.boundsRotation = let rot = (PI2 + (a2 - a1)) % PI2
(this.snapshot.boundsRotation + (a2 - a1)) % (Math.PI * 2)
if (isLocked) {
rot = Math.floor((rot + Math.PI / 8) / (Math.PI / 4)) * (Math.PI / 4)
}
data.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2
for (let { id, center, offset, rotation } of shapes) { for (let { id, center, offset, rotation } of shapes) {
const shape = document.pages[currentPageId].shapes[id] const shape = page.shapes[id]
shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2)) shape.rotation = (PI2 + (rotation + rot)) % PI2
const newCenter = vec.rotWith( const newCenter = vec.rotWith(center, boundsCenter, rot % PI2)
center,
boundsCenter,
(a2 - a1) % (Math.PI * 2)
)
shape.point = vec.sub(newCenter, offset) shape.point = vec.sub(newCenter, offset)
} }
} }
cancel(data: Data) { cancel(data: Data) {
const { document } = data const page = getPage(data, this.snapshot.currentPageId)
for (let { id, point, rotation } of this.snapshot.shapes) { for (let { id, point, rotation } of this.snapshot.shapes) {
const shape = document.pages[this.snapshot.currentPageId].shapes[id] const shape = page.shapes[id]
shape.rotation = rotation shape.rotation = rotation
shape.point = point shape.point = point
} }
@ -55,38 +63,26 @@ export default class RotateSession extends BaseSession {
} }
export function getRotateSnapshot(data: Data) { export function getRotateSnapshot(data: Data) {
const { const shapes = getSelectedShapes(current(data))
boundsRotation,
selectedIds,
currentPageId,
document: { pages },
} = current(data)
const shapes = Array.from(selectedIds.values()).map(
(id) => pages[currentPageId].shapes[id]
)
// A mapping of selected shapes and their bounds // A mapping of selected shapes and their bounds
const shapesBounds = Object.fromEntries( const shapesBounds = Object.fromEntries(
shapes.map((shape) => [shape.id, getShapeUtils(shape).getBounds(shape)]) shapes.map((shape) => [shape.id, getShapeBounds(shape)])
) )
// The common (exterior) bounds of the selected shapes // The common (exterior) bounds of the selected shapes
const bounds = getCommonBounds(...Object.values(shapesBounds)) const bounds = getCommonBounds(...Object.values(shapesBounds))
const boundsCenter = [ const boundsCenter = getBoundsCenter(bounds)
bounds.minX + bounds.width / 2,
bounds.minY + bounds.height / 2,
]
return { return {
currentPageId,
boundsCenter, boundsCenter,
boundsRotation, currentPageId: data.currentPageId,
boundsRotation: data.boundsRotation,
shapes: shapes.map(({ id, point, rotation }) => { shapes: shapes.map(({ id, point, rotation }) => {
const bounds = shapesBounds[id] const bounds = shapesBounds[id]
const offset = [bounds.width / 2, bounds.height / 2] const offset = [bounds.width / 2, bounds.height / 2]
const center = vec.add(offset, [bounds.minX, bounds.minY]) const center = getBoundsCenter(bounds)
return { return {
id, id,

View file

@ -1,25 +1,29 @@
import { Data, TransformEdge, TransformCorner } from "types" import { Data, Edge, Corner } from "types"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import BaseSession from "./base-session" import BaseSession from "./base-session"
import commands from "state/commands" import commands from "state/commands"
import { current } from "immer" import { current } from "immer"
import { getShapeUtils } from "lib/shape-utils" import { getShapeUtils } from "lib/shape-utils"
import { import {
getBoundsCenter,
getBoundsFromPoints,
getCommonBounds, getCommonBounds,
getPage,
getRelativeTransformedBoundingBox, getRelativeTransformedBoundingBox,
getShapes,
getTransformedBoundingBox, getTransformedBoundingBox,
} from "utils/utils" } from "utils/utils"
export default class TransformSession extends BaseSession { export default class TransformSession extends BaseSession {
scaleX = 1 scaleX = 1
scaleY = 1 scaleY = 1
transformType: TransformEdge | TransformCorner transformType: Edge | Corner | "center"
origin: number[] origin: number[]
snapshot: TransformSnapshot snapshot: TransformSnapshot
constructor( constructor(
data: Data, data: Data,
transformType: TransformCorner | TransformEdge, transformType: Corner | Edge | "center",
point: number[] point: number[]
) { ) {
super(data) super(data)
@ -31,8 +35,9 @@ export default class TransformSession extends BaseSession {
update(data: Data, point: number[], isAspectRatioLocked = false) { update(data: Data, point: number[], isAspectRatioLocked = false) {
const { transformType } = this const { transformType } = this
const { currentPageId, selectedIds, shapeBounds, initialBounds } = const { selectedIds, shapeBounds, initialBounds } = this.snapshot
this.snapshot
const { shapes } = getPage(data)
const newBoundingBox = getTransformedBoundingBox( const newBoundingBox = getTransformedBoundingBox(
initialBounds, initialBounds,
@ -48,7 +53,8 @@ export default class TransformSession extends BaseSession {
// Now work backward to calculate a new bounding box for each of the shapes. // Now work backward to calculate a new bounding box for each of the shapes.
selectedIds.forEach((id) => { selectedIds.forEach((id) => {
const { initialShape, initialShapeBounds } = shapeBounds[id] const { initialShape, initialShapeBounds, transformOrigin } =
shapeBounds[id]
const newShapeBounds = getRelativeTransformedBoundingBox( const newShapeBounds = getRelativeTransformedBoundingBox(
newBoundingBox, newBoundingBox,
@ -58,13 +64,27 @@ export default class TransformSession extends BaseSession {
this.scaleY < 0 this.scaleY < 0
) )
const shape = data.document.pages[currentPageId].shapes[id] const shape = shapes[id]
// const transformOrigins = {
// [Edge.Top]: [0.5, 1],
// [Edge.Right]: [0, 0.5],
// [Edge.Bottom]: [0.5, 0],
// [Edge.Left]: [1, 0.5],
// [Corner.TopLeft]: [1, 1],
// [Corner.TopRight]: [0, 1],
// [Corner.BottomLeft]: [1, 0],
// [Corner.BottomRight]: [0, 0],
// }
// const origin = transformOrigins[this.transformType]
getShapeUtils(shape).transform(shape, newShapeBounds, { getShapeUtils(shape).transform(shape, newShapeBounds, {
type: this.transformType, type: this.transformType,
initialShape, initialShape,
scaleX: this.scaleX, scaleX: this.scaleX,
scaleY: this.scaleY, scaleY: this.scaleY,
transformOrigin,
}) })
}) })
} }
@ -72,16 +92,20 @@ export default class TransformSession extends BaseSession {
cancel(data: Data) { cancel(data: Data) {
const { currentPageId, selectedIds, shapeBounds } = this.snapshot const { currentPageId, selectedIds, shapeBounds } = this.snapshot
selectedIds.forEach((id) => { const page = getPage(data, currentPageId)
const shape = data.document.pages[currentPageId].shapes[id]
const { initialShape, initialShapeBounds } = shapeBounds[id] selectedIds.forEach((id) => {
const shape = page.shapes[id]
const { initialShape, initialShapeBounds, transformOrigin } =
shapeBounds[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, { getShapeUtils(shape).transform(shape, initialShapeBounds, {
type: this.transformType, type: this.transformType,
initialShape, initialShape,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
transformOrigin,
}) })
}) })
} }
@ -99,7 +123,7 @@ export default class TransformSession extends BaseSession {
export function getTransformSnapshot( export function getTransformSnapshot(
data: Data, data: Data,
transformType: TransformEdge | TransformCorner transformType: Edge | Corner | "center"
) { ) {
const { const {
document: { pages }, document: { pages },
@ -117,8 +141,12 @@ export function getTransformSnapshot(
}) })
) )
const boundsArr = Object.values(shapesBounds)
// The common (exterior) bounds of the selected shapes // The common (exterior) bounds of the selected shapes
const bounds = getCommonBounds(...Object.values(shapesBounds)) const bounds = getCommonBounds(...boundsArr)
const initialInnerBounds = getBoundsFromPoints(boundsArr.map(getBoundsCenter))
// Return a mapping of shapes to bounds together with the relative // Return a mapping of shapes to bounds together with the relative
// positions of the shape's bounds within the common bounds shape. // positions of the shape's bounds within the common bounds shape.
@ -129,11 +157,18 @@ export function getTransformSnapshot(
initialBounds: bounds, initialBounds: bounds,
shapeBounds: Object.fromEntries( shapeBounds: Object.fromEntries(
Array.from(selectedIds.values()).map((id) => { Array.from(selectedIds.values()).map((id) => {
const initialShapeBounds = shapesBounds[id]
const ic = getBoundsCenter(initialShapeBounds)
let ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
let iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
return [ return [
id, id,
{ {
initialShape: pageShapes[id], initialShape: pageShapes[id],
initialShapeBounds: shapesBounds[id], initialShapeBounds,
transformOrigin: [ix, iy],
}, },
] ]
}) })

View file

@ -1,4 +1,4 @@
import { Data, TransformEdge, TransformCorner } from "types" import { Data, Edge, Corner } from "types"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import BaseSession from "./base-session" import BaseSession from "./base-session"
import commands from "state/commands" import commands from "state/commands"
@ -9,10 +9,13 @@ import {
getCommonBounds, getCommonBounds,
getRotatedCorners, getRotatedCorners,
getTransformAnchor, getTransformAnchor,
getPage,
getShape,
getSelectedShapes,
} from "utils/utils" } from "utils/utils"
export default class TransformSingleSession extends BaseSession { export default class TransformSingleSession extends BaseSession {
transformType: TransformEdge | TransformCorner transformType: Edge | Corner
origin: number[] origin: number[]
scaleX = 1 scaleX = 1
scaleY = 1 scaleY = 1
@ -21,7 +24,7 @@ export default class TransformSingleSession extends BaseSession {
constructor( constructor(
data: Data, data: Data,
transformType: TransformCorner | TransformEdge, transformType: Corner | Edge,
point: number[], point: number[],
isCreating = false isCreating = false
) { ) {
@ -38,7 +41,7 @@ export default class TransformSingleSession extends BaseSession {
const { initialShapeBounds, currentPageId, initialShape, id } = const { initialShapeBounds, currentPageId, initialShape, id } =
this.snapshot this.snapshot
const shape = data.document.pages[currentPageId].shapes[id] const shape = getShape(data, id, currentPageId)
const newBoundingBox = getTransformedBoundingBox( const newBoundingBox = getTransformedBoundingBox(
initialShapeBounds, initialShapeBounds,
@ -56,6 +59,7 @@ export default class TransformSingleSession extends BaseSession {
type: this.transformType, type: this.transformType,
scaleX: this.scaleX, scaleX: this.scaleX,
scaleY: this.scaleY, scaleY: this.scaleY,
transformOrigin: [0.5, 0.5],
}) })
} }
@ -63,15 +67,14 @@ export default class TransformSingleSession extends BaseSession {
const { id, initialShape, initialShapeBounds, currentPageId } = const { id, initialShape, initialShapeBounds, currentPageId } =
this.snapshot this.snapshot
const { shapes } = data.document.pages[currentPageId] const shape = getShape(data, id, currentPageId)
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, { getShapeUtils(shape).transform(shape, initialShapeBounds, {
initialShape, initialShape,
type: this.transformType, type: this.transformType,
scaleX: this.scaleX, scaleX: this.scaleX,
scaleY: this.scaleY, scaleY: this.scaleY,
transformOrigin: [0.5, 0.5],
}) })
} }
@ -89,21 +92,14 @@ export default class TransformSingleSession extends BaseSession {
export function getTransformSingleSnapshot( export function getTransformSingleSnapshot(
data: Data, data: Data,
transformType: TransformEdge | TransformCorner transformType: Edge | Corner
) { ) {
const { const shape = getSelectedShapes(current(data))[0]
document: { pages },
selectedIds,
currentPageId,
} = current(data)
const id = Array.from(selectedIds)[0]
const shape = pages[currentPageId].shapes[id]
const bounds = getShapeUtils(shape).getBounds(shape) const bounds = getShapeUtils(shape).getBounds(shape)
return { return {
id, id: shape.id,
currentPageId, currentPageId: data.currentPageId,
type: transformType, type: transformType,
initialShape: shape, initialShape: shape,
initialShapeBounds: bounds, initialShapeBounds: bounds,

View file

@ -4,6 +4,7 @@ import BaseSession from "./base-session"
import commands from "state/commands" import commands from "state/commands"
import { current } from "immer" import { current } from "immer"
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import { getPage, getSelectedShapes } from "utils/utils"
export default class TranslateSession extends BaseSession { export default class TranslateSession extends BaseSession {
delta = [0, 0] delta = [0, 0]
@ -19,7 +20,7 @@ export default class TranslateSession extends BaseSession {
update(data: Data, point: number[], isAligned: boolean, isCloning: boolean) { update(data: Data, point: number[], isAligned: boolean, isCloning: boolean) {
const { currentPageId, clones, initialShapes } = this.snapshot const { currentPageId, clones, initialShapes } = this.snapshot
const { shapes } = data.document.pages[currentPageId] const { shapes } = getPage(data, currentPageId)
const delta = vec.vec(this.origin, point) const delta = vec.vec(this.origin, point)
@ -71,7 +72,7 @@ export default class TranslateSession extends BaseSession {
cancel(data: Data) { cancel(data: Data) {
const { initialShapes, clones, currentPageId } = this.snapshot const { initialShapes, clones, currentPageId } = this.snapshot
const { shapes } = data.document.pages[currentPageId] const { shapes } = getPage(data, currentPageId)
for (const { id, point } of initialShapes) { for (const { id, point } of initialShapes) {
shapes[id].point = point shapes[id].point = point
@ -93,14 +94,10 @@ export default class TranslateSession extends BaseSession {
} }
export function getTranslateSnapshot(data: Data) { export function getTranslateSnapshot(data: Data) {
const { document, selectedIds, currentPageId } = current(data) const shapes = getSelectedShapes(current(data))
const shapes = Array.from(selectedIds.values()).map(
(id) => document.pages[currentPageId].shapes[id]
)
return { return {
currentPageId, currentPageId: data.currentPageId,
initialShapes: shapes.map(({ id, point }) => ({ id, point })), initialShapes: shapes.map(({ id, point }) => ({ id, point })),
clones: shapes.map((shape) => ({ ...shape, id: uuid() })), clones: shapes.map((shape) => ({ ...shape, id: uuid() })),
} }

View file

@ -1,13 +1,19 @@
import { createSelectorHook, createState } from "@state-designer/react" import { createSelectorHook, createState } from "@state-designer/react"
import { clamp, getCommonBounds, screenToWorld } from "utils/utils" import {
clamp,
getCommonBounds,
getPage,
getShape,
screenToWorld,
} from "utils/utils"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { import {
Data, Data,
PointerInfo, PointerInfo,
Shape, Shape,
ShapeType, ShapeType,
TransformCorner, Corner,
TransformEdge, Edge,
CodeControl, CodeControl,
} from "types" } from "types"
import inputs from "./inputs" import inputs from "./inputs"
@ -99,9 +105,11 @@ const state = createState({
SELECTED_ALL: "selectAll", SELECTED_ALL: "selectAll",
POINTED_CANVAS: { to: "brushSelecting" }, POINTED_CANVAS: { to: "brushSelecting" },
POINTED_BOUNDS: { to: "pointingBounds" }, POINTED_BOUNDS: { to: "pointingBounds" },
POINTED_BOUNDS_EDGE: { to: "transformingSelection" }, POINTED_BOUNDS_HANDLE: {
POINTED_BOUNDS_CORNER: { to: "transformingSelection" }, if: "isPointingRotationHandle",
POINTED_ROTATE_HANDLE: { to: "rotatingSelection" }, to: "rotatingSelection",
else: { to: "transformingSelection" },
},
MOVED_OVER_SHAPE: { MOVED_OVER_SHAPE: {
if: "pointHitsShape", if: "pointHitsShape",
then: { then: {
@ -156,9 +164,12 @@ const state = createState({
}, },
rotatingSelection: { rotatingSelection: {
onEnter: "startRotateSession", onEnter: "startRotateSession",
onExit: "clearBoundsRotation",
on: { on: {
MOVED_POINTER: "updateRotateSession", MOVED_POINTER: "updateRotateSession",
PANNED_CAMERA: "updateRotateSession", PANNED_CAMERA: "updateRotateSession",
PRESSED_SHIFT_KEY: "keyUpdateRotateSession",
RELEASED_SHIFT_KEY: "keyUpdateRotateSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" }, STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" }, CANCELLED: { do: "cancelSession", to: "selecting" },
}, },
@ -420,14 +431,19 @@ const state = createState({
return data.hoveredId === payload.target return data.hoveredId === payload.target
}, },
pointHitsShape(data, payload: { target: string; point: number[] }) { pointHitsShape(data, payload: { target: string; point: number[] }) {
const shape = const shape = getShape(data, payload.target)
data.document.pages[data.currentPageId].shapes[payload.target]
return getShapeUtils(shape).hitTest( return getShapeUtils(shape).hitTest(
shape, shape,
screenToWorld(payload.point, data) screenToWorld(payload.point, data)
) )
}, },
isPointingRotationHandle(
data,
payload: { target: Edge | Corner | "rotate" }
) {
return payload.target === "rotate"
},
}, },
actions: { actions: {
/* --------------------- Shapes --------------------- */ /* --------------------- Shapes --------------------- */
@ -438,7 +454,7 @@ const state = createState({
point: screenToWorld(payload.point, data), point: screenToWorld(payload.point, data),
}) })
data.document.pages[data.currentPageId].shapes[shape.id] = shape getPage(data).shapes[shape.id] = shape
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(shape.id) data.selectedIds.add(shape.id)
}, },
@ -449,7 +465,7 @@ const state = createState({
point: screenToWorld(payload.point, data), point: screenToWorld(payload.point, data),
}) })
data.document.pages[data.currentPageId].shapes[shape.id] = shape getPage(data).shapes[shape.id] = shape
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(shape.id) data.selectedIds.add(shape.id)
}, },
@ -461,7 +477,7 @@ const state = createState({
direction: [0, 1], direction: [0, 1],
}) })
data.document.pages[data.currentPageId].shapes[shape.id] = shape getPage(data).shapes[shape.id] = shape
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(shape.id) data.selectedIds.add(shape.id)
}, },
@ -472,7 +488,7 @@ const state = createState({
radius: 1, radius: 1,
}) })
data.document.pages[data.currentPageId].shapes[shape.id] = shape getPage(data).shapes[shape.id] = shape
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(shape.id) data.selectedIds.add(shape.id)
}, },
@ -484,7 +500,7 @@ const state = createState({
radiusY: 1, radiusY: 1,
}) })
data.document.pages[data.currentPageId].shapes[shape.id] = shape getPage(data).shapes[shape.id] = shape
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(shape.id) data.selectedIds.add(shape.id)
}, },
@ -495,7 +511,7 @@ const state = createState({
size: [1, 1], size: [1, 1],
}) })
data.document.pages[data.currentPageId].shapes[shape.id] = shape getPage(data).shapes[shape.id] = shape
data.selectedIds.clear() data.selectedIds.clear()
data.selectedIds.add(shape.id) data.selectedIds.add(shape.id)
}, },
@ -529,8 +545,15 @@ const state = createState({
screenToWorld(payload.point, data) screenToWorld(payload.point, data)
) )
}, },
keyUpdateRotateSession(data, payload: PointerInfo) {
session.update(
data,
screenToWorld(inputs.pointer.point, data),
payload.shiftKey
)
},
updateRotateSession(data, payload: PointerInfo) { updateRotateSession(data, payload: PointerInfo) {
session.update(data, screenToWorld(payload.point, data)) session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
}, },
// Dragging / Translating // Dragging / Translating
@ -564,7 +587,7 @@ const state = createState({
// Dragging / Translating // Dragging / Translating
startTransformSession( startTransformSession(
data, data,
payload: PointerInfo & { target: TransformCorner | TransformEdge } payload: PointerInfo & { target: Corner | Edge }
) { ) {
session = session =
data.selectedIds.size === 1 data.selectedIds.size === 1
@ -583,7 +606,7 @@ const state = createState({
startDrawTransformSession(data, payload: PointerInfo) { startDrawTransformSession(data, payload: PointerInfo) {
session = new Sessions.TransformSingleSession( session = new Sessions.TransformSingleSession(
data, data,
TransformCorner.BottomRight, Corner.BottomRight,
screenToWorld(payload.point, data), screenToWorld(payload.point, data),
true true
) )
@ -619,9 +642,10 @@ const state = createState({
/* -------------------- Selection ------------------- */ /* -------------------- Selection ------------------- */
selectAll(data) { selectAll(data) {
const { selectedIds, document, currentPageId } = data const { selectedIds } = data
const page = getPage(data)
selectedIds.clear() selectedIds.clear()
for (let id in document.pages[currentPageId].shapes) { for (let id in page.shapes) {
selectedIds.add(id) selectedIds.add(id)
} }
}, },
@ -654,6 +678,14 @@ const state = createState({
document.documentElement.style.setProperty("--camera-zoom", "1") document.documentElement.style.setProperty("--camera-zoom", "1")
}, },
centerCamera(data) {
const { shapes } = getPage(data)
getCommonBounds()
data.camera.zoom = 1
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
document.documentElement.style.setProperty("--camera-zoom", "1")
},
zoomCamera(data, payload: { delta: number; point: number[] }) { zoomCamera(data, payload: { delta: number; point: number[] }) {
const { camera } = data const { camera } = data
const p0 = screenToWorld(payload.point, data) const p0 = screenToWorld(payload.point, data)
@ -678,18 +710,16 @@ const state = createState({
) )
}, },
deleteSelectedIds(data) { deleteSelectedIds(data) {
const { document, currentPageId } = data const page = getPage(data)
const { shapes } = document.pages[currentPageId]
data.hoveredId = undefined data.hoveredId = undefined
data.pointedId = undefined data.pointedId = undefined
data.selectedIds.forEach((id) => { data.selectedIds.forEach((id) => {
delete shapes[id] delete page.shapes[id]
// TODO: recursively delete children // TODO: recursively delete children
}) })
data.document.pages[currentPageId].shapes = shapes
data.selectedIds.clear() data.selectedIds.clear()
}, },
@ -784,14 +814,12 @@ const state = createState({
return new Set(data.selectedIds) return new Set(data.selectedIds)
}, },
selectedBounds(data) { selectedBounds(data) {
const { const { selectedIds } = data
selectedIds,
currentPageId, const page = getPage(data)
document: { pages },
} = data
const shapes = Array.from(selectedIds.values()) const shapes = Array.from(selectedIds.values())
.map((id) => pages[currentPageId].shapes[id]) .map((id) => page.shapes[id])
.filter(Boolean) .filter(Boolean)
if (selectedIds.size === 0) return null if (selectedIds.size === 0) return null

View file

@ -146,14 +146,14 @@ export interface PointerInfo {
altKey: boolean altKey: boolean
} }
export enum TransformEdge { export enum Edge {
Top = "top_edge", Top = "top_edge",
Right = "right_edge", Right = "right_edge",
Bottom = "bottom_edge", Bottom = "bottom_edge",
Left = "left_edge", Left = "left_edge",
} }
export enum TransformCorner { export enum Corner {
TopLeft = "top_left_corner", TopLeft = "top_left_corner",
TopRight = "top_right_corner", TopRight = "top_right_corner",
BottomRight = "bottom_right_corner", BottomRight = "bottom_right_corner",

View file

@ -1,9 +1,9 @@
import Vector from "lib/code/vector" import Vector from "lib/code/vector"
import { getShapeUtils } from "lib/shape-utils"
import React from "react" import React from "react"
import { Data, Bounds, TransformEdge, TransformCorner, Shape } from "types" import { Data, Bounds, Edge, Corner, Shape } from "types"
import * as svg from "./svg"
import * as vec from "./vec" import * as vec from "./vec"
import _isMobile from "ismobilejs"
import { getShapeUtils } from "lib/shape-utils"
export function screenToWorld(point: number[], data: Data) { export function screenToWorld(point: number[], data: Data) {
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point) return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
@ -42,7 +42,7 @@ export function getCommonBounds(...b: Bounds[]) {
return bounds return bounds
} }
// export function getBoundsFromPoints(a: number[], b: number[]) { // export function getBoundsFromTwoPoints(a: number[], b: number[]) {
// const minX = Math.min(a[0], b[0]) // const minX = Math.min(a[0], b[0])
// const maxX = Math.max(a[0], b[0]) // const maxX = Math.max(a[0], b[0])
// const minY = Math.min(a[1], b[1]) // const minY = Math.min(a[1], b[1])
@ -900,59 +900,59 @@ export function metaKey(e: KeyboardEvent | React.KeyboardEvent) {
} }
export function getTransformAnchor( export function getTransformAnchor(
type: TransformEdge | TransformCorner, type: Edge | Corner,
isFlippedX: boolean, isFlippedX: boolean,
isFlippedY: boolean isFlippedY: boolean
) { ) {
let anchor: TransformCorner | TransformEdge = type let anchor: Corner | Edge = type
// Change corner anchors if flipped // Change corner anchors if flipped
switch (type) { switch (type) {
case TransformCorner.TopLeft: { case Corner.TopLeft: {
if (isFlippedX && isFlippedY) { if (isFlippedX && isFlippedY) {
anchor = TransformCorner.BottomRight anchor = Corner.BottomRight
} else if (isFlippedX) { } else if (isFlippedX) {
anchor = TransformCorner.TopRight anchor = Corner.TopRight
} else if (isFlippedY) { } else if (isFlippedY) {
anchor = TransformCorner.BottomLeft anchor = Corner.BottomLeft
} else { } else {
anchor = TransformCorner.BottomRight anchor = Corner.BottomRight
} }
break break
} }
case TransformCorner.TopRight: { case Corner.TopRight: {
if (isFlippedX && isFlippedY) { if (isFlippedX && isFlippedY) {
anchor = TransformCorner.BottomLeft anchor = Corner.BottomLeft
} else if (isFlippedX) { } else if (isFlippedX) {
anchor = TransformCorner.TopLeft anchor = Corner.TopLeft
} else if (isFlippedY) { } else if (isFlippedY) {
anchor = TransformCorner.BottomRight anchor = Corner.BottomRight
} else { } else {
anchor = TransformCorner.BottomLeft anchor = Corner.BottomLeft
} }
break break
} }
case TransformCorner.BottomRight: { case Corner.BottomRight: {
if (isFlippedX && isFlippedY) { if (isFlippedX && isFlippedY) {
anchor = TransformCorner.TopLeft anchor = Corner.TopLeft
} else if (isFlippedX) { } else if (isFlippedX) {
anchor = TransformCorner.BottomLeft anchor = Corner.BottomLeft
} else if (isFlippedY) { } else if (isFlippedY) {
anchor = TransformCorner.TopRight anchor = Corner.TopRight
} else { } else {
anchor = TransformCorner.TopLeft anchor = Corner.TopLeft
} }
break break
} }
case TransformCorner.BottomLeft: { case Corner.BottomLeft: {
if (isFlippedX && isFlippedY) { if (isFlippedX && isFlippedY) {
anchor = TransformCorner.TopRight anchor = Corner.TopRight
} else if (isFlippedX) { } else if (isFlippedX) {
anchor = TransformCorner.BottomRight anchor = Corner.BottomRight
} else if (isFlippedY) { } else if (isFlippedY) {
anchor = TransformCorner.TopLeft anchor = Corner.TopLeft
} else { } else {
anchor = TransformCorner.TopRight anchor = Corner.TopRight
} }
break break
} }
@ -1030,6 +1030,18 @@ export function rotateBounds(
} }
} }
export function getRotatedSize(size: number[], rotation: number) {
const center = vec.div(size, 2)
const points = [[0, 0], [size[0], 0], size, [0, size[1]]].map((point) =>
vec.rotWith(point, center, rotation)
)
const bounds = getBoundsFromPoints(points)
return [bounds.width, bounds.height]
}
export function getRotatedCorners(b: Bounds, rotation: number) { export function getRotatedCorners(b: Bounds, rotation: number) {
const center = [b.minX + b.width / 2, b.minY + b.height / 2] const center = [b.minX + b.width / 2, b.minY + b.height / 2]
@ -1043,7 +1055,7 @@ export function getRotatedCorners(b: Bounds, rotation: number) {
export function getTransformedBoundingBox( export function getTransformedBoundingBox(
bounds: Bounds, bounds: Bounds,
handle: TransformCorner | TransformEdge | "center", handle: Corner | Edge | "center",
delta: number[], delta: number[],
rotation = 0, rotation = 0,
isAspectRatioLocked = false isAspectRatioLocked = false
@ -1082,30 +1094,30 @@ export function getTransformedBoundingBox(
corners should change. corners should change.
*/ */
switch (handle) { switch (handle) {
case TransformEdge.Top: case Edge.Top:
case TransformCorner.TopLeft: case Corner.TopLeft:
case TransformCorner.TopRight: { case Corner.TopRight: {
by0 += dy by0 += dy
break break
} }
case TransformEdge.Bottom: case Edge.Bottom:
case TransformCorner.BottomLeft: case Corner.BottomLeft:
case TransformCorner.BottomRight: { case Corner.BottomRight: {
by1 += dy by1 += dy
break break
} }
} }
switch (handle) { switch (handle) {
case TransformEdge.Left: case Edge.Left:
case TransformCorner.TopLeft: case Corner.TopLeft:
case TransformCorner.BottomLeft: { case Corner.BottomLeft: {
bx0 += dx bx0 += dx
break break
} }
case TransformEdge.Right: case Edge.Right:
case TransformCorner.TopRight: case Corner.TopRight:
case TransformCorner.BottomRight: { case Corner.BottomRight: {
bx1 += dx bx1 += dx
break break
} }
@ -1117,6 +1129,9 @@ export function getTransformedBoundingBox(
const scaleX = (bx1 - bx0) / aw const scaleX = (bx1 - bx0) / aw
const scaleY = (by1 - by0) / ah const scaleY = (by1 - by0) / ah
const flipX = scaleX < 0
const flipY = scaleY < 0
const bw = Math.abs(bx1 - bx0) const bw = Math.abs(bx1 - bx0)
const bh = Math.abs(by1 - by0) const bh = Math.abs(by1 - by0)
@ -1134,36 +1149,36 @@ export function getTransformedBoundingBox(
const th = bh * (scaleX < 0 ? 1 : -1) * ar const th = bh * (scaleX < 0 ? 1 : -1) * ar
switch (handle) { switch (handle) {
case TransformCorner.TopLeft: { case Corner.TopLeft: {
if (isTall) by0 = by1 + tw if (isTall) by0 = by1 + tw
else bx0 = bx1 + th else bx0 = bx1 + th
break break
} }
case TransformCorner.TopRight: { case Corner.TopRight: {
if (isTall) by0 = by1 + tw if (isTall) by0 = by1 + tw
else bx1 = bx0 - th else bx1 = bx0 - th
break break
} }
case TransformCorner.BottomRight: { case Corner.BottomRight: {
if (isTall) by1 = by0 - tw if (isTall) by1 = by0 - tw
else bx1 = bx0 - th else bx1 = bx0 - th
break break
} }
case TransformCorner.BottomLeft: { case Corner.BottomLeft: {
if (isTall) by1 = by0 - tw if (isTall) by1 = by0 - tw
else bx0 = bx1 + th else bx0 = bx1 + th
break break
} }
case TransformEdge.Bottom: case Edge.Bottom:
case TransformEdge.Top: { case Edge.Top: {
const m = (bx0 + bx1) / 2 const m = (bx0 + bx1) / 2
const w = bh * ar const w = bh * ar
bx0 = m - w / 2 bx0 = m - w / 2
bx1 = m + w / 2 bx1 = m + w / 2
break break
} }
case TransformEdge.Left: case Edge.Left:
case TransformEdge.Right: { case Edge.Right: {
const m = (by0 + by1) / 2 const m = (by0 + by1) / 2
const h = bw / ar const h = bw / ar
by0 = m - h / 2 by0 = m - h / 2
@ -1189,56 +1204,56 @@ export function getTransformedBoundingBox(
const c1 = vec.med([bx0, by0], [bx1, by1]) const c1 = vec.med([bx0, by0], [bx1, by1])
switch (handle) { switch (handle) {
case TransformCorner.TopLeft: { case Corner.TopLeft: {
cv = vec.sub( cv = vec.sub(
vec.rotWith([bx1, by1], c1, rotation), vec.rotWith([bx1, by1], c1, rotation),
vec.rotWith([ax1, ay1], c0, rotation) vec.rotWith([ax1, ay1], c0, rotation)
) )
break break
} }
case TransformCorner.TopRight: { case Corner.TopRight: {
cv = vec.sub( cv = vec.sub(
vec.rotWith([bx0, by1], c1, rotation), vec.rotWith([bx0, by1], c1, rotation),
vec.rotWith([ax0, ay1], c0, rotation) vec.rotWith([ax0, ay1], c0, rotation)
) )
break break
} }
case TransformCorner.BottomRight: { case Corner.BottomRight: {
cv = vec.sub( cv = vec.sub(
vec.rotWith([bx0, by0], c1, rotation), vec.rotWith([bx0, by0], c1, rotation),
vec.rotWith([ax0, ay0], c0, rotation) vec.rotWith([ax0, ay0], c0, rotation)
) )
break break
} }
case TransformCorner.BottomLeft: { case Corner.BottomLeft: {
cv = vec.sub( cv = vec.sub(
vec.rotWith([bx1, by0], c1, rotation), vec.rotWith([bx1, by0], c1, rotation),
vec.rotWith([ax1, ay0], c0, rotation) vec.rotWith([ax1, ay0], c0, rotation)
) )
break break
} }
case TransformEdge.Top: { case Edge.Top: {
cv = vec.sub( cv = vec.sub(
vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation), vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation) vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
) )
break break
} }
case TransformEdge.Left: { case Edge.Left: {
cv = vec.sub( cv = vec.sub(
vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation), vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation) vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
) )
break break
} }
case TransformEdge.Bottom: { case Edge.Bottom: {
cv = vec.sub( cv = vec.sub(
vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation), vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation) vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
) )
break break
} }
case TransformEdge.Right: { case Edge.Right: {
cv = vec.sub( cv = vec.sub(
vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation), vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation) vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
@ -1273,8 +1288,8 @@ export function getTransformedBoundingBox(
maxY: by1, maxY: by1,
width: bx1 - bx0, width: bx1 - bx0,
height: by1 - by0, height: by1 - by0,
scaleX, scaleX: ((bx1 - bx0) / (ax1 - ax0)) * (flipX ? -1 : 1),
scaleY, scaleY: ((by1 - by0) / (ay1 - ay0)) * (flipY ? -1 : 1),
} }
} }
@ -1285,25 +1300,23 @@ export function getRelativeTransformedBoundingBox(
isFlippedX: boolean, isFlippedX: boolean,
isFlippedY: boolean isFlippedY: boolean
) { ) {
const minX = const nx =
bounds.minX + (isFlippedX
bounds.width *
((isFlippedX
? initialBounds.maxX - initialShapeBounds.maxX ? initialBounds.maxX - initialShapeBounds.maxX
: initialShapeBounds.minX - initialBounds.minX) / : initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
initialBounds.width)
const minY = const ny =
bounds.minY + (isFlippedY
bounds.height *
((isFlippedY
? initialBounds.maxY - initialShapeBounds.maxY ? initialBounds.maxY - initialShapeBounds.maxY
: initialShapeBounds.minY - initialBounds.minY) / : initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
initialBounds.height)
const width = (initialShapeBounds.width / initialBounds.width) * bounds.width const nw = initialShapeBounds.width / initialBounds.width
const height = const nh = initialShapeBounds.height / initialBounds.height
(initialShapeBounds.height / initialBounds.height) * bounds.height
const minX = bounds.minX + bounds.width * nx
const minY = bounds.minY + bounds.height * ny
const width = bounds.width * nw
const height = bounds.height * nh
return { return {
minX, minX,
@ -1314,3 +1327,42 @@ export function getRelativeTransformedBoundingBox(
height, height,
} }
} }
export function getShape(
data: Data,
shapeId: string,
pageId = data.currentPageId
) {
return data.document.pages[pageId].shapes[shapeId]
}
export function getPage(data: Data, pageId = data.currentPageId) {
return data.document.pages[pageId]
}
export function getCurrentCode(data: Data, fileId = data.currentCodeFileId) {
return data.document.code[fileId]
}
export function getShapes(data: Data, pageId = data.currentPageId) {
const page = getPage(data, pageId)
return Object.values(page.shapes)
}
export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
const page = getPage(data, pageId)
const ids = Array.from(data.selectedIds.values())
return ids.map((id) => page.shapes[id])
}
export function isMobile() {
return _isMobile()
}
export function getShapeBounds(shape: Shape) {
return getShapeUtils(shape).getBounds(shape)
}
export function getBoundsCenter(bounds: Bounds) {
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
}

View file

@ -4154,6 +4154,11 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
ismobilejs@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ismobilejs/-/ismobilejs-1.1.1.tgz#c56ca0ae8e52b24ca0f22ba5ef3215a2ddbbaa0e"
integrity sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==
isobject@^2.0.0: isobject@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"