Basic z ordering
This commit is contained in:
parent
6582eb990c
commit
f11c35e941
20 changed files with 468 additions and 516 deletions
|
@ -56,6 +56,10 @@ const MainSVG = styled("svg", {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
|
|
||||||
|
"& *": {
|
||||||
|
userSelect: "none",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const MainGroup = styled("g", {})
|
const MainGroup = styled("g", {})
|
||||||
|
|
|
@ -9,10 +9,11 @@ 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 }) => {
|
||||||
({ data }) => Object.keys(getPage(data).shapes),
|
return Object.values(getPage(data).shapes)
|
||||||
deepCompareArrays
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
)
|
.map((shape) => shape.id)
|
||||||
|
}, deepCompareArrays)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useCallback, useRef, memo } from "react"
|
import React, { useCallback, useRef, memo } from "react"
|
||||||
import state, { useSelector } from "state"
|
import state, { useSelector } from "state"
|
||||||
import inputs from "state/inputs"
|
import inputs from "state/inputs"
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
|
||||||
import styled from "styles"
|
import styled from "styles"
|
||||||
|
import { getShapeUtils } from "lib/shape-utils"
|
||||||
import { getPage } from "utils/utils"
|
import { getPage } from "utils/utils"
|
||||||
|
|
||||||
function Shape({ id }: { id: string }) {
|
function Shape({ id }: { id: string }) {
|
||||||
|
|
|
@ -1,72 +1,136 @@
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import state from "state"
|
import state from "state"
|
||||||
import { getKeyboardEventInfo, isDarwin, metaKey } from "utils/utils"
|
import { getKeyboardEventInfo, metaKey } from "utils/utils"
|
||||||
|
|
||||||
export default function useKeyboardEvents() {
|
export default function useKeyboardEvents() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
if (e.key === "Escape") {
|
if (metaKey(e) && !["i", "r", "j"].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case "Escape": {
|
||||||
state.send("CANCELLED")
|
state.send("CANCELLED")
|
||||||
} else if (e.key === "z" && metaKey(e)) {
|
break
|
||||||
|
}
|
||||||
|
case "z": {
|
||||||
|
if (metaKey(e)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
state.send("REDO")
|
state.send("REDO", getKeyboardEventInfo(e))
|
||||||
} else {
|
} else {
|
||||||
state.send("UNDO")
|
state.send("UNDO", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "Shift") {
|
}
|
||||||
|
case "]": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
if (e.altKey) {
|
||||||
|
state.send("MOVED_TO_FRONT", getKeyboardEventInfo(e))
|
||||||
|
} else {
|
||||||
|
state.send("MOVED_FORWARD", getKeyboardEventInfo(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "[": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
if (e.altKey) {
|
||||||
|
state.send("MOVED_TO_BACK", getKeyboardEventInfo(e))
|
||||||
|
} else {
|
||||||
|
state.send("MOVED_BACKWARD", getKeyboardEventInfo(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "Shift": {
|
||||||
state.send("PRESSED_SHIFT_KEY", getKeyboardEventInfo(e))
|
state.send("PRESSED_SHIFT_KEY", getKeyboardEventInfo(e))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
case "Alt": {
|
||||||
if (e.key === "Alt") {
|
|
||||||
state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
|
state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
case "Backspace": {
|
||||||
if (e.key === "Backspace" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
|
||||||
state.send("DELETED", getKeyboardEventInfo(e))
|
state.send("DELETED", getKeyboardEventInfo(e))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
case "s": {
|
||||||
if (e.key === "s" && metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
e.preventDefault()
|
state.send("SAVED", getKeyboardEventInfo(e))
|
||||||
state.send("SAVED")
|
|
||||||
}
|
}
|
||||||
if (e.key === "a" && metaKey(e)) {
|
break
|
||||||
e.preventDefault()
|
|
||||||
state.send("SELECTED_ALL")
|
|
||||||
}
|
}
|
||||||
|
case "a": {
|
||||||
if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
if (metaKey(e)) {
|
||||||
|
state.send("SELECTED_ALL", getKeyboardEventInfo(e))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "v": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
state.send("PASTED", getKeyboardEventInfo(e))
|
||||||
|
} else {
|
||||||
state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "d" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
}
|
||||||
|
case "d": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
state.send("DUPLICATED", getKeyboardEventInfo(e))
|
||||||
|
} else {
|
||||||
state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "c" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
}
|
||||||
|
case "c": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
state.send("COPIED", getKeyboardEventInfo(e))
|
||||||
|
} else {
|
||||||
state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "i" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
}
|
||||||
|
case "i": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
} else {
|
||||||
state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "l" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
}
|
||||||
|
case "l": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
} else {
|
||||||
state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "y" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
}
|
||||||
|
case "y": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
} else {
|
||||||
state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "p" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
}
|
||||||
|
case "p": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
} else {
|
||||||
state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
if (e.key === "r" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
}
|
||||||
|
case "r": {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
} else {
|
||||||
state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
state.send("PRESSED_KEY", getKeyboardEventInfo(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp(e: KeyboardEvent) {
|
function handleKeyUp(e: KeyboardEvent) {
|
||||||
|
|
|
@ -94,80 +94,30 @@ const circle = registerShapeUtils<CircleShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
transform(shape, bounds, { type, initialShape, scaleX, scaleY }) {
|
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
|
||||||
const anchor = getTransformAnchor(type, scaleX < 0, scaleY < 0)
|
shape.radius =
|
||||||
|
initialShape.radius * Math.min(Math.abs(scaleX), Math.abs(scaleY))
|
||||||
|
|
||||||
// Set the new corner or position depending on the anchor
|
|
||||||
switch (anchor) {
|
|
||||||
case Corner.TopLeft: {
|
|
||||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
|
||||||
shape.point = [
|
shape.point = [
|
||||||
bounds.maxX - shape.radius * 2,
|
bounds.minX +
|
||||||
bounds.maxY - shape.radius * 2,
|
(bounds.width - shape.radius * 2) *
|
||||||
|
(scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
|
||||||
|
bounds.minY +
|
||||||
|
(bounds.height - shape.radius * 2) *
|
||||||
|
(scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
|
||||||
]
|
]
|
||||||
break
|
|
||||||
}
|
|
||||||
case Corner.TopRight: {
|
|
||||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
|
||||||
shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case Corner.BottomRight: {
|
|
||||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
|
||||||
shape.point = [
|
|
||||||
bounds.maxX - shape.radius * 2,
|
|
||||||
bounds.maxY - shape.radius * 2,
|
|
||||||
]
|
|
||||||
break
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case Corner.BottomLeft: {
|
|
||||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
|
||||||
shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case Edge.Top: {
|
|
||||||
shape.radius = bounds.height / 2
|
|
||||||
shape.point = [
|
|
||||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
|
||||||
bounds.minY,
|
|
||||||
]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case Edge.Right: {
|
|
||||||
shape.radius = bounds.width / 2
|
|
||||||
shape.point = [
|
|
||||||
bounds.maxX - shape.radius * 2,
|
|
||||||
bounds.minY + (bounds.height / 2 - shape.radius),
|
|
||||||
]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case Edge.Bottom: {
|
|
||||||
shape.radius = bounds.height / 2
|
|
||||||
shape.point = [
|
|
||||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
|
||||||
bounds.maxY - shape.radius * 2,
|
|
||||||
]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case Edge.Left: {
|
|
||||||
shape.radius = bounds.width / 2
|
|
||||||
shape.point = [
|
|
||||||
bounds.minX,
|
|
||||||
bounds.minY + (bounds.height / 2 - shape.radius),
|
|
||||||
]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
transformSingle(shape, bounds, info) {
|
transformSingle(shape, bounds, info) {
|
||||||
return this.transform(shape, bounds, info)
|
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||||
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
|
canChangeAspectRatio: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default circle
|
export default circle
|
||||||
|
|
|
@ -94,6 +94,7 @@ const dot = registerShapeUtils<DotShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
canTransform: false,
|
canTransform: false,
|
||||||
|
canChangeAspectRatio: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default dot
|
export default dot
|
||||||
|
|
|
@ -130,6 +130,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
|
canChangeAspectRatio: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default ellipse
|
export default ellipse
|
||||||
|
|
|
@ -60,7 +60,7 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
shape: K,
|
shape: K,
|
||||||
bounds: Bounds,
|
bounds: Bounds,
|
||||||
info: {
|
info: {
|
||||||
type: Edge | Corner | "center"
|
type: Edge | Corner
|
||||||
initialShape: K
|
initialShape: K
|
||||||
scaleX: number
|
scaleX: number
|
||||||
scaleY: number
|
scaleY: number
|
||||||
|
@ -73,7 +73,7 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
shape: K,
|
shape: K,
|
||||||
bounds: Bounds,
|
bounds: Bounds,
|
||||||
info: {
|
info: {
|
||||||
type: Edge | Corner | "center"
|
type: Edge | Corner
|
||||||
initialShape: K
|
initialShape: K
|
||||||
scaleX: number
|
scaleX: number
|
||||||
scaleY: number
|
scaleY: number
|
||||||
|
@ -89,6 +89,9 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
|
|
||||||
// Whether to show transform controls when this shape is selected.
|
// Whether to show transform controls when this shape is selected.
|
||||||
canTransform: boolean
|
canTransform: boolean
|
||||||
|
|
||||||
|
// Whether the shape's aspect ratio can change
|
||||||
|
canChangeAspectRatio: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// A mapping of shape types to shape utilities.
|
// A mapping of shape types to shape utilities.
|
||||||
|
|
|
@ -102,6 +102,7 @@ const line = registerShapeUtils<LineShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
canTransform: false,
|
canTransform: false,
|
||||||
|
canChangeAspectRatio: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default line
|
export default line
|
||||||
|
|
|
@ -99,21 +99,18 @@ const polyline = registerShapeUtils<PolylineShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
transform(
|
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
|
||||||
shape,
|
const initialShapeBounds = this.getBounds(initialShape)
|
||||||
bounds,
|
|
||||||
{ initialShape, initialShapeBounds, isFlippedX, isFlippedY }
|
|
||||||
) {
|
|
||||||
shape.points = shape.points.map((_, i) => {
|
shape.points = shape.points.map((_, i) => {
|
||||||
const [x, y] = initialShape.points[i]
|
const [x, y] = initialShape.points[i]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
bounds.width *
|
bounds.width *
|
||||||
(isFlippedX
|
(scaleX < 0
|
||||||
? 1 - x / initialShapeBounds.width
|
? 1 - x / initialShapeBounds.width
|
||||||
: x / initialShapeBounds.width),
|
: x / initialShapeBounds.width),
|
||||||
bounds.height *
|
bounds.height *
|
||||||
(isFlippedY
|
(scaleY < 0
|
||||||
? 1 - y / initialShapeBounds.height
|
? 1 - y / initialShapeBounds.height
|
||||||
: y / initialShapeBounds.height),
|
: y / initialShapeBounds.height),
|
||||||
]
|
]
|
||||||
|
@ -128,6 +125,7 @@ const polyline = registerShapeUtils<PolylineShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
|
canChangeAspectRatio: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default polyline
|
export default polyline
|
||||||
|
|
|
@ -97,7 +97,12 @@ const ray = registerShapeUtils<RayShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
transformSingle(shape, bounds, info) {
|
||||||
|
return this.transform(shape, bounds, info)
|
||||||
|
},
|
||||||
|
|
||||||
canTransform: false,
|
canTransform: false,
|
||||||
|
canChangeAspectRatio: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default ray
|
export default ray
|
||||||
|
|
|
@ -31,8 +31,23 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render({ id, size }) {
|
render({ id, size, parentId, childIndex }) {
|
||||||
return <rect id={id} width={size[0]} height={size[1]} />
|
return (
|
||||||
|
<g id={id}>
|
||||||
|
<rect id={id} width={size[0]} height={size[1]} />
|
||||||
|
<text
|
||||||
|
y={4}
|
||||||
|
x={4}
|
||||||
|
fontSize={18}
|
||||||
|
fill="black"
|
||||||
|
stroke="none"
|
||||||
|
alignmentBaseline="text-before-edge"
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
|
{childIndex}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
|
@ -128,6 +143,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
|
canChangeAspectRatio: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default rectangle
|
export default rectangle
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import Command from "./command"
|
|
||||||
import history from "../history"
|
|
||||||
import { Data, Shape } from "types"
|
|
||||||
import { getPage } from "utils/utils"
|
|
||||||
|
|
||||||
export default function registerShapeUtilsCommand(data: Data, shape: Shape) {
|
|
||||||
const { currentPageId } = data
|
|
||||||
|
|
||||||
history.execute(
|
|
||||||
data,
|
|
||||||
new Command({
|
|
||||||
name: "translate_shapes",
|
|
||||||
category: "canvas",
|
|
||||||
do(data) {
|
|
||||||
const page = getPage(data)
|
|
||||||
|
|
||||||
page.shapes[shape.id] = shape
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.pointedId = undefined
|
|
||||||
data.hoveredId = undefined
|
|
||||||
},
|
|
||||||
undo(data) {
|
|
||||||
const page = getPage(data)
|
|
||||||
|
|
||||||
delete page.shapes[shape.id]
|
|
||||||
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.pointedId = undefined
|
|
||||||
data.hoveredId = undefined
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@ import translate from "./translate"
|
||||||
import transform from "./transform"
|
import transform from "./transform"
|
||||||
import transformSingle from "./transform-single"
|
import transformSingle from "./transform-single"
|
||||||
import generate from "./generate"
|
import generate from "./generate"
|
||||||
import registerShapeUtils from "./create-shape"
|
|
||||||
import direct from "./direct"
|
import direct from "./direct"
|
||||||
import rotate from "./rotate"
|
import rotate from "./rotate"
|
||||||
|
|
||||||
|
@ -11,7 +10,6 @@ const commands = {
|
||||||
transform,
|
transform,
|
||||||
transformSingle,
|
transformSingle,
|
||||||
generate,
|
generate,
|
||||||
registerShapeUtils,
|
|
||||||
direct,
|
direct,
|
||||||
rotate,
|
rotate,
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,11 @@ import {
|
||||||
export default class TransformSession extends BaseSession {
|
export default class TransformSession extends BaseSession {
|
||||||
scaleX = 1
|
scaleX = 1
|
||||||
scaleY = 1
|
scaleY = 1
|
||||||
transformType: Edge | Corner | "center"
|
transformType: Edge | Corner
|
||||||
origin: number[]
|
origin: number[]
|
||||||
snapshot: TransformSnapshot
|
snapshot: TransformSnapshot
|
||||||
|
|
||||||
constructor(
|
constructor(data: Data, transformType: Corner | Edge, point: number[]) {
|
||||||
data: Data,
|
|
||||||
transformType: Corner | Edge | "center",
|
|
||||||
point: number[]
|
|
||||||
) {
|
|
||||||
super(data)
|
super(data)
|
||||||
this.origin = point
|
this.origin = point
|
||||||
this.transformType = transformType
|
this.transformType = transformType
|
||||||
|
@ -108,10 +104,7 @@ export default class TransformSession extends BaseSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransformSnapshot(
|
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
||||||
data: Data,
|
|
||||||
transformType: Edge | Corner | "center"
|
|
||||||
) {
|
|
||||||
const {
|
const {
|
||||||
document: { pages },
|
document: { pages },
|
||||||
selectedIds,
|
selectedIds,
|
||||||
|
@ -144,6 +137,7 @@ 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 shape = pageShapes[id]
|
||||||
const initialShapeBounds = shapesBounds[id]
|
const initialShapeBounds = shapesBounds[id]
|
||||||
const ic = getBoundsCenter(initialShapeBounds)
|
const ic = getBoundsCenter(initialShapeBounds)
|
||||||
|
|
||||||
|
@ -153,7 +147,7 @@ export function getTransformSnapshot(
|
||||||
return [
|
return [
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
initialShape: pageShapes[id],
|
initialShape: shape,
|
||||||
initialShapeBounds,
|
initialShapeBounds,
|
||||||
transformOrigin: [ix, iy],
|
transformOrigin: [ix, iy],
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default class TransformSingleSession extends BaseSession {
|
||||||
transformType,
|
transformType,
|
||||||
vec.vec(this.origin, point),
|
vec.vec(this.origin, point),
|
||||||
shape.rotation,
|
shape.rotation,
|
||||||
isAspectRatioLocked
|
isAspectRatioLocked || !getShapeUtils(initialShape).canChangeAspectRatio
|
||||||
)
|
)
|
||||||
|
|
||||||
this.scaleX = newBoundingBox.scaleX
|
this.scaleX = newBoundingBox.scaleX
|
||||||
|
|
|
@ -4,7 +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"
|
import { getChildIndexAbove, getPage, getSelectedShapes } from "utils/utils"
|
||||||
|
|
||||||
export default class TranslateSession extends BaseSession {
|
export default class TranslateSession extends BaseSession {
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
|
@ -94,12 +94,17 @@ export default class TranslateSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTranslateSnapshot(data: Data) {
|
export function getTranslateSnapshot(data: Data) {
|
||||||
const shapes = getSelectedShapes(current(data))
|
const cData = current(data)
|
||||||
|
const shapes = getSelectedShapes(cData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentPageId: data.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(),
|
||||||
|
childIndex: getChildIndexAbove(cData, shape.id),
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
256
state/state.ts
256
state/state.ts
|
@ -1,9 +1,11 @@
|
||||||
import { createSelectorHook, createState } from "@state-designer/react"
|
import { createSelectorHook, createState } from "@state-designer/react"
|
||||||
import {
|
import {
|
||||||
clamp,
|
clamp,
|
||||||
|
getChildren,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getPage,
|
getPage,
|
||||||
getShape,
|
getShape,
|
||||||
|
getSiblings,
|
||||||
screenToWorld,
|
screenToWorld,
|
||||||
} from "utils/utils"
|
} from "utils/utils"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
|
@ -97,6 +99,10 @@ const state = createState({
|
||||||
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
||||||
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
||||||
CHANGED_CODE_CONTROL: "updateControls",
|
CHANGED_CODE_CONTROL: "updateControls",
|
||||||
|
MOVED_TO_FRONT: "moveSelectionToFront",
|
||||||
|
MOVED_TO_BACK: "moveSelectionToBack",
|
||||||
|
MOVED_FORWARD: "moveSelectionForward",
|
||||||
|
MOVED_BACKWARD: "moveSelectionBackward",
|
||||||
},
|
},
|
||||||
initial: "notPointing",
|
initial: "notPointing",
|
||||||
states: {
|
states: {
|
||||||
|
@ -222,7 +228,8 @@ const state = createState({
|
||||||
creating: {
|
creating: {
|
||||||
on: {
|
on: {
|
||||||
POINTED_CANVAS: {
|
POINTED_CANVAS: {
|
||||||
do: "createDot",
|
get: "newDot",
|
||||||
|
do: "createShape",
|
||||||
to: "dot.editing",
|
to: "dot.editing",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -272,13 +279,16 @@ const state = createState({
|
||||||
CANCELLED: { to: "selecting" },
|
CANCELLED: { to: "selecting" },
|
||||||
MOVED_POINTER: {
|
MOVED_POINTER: {
|
||||||
if: "distanceImpliesDrag",
|
if: "distanceImpliesDrag",
|
||||||
do: "createCircle",
|
then: {
|
||||||
|
get: "newDot",
|
||||||
|
do: "createShape",
|
||||||
to: "drawingShape.bounds",
|
to: "drawingShape.bounds",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
ellipse: {
|
ellipse: {
|
||||||
initial: "creating",
|
initial: "creating",
|
||||||
states: {
|
states: {
|
||||||
|
@ -296,13 +306,16 @@ const state = createState({
|
||||||
CANCELLED: { to: "selecting" },
|
CANCELLED: { to: "selecting" },
|
||||||
MOVED_POINTER: {
|
MOVED_POINTER: {
|
||||||
if: "distanceImpliesDrag",
|
if: "distanceImpliesDrag",
|
||||||
do: "createEllipse",
|
then: {
|
||||||
|
get: "newEllipse",
|
||||||
|
do: "createShape",
|
||||||
to: "drawingShape.bounds",
|
to: "drawingShape.bounds",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
rectangle: {
|
rectangle: {
|
||||||
initial: "creating",
|
initial: "creating",
|
||||||
states: {
|
states: {
|
||||||
|
@ -320,13 +333,16 @@ const state = createState({
|
||||||
CANCELLED: { to: "selecting" },
|
CANCELLED: { to: "selecting" },
|
||||||
MOVED_POINTER: {
|
MOVED_POINTER: {
|
||||||
if: "distanceImpliesDrag",
|
if: "distanceImpliesDrag",
|
||||||
do: "createRectangle",
|
then: {
|
||||||
|
get: "newRectangle",
|
||||||
|
do: "createShape",
|
||||||
to: "drawingShape.bounds",
|
to: "drawingShape.bounds",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
ray: {
|
ray: {
|
||||||
initial: "creating",
|
initial: "creating",
|
||||||
states: {
|
states: {
|
||||||
|
@ -334,7 +350,8 @@ const state = createState({
|
||||||
on: {
|
on: {
|
||||||
CANCELLED: { to: "selecting" },
|
CANCELLED: { to: "selecting" },
|
||||||
POINTED_CANVAS: {
|
POINTED_CANVAS: {
|
||||||
do: "createRay",
|
get: "newRay",
|
||||||
|
do: "createShape",
|
||||||
to: "ray.editing",
|
to: "ray.editing",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -358,7 +375,8 @@ const state = createState({
|
||||||
on: {
|
on: {
|
||||||
CANCELLED: { to: "selecting" },
|
CANCELLED: { to: "selecting" },
|
||||||
POINTED_CANVAS: {
|
POINTED_CANVAS: {
|
||||||
do: "createLine",
|
get: "newLine",
|
||||||
|
do: "createShape",
|
||||||
to: "line.editing",
|
to: "line.editing",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -408,6 +426,51 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
results: {
|
||||||
|
// Dot
|
||||||
|
newDot(data, payload: PointerInfo) {
|
||||||
|
return shapeUtilityMap[ShapeType.Dot].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ray
|
||||||
|
newRay(data, payload: PointerInfo) {
|
||||||
|
return shapeUtilityMap[ShapeType.Ray].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Line
|
||||||
|
newLine(data, payload: PointerInfo) {
|
||||||
|
return shapeUtilityMap[ShapeType.Line].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
direction: [0, 1],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
newCircle(data, payload: PointerInfo) {
|
||||||
|
return shapeUtilityMap[ShapeType.Circle].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
radius: 1,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
newEllipse(data, payload: PointerInfo) {
|
||||||
|
return shapeUtilityMap[ShapeType.Ellipse].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
radiusX: 1,
|
||||||
|
radiusY: 1,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
newRectangle(data, payload: PointerInfo) {
|
||||||
|
return shapeUtilityMap[ShapeType.Rectangle].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
size: [1, 1],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
isPointingBounds(data, payload: PointerInfo) {
|
isPointingBounds(data, payload: PointerInfo) {
|
||||||
return payload.target === "bounds"
|
return payload.target === "bounds"
|
||||||
|
@ -447,69 +510,10 @@ const state = createState({
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
/* --------------------- Shapes --------------------- */
|
/* --------------------- Shapes --------------------- */
|
||||||
|
createShape(data, payload: PointerInfo, shape: Shape) {
|
||||||
// Dot
|
const siblings = getChildren(data, shape.parentId)
|
||||||
createDot(data, payload: PointerInfo) {
|
shape.childIndex =
|
||||||
const shape = shapeUtilityMap[ShapeType.Dot].create({
|
siblings.length > 0 ? siblings[siblings.length - 1].childIndex + 1 : 1
|
||||||
point: screenToWorld(payload.point, data),
|
|
||||||
})
|
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.selectedIds.add(shape.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Ray
|
|
||||||
createRay(data, payload: PointerInfo) {
|
|
||||||
const shape = shapeUtilityMap[ShapeType.Ray].create({
|
|
||||||
point: screenToWorld(payload.point, data),
|
|
||||||
})
|
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.selectedIds.add(shape.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Line
|
|
||||||
createLine(data, payload: PointerInfo) {
|
|
||||||
const shape = shapeUtilityMap[ShapeType.Line].create({
|
|
||||||
point: screenToWorld(payload.point, data),
|
|
||||||
direction: [0, 1],
|
|
||||||
})
|
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.selectedIds.add(shape.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
createCircle(data, payload: PointerInfo) {
|
|
||||||
const shape = shapeUtilityMap[ShapeType.Circle].create({
|
|
||||||
point: screenToWorld(payload.point, data),
|
|
||||||
radius: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.selectedIds.add(shape.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
createEllipse(data, payload: PointerInfo) {
|
|
||||||
const shape = shapeUtilityMap[ShapeType.Ellipse].create({
|
|
||||||
point: screenToWorld(payload.point, data),
|
|
||||||
radiusX: 1,
|
|
||||||
radiusY: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.selectedIds.add(shape.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
createRectangle(data, payload: PointerInfo) {
|
|
||||||
const shape = shapeUtilityMap[ShapeType.Rectangle].create({
|
|
||||||
point: screenToWorld(payload.point, data),
|
|
||||||
size: [1, 1],
|
|
||||||
})
|
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
getPage(data).shapes[shape.id] = shape
|
||||||
data.selectedIds.clear()
|
data.selectedIds.clear()
|
||||||
|
@ -671,7 +675,119 @@ const state = createState({
|
||||||
pushPointedIdToSelectedIds(data) {
|
pushPointedIdToSelectedIds(data) {
|
||||||
data.selectedIds.add(data.pointedId)
|
data.selectedIds.add(data.pointedId)
|
||||||
},
|
},
|
||||||
// Camera
|
moveSelectionToFront(data) {
|
||||||
|
const { selectedIds } = data
|
||||||
|
},
|
||||||
|
moveSelectionToBack(data) {
|
||||||
|
const { selectedIds } = data
|
||||||
|
},
|
||||||
|
moveSelectionForward(data) {
|
||||||
|
const { selectedIds } = data
|
||||||
|
|
||||||
|
const page = getPage(data)
|
||||||
|
|
||||||
|
const shapes = Array.from(selectedIds.values()).map(
|
||||||
|
(id) => page.shapes[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const shapesByParentId = shapes.reduce<Record<string, Shape[]>>(
|
||||||
|
(acc, shape) => {
|
||||||
|
if (acc[shape.parentId] === undefined) {
|
||||||
|
acc[shape.parentId] = []
|
||||||
|
}
|
||||||
|
acc[shape.parentId].push(shape)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const visited = new Set<string>()
|
||||||
|
|
||||||
|
for (let id in shapesByParentId) {
|
||||||
|
const children = getChildren(data, id)
|
||||||
|
|
||||||
|
shapesByParentId[id]
|
||||||
|
.sort((a, b) => b.childIndex - a.childIndex)
|
||||||
|
.forEach((shape) => {
|
||||||
|
visited.add(shape.id)
|
||||||
|
children.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
const index = children.indexOf(shape)
|
||||||
|
|
||||||
|
const nextSibling = children[index + 1]
|
||||||
|
|
||||||
|
if (!nextSibling || visited.has(nextSibling.id)) {
|
||||||
|
// At the top already, no change
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextNextSibling = children[index + 2]
|
||||||
|
|
||||||
|
if (!nextNextSibling) {
|
||||||
|
// Moving to the top
|
||||||
|
shape.childIndex = nextSibling.childIndex + 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shape.childIndex =
|
||||||
|
(nextSibling.childIndex + nextNextSibling.childIndex) / 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moveSelectionBackward(data) {
|
||||||
|
const { selectedIds } = data
|
||||||
|
|
||||||
|
const page = getPage(data)
|
||||||
|
|
||||||
|
const shapes = Array.from(selectedIds.values()).map(
|
||||||
|
(id) => page.shapes[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const shapesByParentId = shapes.reduce<Record<string, Shape[]>>(
|
||||||
|
(acc, shape) => {
|
||||||
|
if (acc[shape.parentId] === undefined) {
|
||||||
|
acc[shape.parentId] = []
|
||||||
|
}
|
||||||
|
acc[shape.parentId].push(shape)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const visited = new Set<string>()
|
||||||
|
|
||||||
|
for (let id in shapesByParentId) {
|
||||||
|
const children = getChildren(data, id)
|
||||||
|
|
||||||
|
shapesByParentId[id]
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
.forEach((shape) => {
|
||||||
|
visited.add(shape.id)
|
||||||
|
children.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
const index = children.indexOf(shape)
|
||||||
|
|
||||||
|
const nextSibling = children[index - 1]
|
||||||
|
|
||||||
|
if (!nextSibling || visited.has(nextSibling.id)) {
|
||||||
|
// At the bottom already, no change
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextNextSibling = children[index - 2]
|
||||||
|
|
||||||
|
if (!nextNextSibling) {
|
||||||
|
// Moving to the bottom
|
||||||
|
shape.childIndex = nextSibling.childIndex / 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shape.childIndex =
|
||||||
|
(nextSibling.childIndex + nextNextSibling.childIndex) / 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* --------------------- Camera --------------------- */
|
||||||
|
|
||||||
resetCamera(data) {
|
resetCamera(data) {
|
||||||
data.camera.zoom = 1
|
data.camera.zoom = 1
|
||||||
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
|
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
|
||||||
|
|
|
@ -1,251 +0,0 @@
|
||||||
import { Bounds, BoundsSnapshot, ShapeBounds } from "types"
|
|
||||||
|
|
||||||
export function stretchshapesX(shapes: ShapeBounds[]) {
|
|
||||||
const [first, ...rest] = shapes
|
|
||||||
let min = first.minX
|
|
||||||
let max = first.minX + first.width
|
|
||||||
for (let box of rest) {
|
|
||||||
min = Math.min(min, box.minX)
|
|
||||||
max = Math.max(max, box.minX + box.width)
|
|
||||||
}
|
|
||||||
return shapes.map((box) => ({ ...box, x: min, width: max - min }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stretchshapesY(shapes: ShapeBounds[]) {
|
|
||||||
const [first, ...rest] = shapes
|
|
||||||
let min = first.minY
|
|
||||||
let max = first.minY + first.height
|
|
||||||
for (let box of rest) {
|
|
||||||
min = Math.min(min, box.minY)
|
|
||||||
max = Math.max(max, box.minY + box.height)
|
|
||||||
}
|
|
||||||
return shapes.map((box) => ({ ...box, y: min, height: max - min }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function distributeshapesX(shapes: ShapeBounds[]) {
|
|
||||||
const len = shapes.length
|
|
||||||
const sorted = [...shapes].sort((a, b) => a.minX - b.minX)
|
|
||||||
let min = sorted[0].minX
|
|
||||||
|
|
||||||
sorted.sort((a, b) => a.minX + a.width - b.minX - b.width)
|
|
||||||
let last = sorted[len - 1]
|
|
||||||
let max = last.minX + last.width
|
|
||||||
|
|
||||||
let range = max - min
|
|
||||||
let step = range / len
|
|
||||||
return sorted.map((box, i) => ({ ...box, x: min + step * i }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function distributeshapesY(shapes: ShapeBounds[]) {
|
|
||||||
const len = shapes.length
|
|
||||||
const sorted = [...shapes].sort((a, b) => a.minY - b.minY)
|
|
||||||
let min = sorted[0].minY
|
|
||||||
|
|
||||||
sorted.sort((a, b) => a.minY + a.height - b.minY - b.height)
|
|
||||||
let last = sorted[len - 1]
|
|
||||||
let max = last.minY + last.height
|
|
||||||
|
|
||||||
let range = max - min
|
|
||||||
let step = range / len
|
|
||||||
return sorted.map((box, i) => ({ ...box, y: min + step * i }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alignshapesCenterX(shapes: ShapeBounds[]) {
|
|
||||||
let midX = 0
|
|
||||||
for (let box of shapes) midX += box.minX + box.width / 2
|
|
||||||
midX /= shapes.length
|
|
||||||
return shapes.map((box) => ({ ...box, x: midX - box.width / 2 }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alignshapesCenterY(shapes: ShapeBounds[]) {
|
|
||||||
let midY = 0
|
|
||||||
for (let box of shapes) midY += box.minY + box.height / 2
|
|
||||||
midY /= shapes.length
|
|
||||||
return shapes.map((box) => ({ ...box, y: midY - box.height / 2 }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alignshapesTop(shapes: ShapeBounds[]) {
|
|
||||||
const [first, ...rest] = shapes
|
|
||||||
let y = first.minY
|
|
||||||
for (let box of rest) if (box.minY < y) y = box.minY
|
|
||||||
return shapes.map((box) => ({ ...box, y }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alignshapesBottom(shapes: ShapeBounds[]) {
|
|
||||||
const [first, ...rest] = shapes
|
|
||||||
let maxY = first.minY + first.height
|
|
||||||
for (let box of rest)
|
|
||||||
if (box.minY + box.height > maxY) maxY = box.minY + box.height
|
|
||||||
return shapes.map((box) => ({ ...box, y: maxY - box.height }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alignshapesLeft(shapes: ShapeBounds[]) {
|
|
||||||
const [first, ...rest] = shapes
|
|
||||||
let x = first.minX
|
|
||||||
for (let box of rest) if (box.minX < x) x = box.minX
|
|
||||||
return shapes.map((box) => ({ ...box, x }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alignshapesRight(shapes: ShapeBounds[]) {
|
|
||||||
const [first, ...rest] = shapes
|
|
||||||
let maxX = first.minX + first.width
|
|
||||||
for (let box of rest)
|
|
||||||
if (box.minX + box.width > maxX) maxX = box.minX + box.width
|
|
||||||
return shapes.map((box) => ({ ...box, x: maxX - box.width }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resizers
|
|
||||||
|
|
||||||
export function getBoundingBox(shapes: ShapeBounds[]): Bounds {
|
|
||||||
if (shapes.length === 0) {
|
|
||||||
return {
|
|
||||||
minX: 0,
|
|
||||||
minY: 0,
|
|
||||||
maxX: 0,
|
|
||||||
maxY: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const first = shapes[0]
|
|
||||||
|
|
||||||
let minX = first.minX
|
|
||||||
let minY = first.minY
|
|
||||||
let maxX = first.minX + first.width
|
|
||||||
let maxY = first.minY + first.height
|
|
||||||
|
|
||||||
for (let box of shapes) {
|
|
||||||
minX = Math.min(minX, box.minX)
|
|
||||||
minY = Math.min(minY, box.minY)
|
|
||||||
maxX = Math.max(maxX, box.minX + box.width)
|
|
||||||
maxY = Math.max(maxY, box.minY + box.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
minX,
|
|
||||||
minY,
|
|
||||||
maxX,
|
|
||||||
maxY,
|
|
||||||
width: maxX - minX,
|
|
||||||
height: maxY - minY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSnapshots(
|
|
||||||
shapes: ShapeBounds[],
|
|
||||||
bounds: Bounds
|
|
||||||
): Record<string, BoundsSnapshot> {
|
|
||||||
const acc = {} as Record<string, BoundsSnapshot>
|
|
||||||
|
|
||||||
const w = bounds.maxX - bounds.minX
|
|
||||||
const h = bounds.maxY - bounds.minY
|
|
||||||
|
|
||||||
for (let box of shapes) {
|
|
||||||
acc[box.id] = {
|
|
||||||
...box,
|
|
||||||
nx: (box.minX - bounds.minX) / w,
|
|
||||||
ny: (box.minY - bounds.minY) / h,
|
|
||||||
nmx: 1 - (box.minX + box.width - bounds.minX) / w,
|
|
||||||
nmy: 1 - (box.minY + box.height - bounds.minY) / h,
|
|
||||||
nw: box.width / w,
|
|
||||||
nh: box.height / h,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEdgeResizer(shapes: ShapeBounds[], edge: number) {
|
|
||||||
const initial = getBoundingBox(shapes)
|
|
||||||
const snapshots = getSnapshots(shapes, initial)
|
|
||||||
const mshapes = [...shapes]
|
|
||||||
|
|
||||||
let { minX: x0, minY: y0, maxX: x1, maxY: y1 } = initial
|
|
||||||
let { minX: mx, minY: my } = initial
|
|
||||||
let mw = x1 - x0
|
|
||||||
let mh = y1 - y0
|
|
||||||
|
|
||||||
return function edgeResize({ x, y }) {
|
|
||||||
if (edge === 0 || edge === 2) {
|
|
||||||
edge === 0 ? (y0 = y) : (y1 = y)
|
|
||||||
my = y0 < y1 ? y0 : y1
|
|
||||||
mh = Math.abs(y1 - y0)
|
|
||||||
for (let box of mshapes) {
|
|
||||||
const { ny, nmy, nh } = snapshots[box.id]
|
|
||||||
box.minY = my + (y1 < y0 ? nmy : ny) * mh
|
|
||||||
box.height = nh * mh
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
edge === 1 ? (x1 = x) : (x0 = x)
|
|
||||||
mx = x0 < x1 ? x0 : x1
|
|
||||||
mw = Math.abs(x1 - x0)
|
|
||||||
for (let box of mshapes) {
|
|
||||||
const { nx, nmx, nw } = snapshots[box.id]
|
|
||||||
box.minX = mx + (x1 < x0 ? nmx : nx) * mw
|
|
||||||
box.width = nw * mw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
mshapes,
|
|
||||||
{
|
|
||||||
x: mx,
|
|
||||||
y: my,
|
|
||||||
width: mw,
|
|
||||||
height: mh,
|
|
||||||
maxX: mx + mw,
|
|
||||||
maxY: my + mh,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function that can be used to calculate corner resize transforms.
|
|
||||||
* @param shapes An array of the shapes being resized.
|
|
||||||
* @param corner A number representing the corner being dragged. Top Left: 0, Top Right: 1, Bottom Right: 2, Bottom Left: 3.
|
|
||||||
* @example
|
|
||||||
* const resizer = getCornerResizer(selectedshapes, 3)
|
|
||||||
* resizer(selectedshapes, )
|
|
||||||
*/
|
|
||||||
export function getCornerResizer(shapes: ShapeBounds[], corner: number) {
|
|
||||||
const initial = getBoundingBox(shapes)
|
|
||||||
const snapshots = getSnapshots(shapes, initial)
|
|
||||||
const mshapes = [...shapes]
|
|
||||||
|
|
||||||
let { minX: x0, minY: y0, maxX: x1, maxY: y1 } = initial
|
|
||||||
let { minX: mx, minY: my } = initial
|
|
||||||
let mw = x1 - x0
|
|
||||||
let mh = y1 - y0
|
|
||||||
|
|
||||||
return function cornerResizer({ x, y }) {
|
|
||||||
corner < 2 ? (y0 = y) : (y1 = y)
|
|
||||||
my = y0 < y1 ? y0 : y1
|
|
||||||
mh = Math.abs(y1 - y0)
|
|
||||||
|
|
||||||
corner === 1 || corner === 2 ? (x1 = x) : (x0 = x)
|
|
||||||
mx = x0 < x1 ? x0 : x1
|
|
||||||
mw = Math.abs(x1 - x0)
|
|
||||||
|
|
||||||
for (let box of mshapes) {
|
|
||||||
const { nx, nmx, nw, ny, nmy, nh } = snapshots[box.id]
|
|
||||||
box.minX = mx + (x1 < x0 ? nmx : nx) * mw
|
|
||||||
box.minY = my + (y1 < y0 ? nmy : ny) * mh
|
|
||||||
box.width = nw * mw
|
|
||||||
box.height = nh * mh
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
mshapes,
|
|
||||||
{
|
|
||||||
x: mx,
|
|
||||||
y: my,
|
|
||||||
width: mw,
|
|
||||||
height: mh,
|
|
||||||
maxX: mx + mw,
|
|
||||||
maxY: my + mh,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1375,3 +1375,82 @@ export function clampToRotationToSegments(r: number, segments: number) {
|
||||||
const seg = (Math.PI * 2) / segments
|
const seg = (Math.PI * 2) / segments
|
||||||
return Math.floor((clampRadians(r) + seg / 2) / seg) * seg
|
return Math.floor((clampRadians(r) + seg / 2) / seg) * seg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getParent(data: Data, id: string, pageId = data.currentPageId) {
|
||||||
|
const page = getPage(data, pageId)
|
||||||
|
const shape = page.shapes[id]
|
||||||
|
|
||||||
|
return page.shapes[shape.parentId] || data.document.pages[shape.parentId]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChildren(
|
||||||
|
data: Data,
|
||||||
|
id: string,
|
||||||
|
pageId = data.currentPageId
|
||||||
|
) {
|
||||||
|
const page = getPage(data, pageId)
|
||||||
|
return Object.values(page.shapes)
|
||||||
|
.filter(({ parentId }) => parentId === id)
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSiblings(
|
||||||
|
data: Data,
|
||||||
|
id: string,
|
||||||
|
pageId = data.currentPageId
|
||||||
|
) {
|
||||||
|
const page = getPage(data, pageId)
|
||||||
|
const shape = page.shapes[id]
|
||||||
|
|
||||||
|
return Object.values(page.shapes)
|
||||||
|
.filter(({ parentId }) => parentId === shape.parentId)
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChildIndexAbove(
|
||||||
|
data: Data,
|
||||||
|
id: string,
|
||||||
|
pageId = data.currentPageId
|
||||||
|
) {
|
||||||
|
const page = getPage(data, pageId)
|
||||||
|
|
||||||
|
const shape = page.shapes[id]
|
||||||
|
|
||||||
|
const siblings = Object.values(page.shapes)
|
||||||
|
.filter(({ parentId }) => parentId === shape.parentId)
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
|
||||||
|
const index = siblings.indexOf(shape)
|
||||||
|
|
||||||
|
const nextSibling = siblings[index + 1]
|
||||||
|
|
||||||
|
if (!nextSibling) {
|
||||||
|
return shape.childIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return (shape.childIndex + nextSibling.childIndex) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChildIndexBelow(
|
||||||
|
data: Data,
|
||||||
|
id: string,
|
||||||
|
pageId = data.currentPageId
|
||||||
|
) {
|
||||||
|
const page = getPage(data, pageId)
|
||||||
|
|
||||||
|
const shape = page.shapes[id]
|
||||||
|
|
||||||
|
const siblings = Object.values(page.shapes)
|
||||||
|
.filter(({ parentId }) => parentId === shape.parentId)
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
|
||||||
|
const index = siblings.indexOf(shape)
|
||||||
|
|
||||||
|
const prevSibling = siblings[index - 1]
|
||||||
|
|
||||||
|
if (!prevSibling) {
|
||||||
|
return shape.childIndex / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return (shape.childIndex + prevSibling.childIndex) / 2
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue