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%",
|
||||
touchAction: "none",
|
||||
zIndex: 100,
|
||||
|
||||
"& *": {
|
||||
userSelect: "none",
|
||||
},
|
||||
})
|
||||
|
||||
const MainGroup = styled("g", {})
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -94,6 +94,7 @@ const dot = registerShapeUtils<DotShape>({
|
|||
},
|
||||
|
||||
canTransform: false,
|
||||
canChangeAspectRatio: false,
|
||||
})
|
||||
|
||||
export default dot
|
||||
|
|
|
@ -130,6 +130,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
|||
},
|
||||
|
||||
canTransform: true,
|
||||
canChangeAspectRatio: true,
|
||||
})
|
||||
|
||||
export default ellipse
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -102,6 +102,7 @@ const line = registerShapeUtils<LineShape>({
|
|||
},
|
||||
|
||||
canTransform: false,
|
||||
canChangeAspectRatio: false,
|
||||
})
|
||||
|
||||
export default line
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 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,
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
256
state/state.ts
256
state/state.ts
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
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