Basic z ordering

This commit is contained in:
Steve Ruiz 2021-05-23 14:46:04 +01:00
parent 6582eb990c
commit f11c35e941
20 changed files with 468 additions and 516 deletions

View file

@ -56,6 +56,10 @@ const MainSVG = styled("svg", {
height: "100%",
touchAction: "none",
zIndex: 100,
"& *": {
userSelect: "none",
},
})
const MainGroup = styled("g", {})

View file

@ -9,10 +9,11 @@ here; and still cheaper than any other pattern I've found.
*/
export default function Page() {
const currentPageShapeIds = useSelector(
({ data }) => Object.keys(getPage(data).shapes),
deepCompareArrays
)
const currentPageShapeIds = useSelector(({ data }) => {
return Object.values(getPage(data).shapes)
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => shape.id)
}, deepCompareArrays)
return (
<>

View file

@ -1,8 +1,8 @@
import React, { useCallback, useRef, memo } from "react"
import state, { useSelector } from "state"
import inputs from "state/inputs"
import { getShapeUtils } from "lib/shape-utils"
import styled from "styles"
import { getShapeUtils } from "lib/shape-utils"
import { getPage } from "utils/utils"
function Shape({ id }: { id: string }) {

View file

@ -1,72 +1,136 @@
import { useEffect } from "react"
import state from "state"
import { getKeyboardEventInfo, isDarwin, metaKey } from "utils/utils"
import { getKeyboardEventInfo, metaKey } from "utils/utils"
export default function useKeyboardEvents() {
useEffect(() => {
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")
} else if (e.key === "z" && metaKey(e)) {
break
}
case "z": {
if (metaKey(e)) {
if (e.shiftKey) {
state.send("REDO")
state.send("REDO", getKeyboardEventInfo(e))
} else {
state.send("UNDO")
state.send("UNDO", getKeyboardEventInfo(e))
}
}
if (e.key === "Shift") {
break
}
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))
break
}
if (e.key === "Alt") {
case "Alt": {
state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
break
}
if (e.key === "Backspace" && !(metaKey(e) || e.shiftKey || e.altKey)) {
case "Backspace": {
state.send("DELETED", getKeyboardEventInfo(e))
break
}
if (e.key === "s" && metaKey(e)) {
e.preventDefault()
state.send("SAVED")
case "s": {
if (metaKey(e)) {
state.send("SAVED", getKeyboardEventInfo(e))
}
if (e.key === "a" && metaKey(e)) {
e.preventDefault()
state.send("SELECTED_ALL")
break
}
if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) {
case "a": {
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))
}
if (e.key === "d" && !(metaKey(e) || e.shiftKey || e.altKey)) {
break
}
case "d": {
if (metaKey(e)) {
state.send("DUPLICATED", getKeyboardEventInfo(e))
} else {
state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
}
if (e.key === "c" && !(metaKey(e) || e.shiftKey || e.altKey)) {
break
}
case "c": {
if (metaKey(e)) {
state.send("COPIED", getKeyboardEventInfo(e))
} else {
state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
}
if (e.key === "i" && !(metaKey(e) || e.shiftKey || e.altKey)) {
break
}
case "i": {
if (metaKey(e)) {
} else {
state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
}
if (e.key === "l" && !(metaKey(e) || e.shiftKey || e.altKey)) {
break
}
case "l": {
if (metaKey(e)) {
} else {
state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
}
if (e.key === "y" && !(metaKey(e) || e.shiftKey || e.altKey)) {
break
}
case "y": {
if (metaKey(e)) {
} else {
state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
}
if (e.key === "p" && !(metaKey(e) || e.shiftKey || e.altKey)) {
break
}
case "p": {
if (metaKey(e)) {
} else {
state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
}
if (e.key === "r" && !(metaKey(e) || e.shiftKey || e.altKey)) {
break
}
case "r": {
if (metaKey(e)) {
} else {
state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
}
break
}
default: {
state.send("PRESSED_KEY", getKeyboardEventInfo(e))
}
}
}
function handleKeyUp(e: KeyboardEvent) {

View file

@ -94,80 +94,30 @@ const circle = registerShapeUtils<CircleShape>({
return shape
},
transform(shape, bounds, { type, initialShape, scaleX, scaleY }) {
const anchor = getTransformAnchor(type, scaleX < 0, scaleY < 0)
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
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 = [
bounds.maxX - shape.radius * 2,
bounds.maxY - shape.radius * 2,
bounds.minX +
(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
},
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,
canChangeAspectRatio: false,
})
export default circle

View file

@ -94,6 +94,7 @@ const dot = registerShapeUtils<DotShape>({
},
canTransform: false,
canChangeAspectRatio: false,
})
export default dot

View file

@ -130,6 +130,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
},
canTransform: true,
canChangeAspectRatio: true,
})
export default ellipse

View file

@ -60,7 +60,7 @@ export interface ShapeUtility<K extends Shape> {
shape: K,
bounds: Bounds,
info: {
type: Edge | Corner | "center"
type: Edge | Corner
initialShape: K
scaleX: number
scaleY: number
@ -73,7 +73,7 @@ export interface ShapeUtility<K extends Shape> {
shape: K,
bounds: Bounds,
info: {
type: Edge | Corner | "center"
type: Edge | Corner
initialShape: K
scaleX: number
scaleY: number
@ -89,6 +89,9 @@ export interface ShapeUtility<K extends Shape> {
// Whether to show transform controls when this shape is selected.
canTransform: boolean
// Whether the shape's aspect ratio can change
canChangeAspectRatio: boolean
}
// A mapping of shape types to shape utilities.

View file

@ -102,6 +102,7 @@ const line = registerShapeUtils<LineShape>({
},
canTransform: false,
canChangeAspectRatio: false,
})
export default line

View file

@ -99,21 +99,18 @@ const polyline = registerShapeUtils<PolylineShape>({
return shape
},
transform(
shape,
bounds,
{ initialShape, initialShapeBounds, isFlippedX, isFlippedY }
) {
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
const initialShapeBounds = this.getBounds(initialShape)
shape.points = shape.points.map((_, i) => {
const [x, y] = initialShape.points[i]
return [
bounds.width *
(isFlippedX
(scaleX < 0
? 1 - x / initialShapeBounds.width
: x / initialShapeBounds.width),
bounds.height *
(isFlippedY
(scaleY < 0
? 1 - y / initialShapeBounds.height
: y / initialShapeBounds.height),
]
@ -128,6 +125,7 @@ const polyline = registerShapeUtils<PolylineShape>({
},
canTransform: true,
canChangeAspectRatio: true,
})
export default polyline

View file

@ -97,7 +97,12 @@ const ray = registerShapeUtils<RayShape>({
return shape
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
canTransform: false,
canChangeAspectRatio: false,
})
export default ray

View file

@ -31,8 +31,23 @@ const rectangle = registerShapeUtils<RectangleShape>({
}
},
render({ id, size }) {
return <rect id={id} width={size[0]} height={size[1]} />
render({ id, size, parentId, childIndex }) {
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) {
@ -128,6 +143,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
},
canTransform: true,
canChangeAspectRatio: true,
})
export default rectangle

View file

@ -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
},
})
)
}

View file

@ -2,7 +2,6 @@ import translate from "./translate"
import transform from "./transform"
import transformSingle from "./transform-single"
import generate from "./generate"
import registerShapeUtils from "./create-shape"
import direct from "./direct"
import rotate from "./rotate"
@ -11,7 +10,6 @@ const commands = {
transform,
transformSingle,
generate,
registerShapeUtils,
direct,
rotate,
}

View file

@ -17,15 +17,11 @@ import {
export default class TransformSession extends BaseSession {
scaleX = 1
scaleY = 1
transformType: Edge | Corner | "center"
transformType: Edge | Corner
origin: number[]
snapshot: TransformSnapshot
constructor(
data: Data,
transformType: Corner | Edge | "center",
point: number[]
) {
constructor(data: Data, transformType: Corner | Edge, point: number[]) {
super(data)
this.origin = point
this.transformType = transformType
@ -108,10 +104,7 @@ export default class TransformSession extends BaseSession {
}
}
export function getTransformSnapshot(
data: Data,
transformType: Edge | Corner | "center"
) {
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
const {
document: { pages },
selectedIds,
@ -144,6 +137,7 @@ export function getTransformSnapshot(
initialBounds: bounds,
shapeBounds: Object.fromEntries(
Array.from(selectedIds.values()).map((id) => {
const shape = pageShapes[id]
const initialShapeBounds = shapesBounds[id]
const ic = getBoundsCenter(initialShapeBounds)
@ -153,7 +147,7 @@ export function getTransformSnapshot(
return [
id,
{
initialShape: pageShapes[id],
initialShape: shape,
initialShapeBounds,
transformOrigin: [ix, iy],
},

View file

@ -48,7 +48,7 @@ export default class TransformSingleSession extends BaseSession {
transformType,
vec.vec(this.origin, point),
shape.rotation,
isAspectRatioLocked
isAspectRatioLocked || !getShapeUtils(initialShape).canChangeAspectRatio
)
this.scaleX = newBoundingBox.scaleX

View file

@ -4,7 +4,7 @@ import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
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 {
delta = [0, 0]
@ -94,12 +94,17 @@ export default class TranslateSession extends BaseSession {
}
export function getTranslateSnapshot(data: Data) {
const shapes = getSelectedShapes(current(data))
const cData = current(data)
const shapes = getSelectedShapes(cData)
return {
currentPageId: data.currentPageId,
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),
})),
}
}

View file

@ -1,9 +1,11 @@
import { createSelectorHook, createState } from "@state-designer/react"
import {
clamp,
getChildren,
getCommonBounds,
getPage,
getShape,
getSiblings,
screenToWorld,
} from "utils/utils"
import * as vec from "utils/vec"
@ -97,6 +99,10 @@ const state = createState({
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
CHANGED_CODE_CONTROL: "updateControls",
MOVED_TO_FRONT: "moveSelectionToFront",
MOVED_TO_BACK: "moveSelectionToBack",
MOVED_FORWARD: "moveSelectionForward",
MOVED_BACKWARD: "moveSelectionBackward",
},
initial: "notPointing",
states: {
@ -222,7 +228,8 @@ const state = createState({
creating: {
on: {
POINTED_CANVAS: {
do: "createDot",
get: "newDot",
do: "createShape",
to: "dot.editing",
},
},
@ -272,13 +279,16 @@ const state = createState({
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
do: "createCircle",
then: {
get: "newDot",
do: "createShape",
to: "drawingShape.bounds",
},
},
},
},
},
},
ellipse: {
initial: "creating",
states: {
@ -296,13 +306,16 @@ const state = createState({
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
do: "createEllipse",
then: {
get: "newEllipse",
do: "createShape",
to: "drawingShape.bounds",
},
},
},
},
},
},
rectangle: {
initial: "creating",
states: {
@ -320,13 +333,16 @@ const state = createState({
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
do: "createRectangle",
then: {
get: "newRectangle",
do: "createShape",
to: "drawingShape.bounds",
},
},
},
},
},
},
ray: {
initial: "creating",
states: {
@ -334,7 +350,8 @@ const state = createState({
on: {
CANCELLED: { to: "selecting" },
POINTED_CANVAS: {
do: "createRay",
get: "newRay",
do: "createShape",
to: "ray.editing",
},
},
@ -358,7 +375,8 @@ const state = createState({
on: {
CANCELLED: { to: "selecting" },
POINTED_CANVAS: {
do: "createLine",
get: "newLine",
do: "createShape",
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: {
isPointingBounds(data, payload: PointerInfo) {
return payload.target === "bounds"
@ -447,69 +510,10 @@ const state = createState({
},
actions: {
/* --------------------- Shapes --------------------- */
// Dot
createDot(data, payload: PointerInfo) {
const shape = shapeUtilityMap[ShapeType.Dot].create({
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],
})
createShape(data, payload: PointerInfo, shape: Shape) {
const siblings = getChildren(data, shape.parentId)
shape.childIndex =
siblings.length > 0 ? siblings[siblings.length - 1].childIndex + 1 : 1
getPage(data).shapes[shape.id] = shape
data.selectedIds.clear()
@ -671,7 +675,119 @@ const state = createState({
pushPointedIdToSelectedIds(data) {
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) {
data.camera.zoom = 1
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]

View file

@ -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,
},
]
}
}

View file

@ -1375,3 +1375,82 @@ export function clampToRotationToSegments(r: number, segments: number) {
const seg = (Math.PI * 2) / segments
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
}