refactors bounds, improves transforming rotating shapes
This commit is contained in:
parent
fb0bb47c19
commit
b752782753
33 changed files with 690 additions and 632 deletions
|
@ -1,47 +0,0 @@
|
|||
import { useRef } from "react"
|
||||
import state, { useSelector } from "state"
|
||||
import inputs from "state/inputs"
|
||||
import styled from "styles"
|
||||
|
||||
export default function BoundsBg() {
|
||||
const rBounds = useRef<SVGRectElement>(null)
|
||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||
const isSelecting = useSelector((s) => s.isIn("selecting"))
|
||||
const rotation = useSelector((s) => {
|
||||
if (s.data.selectedIds.size === 1) {
|
||||
const { shapes } = s.data.document.pages[s.data.currentPageId]
|
||||
const selected = Array.from(s.data.selectedIds.values())[0]
|
||||
return shapes[selected].rotation
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!bounds) return null
|
||||
if (!isSelecting) return null
|
||||
|
||||
const { minX, minY, width, height } = bounds
|
||||
|
||||
return (
|
||||
<StyledBoundsBg
|
||||
ref={rBounds}
|
||||
x={minX}
|
||||
y={minY}
|
||||
width={Math.max(1, width)}
|
||||
height={Math.max(1, height)}
|
||||
onPointerDown={(e) => {
|
||||
if (e.buttons !== 1) return
|
||||
e.stopPropagation()
|
||||
rBounds.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
|
||||
}}
|
||||
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
|
||||
minY + height / 2
|
||||
})`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledBoundsBg = styled("rect", {
|
||||
fill: "$boundsBg",
|
||||
})
|
|
@ -1,285 +0,0 @@
|
|||
import state, { useSelector } from "state"
|
||||
import styled from "styles"
|
||||
import inputs from "state/inputs"
|
||||
import { useRef } from "react"
|
||||
import { TransformCorner, TransformEdge } from "types"
|
||||
import { lerp } from "utils/utils"
|
||||
|
||||
export default function Bounds() {
|
||||
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
|
||||
const isSelecting = useSelector((s) => s.isIn("selecting"))
|
||||
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||
const rotation = useSelector((s) => {
|
||||
if (s.data.selectedIds.size === 1) {
|
||||
const { shapes } = s.data.document.pages[s.data.currentPageId]
|
||||
const selected = Array.from(s.data.selectedIds.values())[0]
|
||||
return shapes[selected].rotation
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!bounds) return null
|
||||
if (!isSelecting) return null
|
||||
|
||||
let { minX, minY, maxX, maxY, width, height } = bounds
|
||||
|
||||
const p = 4 / zoom
|
||||
const cp = p * 2
|
||||
|
||||
return (
|
||||
<g
|
||||
pointerEvents={isBrushing ? "none" : "all"}
|
||||
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
|
||||
minY + height / 2
|
||||
})`}
|
||||
>
|
||||
<StyledBounds
|
||||
x={minX}
|
||||
y={minY}
|
||||
width={width}
|
||||
height={height}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<EdgeHorizontal
|
||||
x={minX + p}
|
||||
y={minY}
|
||||
width={Math.max(0, width - p * 2)}
|
||||
height={p}
|
||||
edge={TransformEdge.Top}
|
||||
/>
|
||||
<EdgeVertical
|
||||
x={maxX}
|
||||
y={minY + p}
|
||||
width={p}
|
||||
height={Math.max(0, height - p * 2)}
|
||||
edge={TransformEdge.Right}
|
||||
/>
|
||||
<EdgeHorizontal
|
||||
x={minX + p}
|
||||
y={maxY}
|
||||
width={Math.max(0, width - p * 2)}
|
||||
height={p}
|
||||
edge={TransformEdge.Bottom}
|
||||
/>
|
||||
<EdgeVertical
|
||||
x={minX}
|
||||
y={minY + p}
|
||||
width={p}
|
||||
height={Math.max(0, height - p * 2)}
|
||||
edge={TransformEdge.Left}
|
||||
/>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={minY}
|
||||
width={cp}
|
||||
height={cp}
|
||||
corner={TransformCorner.TopLeft}
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={minY}
|
||||
width={cp}
|
||||
height={cp}
|
||||
corner={TransformCorner.TopRight}
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={maxY}
|
||||
width={cp}
|
||||
height={cp}
|
||||
corner={TransformCorner.BottomRight}
|
||||
/>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={maxY}
|
||||
width={cp}
|
||||
height={cp}
|
||||
corner={TransformCorner.BottomLeft}
|
||||
/>
|
||||
<RotateHandle x={minX + width / 2} y={minY - cp * 2} r={cp / 2} />
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
function RotateHandle({ x, y, r }: { x: number; y: number; r: number }) {
|
||||
const rRotateHandle = useRef<SVGCircleElement>(null)
|
||||
|
||||
return (
|
||||
<StyledRotateHandle
|
||||
ref={rRotateHandle}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={r}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
rRotateHandle.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_ROTATE_HANDLE", inputs.pointerDown(e, "rotate"))
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
e.stopPropagation()
|
||||
rRotateHandle.current.releasePointerCapture(e.pointerId)
|
||||
rRotateHandle.current.replaceWith(rRotateHandle.current)
|
||||
state.send("STOPPED_POINTING", inputs.pointerDown(e, "rotate"))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Corner({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
corner,
|
||||
}: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
corner: TransformCorner
|
||||
}) {
|
||||
const rCorner = useRef<SVGRectElement>(null)
|
||||
|
||||
return (
|
||||
<g>
|
||||
<StyledCorner
|
||||
ref={rCorner}
|
||||
x={x + width * -0.5}
|
||||
y={y + height * -0.5}
|
||||
width={width}
|
||||
height={height}
|
||||
corner={corner}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
rCorner.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
e.stopPropagation()
|
||||
rCorner.current.releasePointerCapture(e.pointerId)
|
||||
rCorner.current.replaceWith(rCorner.current)
|
||||
state.send("STOPPED_POINTING", inputs.pointerDown(e, corner))
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
function EdgeHorizontal({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
edge,
|
||||
}: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
edge: TransformEdge.Top | TransformEdge.Bottom
|
||||
}) {
|
||||
const rEdge = useRef<SVGRectElement>(null)
|
||||
|
||||
return (
|
||||
<StyledEdge
|
||||
ref={rEdge}
|
||||
x={x}
|
||||
y={y - height / 2}
|
||||
width={width}
|
||||
height={height}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
rEdge.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
state.send("STOPPED_POINTING", inputs.pointerUp(e))
|
||||
rEdge.current.releasePointerCapture(e.pointerId)
|
||||
rEdge.current.replaceWith(rEdge.current)
|
||||
}}
|
||||
edge={edge}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EdgeVertical({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
edge,
|
||||
}: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
edge: TransformEdge.Right | TransformEdge.Left
|
||||
}) {
|
||||
const rEdge = useRef<SVGRectElement>(null)
|
||||
|
||||
return (
|
||||
<StyledEdge
|
||||
ref={rEdge}
|
||||
x={x - width / 2}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
|
||||
rEdge.current.setPointerCapture(e.pointerId)
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
e.stopPropagation()
|
||||
state.send("STOPPED_POINTING", inputs.pointerUp(e))
|
||||
rEdge.current.releasePointerCapture(e.pointerId)
|
||||
rEdge.current.replaceWith(rEdge.current)
|
||||
}}
|
||||
edge={edge}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledEdge = styled("rect", {
|
||||
stroke: "none",
|
||||
fill: "none",
|
||||
variants: {
|
||||
edge: {
|
||||
bottom_edge: { cursor: "ns-resize" },
|
||||
right_edge: { cursor: "ew-resize" },
|
||||
top_edge: { cursor: "ns-resize" },
|
||||
left_edge: { cursor: "ew-resize" },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledCorner = styled("rect", {
|
||||
stroke: "$bounds",
|
||||
fill: "#fff",
|
||||
zStrokeWidth: 2,
|
||||
variants: {
|
||||
corner: {
|
||||
top_left_corner: { cursor: "nwse-resize" },
|
||||
top_right_corner: { cursor: "nesw-resize" },
|
||||
bottom_right_corner: { cursor: "nwse-resize" },
|
||||
bottom_left_corner: { cursor: "nesw-resize" },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledRotateHandle = styled("circle", {
|
||||
stroke: "$bounds",
|
||||
fill: "#fff",
|
||||
zStrokeWidth: 2,
|
||||
cursor: "grab",
|
||||
})
|
||||
|
||||
const StyledBounds = styled("rect", {
|
||||
fill: "none",
|
||||
stroke: "$bounds",
|
||||
zStrokeWidth: 2,
|
||||
})
|
46
components/canvas/bounds/bounding-box.tsx
Normal file
46
components/canvas/bounds/bounding-box.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Edge, Corner } from "types"
|
||||
import { useSelector } from "state"
|
||||
import { getSelectedShapes, isMobile } from "utils/utils"
|
||||
|
||||
import CenterHandle from "./center-handle"
|
||||
import CornerHandle from "./corner-handle"
|
||||
import EdgeHandle from "./edge-handle"
|
||||
import RotateHandle from "./rotate-handle"
|
||||
|
||||
export default function Bounds() {
|
||||
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
|
||||
const isSelecting = useSelector((s) => s.isIn("selecting"))
|
||||
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||
const rotation = useSelector(({ data }) =>
|
||||
data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
|
||||
)
|
||||
|
||||
if (!bounds) return null
|
||||
if (!isSelecting) return null
|
||||
|
||||
const size = (isMobile().any ? 16 : 8) / zoom // Touch target size
|
||||
|
||||
return (
|
||||
<g
|
||||
pointerEvents={isBrushing ? "none" : "all"}
|
||||
transform={`
|
||||
rotate(${rotation * (180 / Math.PI)},
|
||||
${(bounds.minX + bounds.maxX) / 2},
|
||||
${(bounds.minY + bounds.maxY) / 2})
|
||||
translate(${bounds.minX},${bounds.minY})`}
|
||||
>
|
||||
<CenterHandle bounds={bounds} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Bottom} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Left} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={Corner.TopLeft} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={Corner.TopRight} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomRight} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomLeft} />
|
||||
<RotateHandle size={size} bounds={bounds} />
|
||||
</g>
|
||||
)
|
||||
}
|
58
components/canvas/bounds/bounds-bg.tsx
Normal file
58
components/canvas/bounds/bounds-bg.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { useCallback, useRef } from "react"
|
||||
import state, { useSelector } from "state"
|
||||
import inputs from "state/inputs"
|
||||
import styled from "styles"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
||||
if (e.buttons !== 1) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
|
||||
}
|
||||
|
||||
function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
|
||||
if (e.buttons !== 1) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
state.send("STOPPED_POINTING", inputs.pointerUp(e))
|
||||
}
|
||||
|
||||
export default function BoundsBg() {
|
||||
const rBounds = useRef<SVGRectElement>(null)
|
||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||
const isSelecting = useSelector((s) => s.isIn("selecting"))
|
||||
const rotation = useSelector((s) => {
|
||||
if (s.data.selectedIds.size === 1) {
|
||||
const { shapes } = getPage(s.data)
|
||||
const selected = Array.from(s.data.selectedIds.values())[0]
|
||||
return shapes[selected].rotation
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!bounds) return null
|
||||
if (!isSelecting) return null
|
||||
|
||||
const { width, height } = bounds
|
||||
|
||||
return (
|
||||
<StyledBoundsBg
|
||||
ref={rBounds}
|
||||
width={Math.max(1, width)}
|
||||
height={Math.max(1, height)}
|
||||
transform={`
|
||||
rotate(${rotation * (180 / Math.PI)},
|
||||
${(bounds.minX + bounds.maxX) / 2},
|
||||
${(bounds.minY + bounds.maxY) / 2})
|
||||
translate(${bounds.minX},${bounds.minY})`}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledBoundsBg = styled("rect", {
|
||||
fill: "$boundsBg",
|
||||
})
|
18
components/canvas/bounds/center-handle.tsx
Normal file
18
components/canvas/bounds/center-handle.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import styled from "styles"
|
||||
import { Bounds } from "types"
|
||||
|
||||
export default function CenterHandle({ bounds }: { bounds: Bounds }) {
|
||||
return (
|
||||
<StyledBounds
|
||||
width={bounds.width}
|
||||
height={bounds.height}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledBounds = styled("rect", {
|
||||
fill: "none",
|
||||
stroke: "$bounds",
|
||||
zStrokeWidth: 2,
|
||||
})
|
43
components/canvas/bounds/corner-handle.tsx
Normal file
43
components/canvas/bounds/corner-handle.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import useHandleEvents from "hooks/useBoundsHandleEvents"
|
||||
import styled from "styles"
|
||||
import { Corner, Bounds } from "types"
|
||||
|
||||
export default function CornerHandle({
|
||||
size,
|
||||
corner,
|
||||
bounds,
|
||||
}: {
|
||||
size: number
|
||||
bounds: Bounds
|
||||
corner: Corner
|
||||
}) {
|
||||
const events = useHandleEvents(corner)
|
||||
|
||||
const isTop = corner === Corner.TopLeft || corner === Corner.TopRight
|
||||
const isLeft = corner === Corner.TopLeft || corner === Corner.BottomLeft
|
||||
|
||||
return (
|
||||
<StyledCorner
|
||||
corner={corner}
|
||||
x={(isLeft ? 0 : bounds.width) - size / 2}
|
||||
y={(isTop ? 0 : bounds.height) - size / 2}
|
||||
width={size}
|
||||
height={size}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledCorner = styled("rect", {
|
||||
stroke: "$bounds",
|
||||
fill: "#fff",
|
||||
zStrokeWidth: 2,
|
||||
variants: {
|
||||
corner: {
|
||||
[Corner.TopLeft]: { cursor: "nwse-resize" },
|
||||
[Corner.TopRight]: { cursor: "nesw-resize" },
|
||||
[Corner.BottomRight]: { cursor: "nwse-resize" },
|
||||
[Corner.BottomLeft]: { cursor: "nesw-resize" },
|
||||
},
|
||||
},
|
||||
})
|
42
components/canvas/bounds/edge-handle.tsx
Normal file
42
components/canvas/bounds/edge-handle.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import useHandleEvents from "hooks/useBoundsHandleEvents"
|
||||
import styled from "styles"
|
||||
import { Edge, Bounds } from "types"
|
||||
|
||||
export default function EdgeHandle({
|
||||
size,
|
||||
bounds,
|
||||
edge,
|
||||
}: {
|
||||
size: number
|
||||
bounds: Bounds
|
||||
edge: Edge
|
||||
}) {
|
||||
const events = useHandleEvents(edge)
|
||||
|
||||
const isHorizontal = edge === Edge.Top || edge === Edge.Bottom
|
||||
const isFarEdge = edge === Edge.Right || edge === Edge.Bottom
|
||||
|
||||
return (
|
||||
<StyledEdge
|
||||
edge={edge}
|
||||
x={isHorizontal ? size / 2 : (isFarEdge ? bounds.width : 0) - size / 2}
|
||||
y={isHorizontal ? (isFarEdge ? bounds.height : 0) - size / 2 : size / 2}
|
||||
width={isHorizontal ? Math.max(0, bounds.width - size) : size}
|
||||
height={isHorizontal ? size : Math.max(0, bounds.height - size)}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledEdge = styled("rect", {
|
||||
stroke: "none",
|
||||
fill: "none",
|
||||
variants: {
|
||||
edge: {
|
||||
[Edge.Top]: { cursor: "ns-resize" },
|
||||
[Edge.Right]: { cursor: "ew-resize" },
|
||||
[Edge.Bottom]: { cursor: "ns-resize" },
|
||||
[Edge.Left]: { cursor: "ew-resize" },
|
||||
},
|
||||
},
|
||||
})
|
30
components/canvas/bounds/rotate-handle.tsx
Normal file
30
components/canvas/bounds/rotate-handle.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import useHandleEvents from "hooks/useBoundsHandleEvents"
|
||||
import styled from "styles"
|
||||
import { Bounds } from "types"
|
||||
|
||||
export default function Rotate({
|
||||
bounds,
|
||||
size,
|
||||
}: {
|
||||
bounds: Bounds
|
||||
size: number
|
||||
}) {
|
||||
const events = useHandleEvents("rotate")
|
||||
|
||||
return (
|
||||
<StyledRotateHandle
|
||||
cursor="grab"
|
||||
cx={bounds.width / 2}
|
||||
cy={size * -2}
|
||||
r={size / 2}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledRotateHandle = styled("circle", {
|
||||
stroke: "$bounds",
|
||||
fill: "#fff",
|
||||
zStrokeWidth: 2,
|
||||
cursor: "grab",
|
||||
})
|
|
@ -5,8 +5,8 @@ import useCamera from "hooks/useCamera"
|
|||
import Page from "./page"
|
||||
import Brush from "./brush"
|
||||
import state from "state"
|
||||
import Bounds from "./bounds"
|
||||
import BoundsBg from "./bounds-bg"
|
||||
import Bounds from "./bounds/bounding-box"
|
||||
import BoundsBg from "./bounds/bounds-bg"
|
||||
import inputs from "state/inputs"
|
||||
|
||||
export default function Canvas() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useSelector } from "state"
|
||||
import { deepCompareArrays } from "utils/utils"
|
||||
import { deepCompareArrays, getPage } from "utils/utils"
|
||||
import Shape from "./shape"
|
||||
|
||||
/*
|
||||
|
@ -10,7 +10,7 @@ here; and still cheaper than any other pattern I've found.
|
|||
|
||||
export default function Page() {
|
||||
const currentPageShapeIds = useSelector(
|
||||
({ data }) => Object.keys(data.document.pages[data.currentPageId].shapes),
|
||||
({ data }) => Object.keys(getPage(data).shapes),
|
||||
deepCompareArrays
|
||||
)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import state, { useSelector } from "state"
|
|||
import inputs from "state/inputs"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import styled from "styles"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
function Shape({ id }: { id: string }) {
|
||||
const rGroup = useRef<SVGGElement>(null)
|
||||
|
@ -11,9 +12,7 @@ function Shape({ id }: { id: string }) {
|
|||
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
|
||||
const shape = useSelector(
|
||||
({ data }) => data.document.pages[data.currentPageId].shapes[id]
|
||||
)
|
||||
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
|
|
38
hooks/useBoundsHandleEvents.ts
Normal file
38
hooks/useBoundsHandleEvents.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { useCallback, useRef } from "react"
|
||||
import inputs from "state/inputs"
|
||||
import { Edge, Corner } from "types"
|
||||
|
||||
import state from "../state"
|
||||
|
||||
export default function useBoundsHandleEvents(
|
||||
handle: Edge | Corner | "rotate"
|
||||
) {
|
||||
const onPointerDown = useCallback(
|
||||
(e) => {
|
||||
if (e.buttons !== 1) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_BOUNDS_HANDLE", inputs.pointerDown(e, handle))
|
||||
},
|
||||
[handle]
|
||||
)
|
||||
|
||||
const onPointerMove = useCallback(
|
||||
(e) => {
|
||||
if (e.buttons !== 1) return
|
||||
e.stopPropagation()
|
||||
state.send("MOVED_POINTER", inputs.pointerMove(e))
|
||||
},
|
||||
[handle]
|
||||
)
|
||||
|
||||
const onPointerUp = useCallback((e) => {
|
||||
if (e.buttons !== 1) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
e.currentTarget.replaceWith(e.currentTarget)
|
||||
state.send("STOPPED_POINTING", inputs.pointerUp(e))
|
||||
}, [])
|
||||
|
||||
return { onPointerDown, onPointerMove, onPointerUp }
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { CircleShape, ShapeType, TransformCorner, TransformEdge } from "types"
|
||||
import { CircleShape, ShapeType, Corner, Edge } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
|
@ -99,7 +99,7 @@ const circle = registerShapeUtils<CircleShape>({
|
|||
|
||||
// Set the new corner or position depending on the anchor
|
||||
switch (anchor) {
|
||||
case TransformCorner.TopLeft: {
|
||||
case Corner.TopLeft: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
|
@ -107,12 +107,12 @@ const circle = registerShapeUtils<CircleShape>({
|
|||
]
|
||||
break
|
||||
}
|
||||
case TransformCorner.TopRight: {
|
||||
case Corner.TopRight: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomRight: {
|
||||
case Corner.BottomRight: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
|
@ -121,12 +121,12 @@ const circle = registerShapeUtils<CircleShape>({
|
|||
break
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomLeft: {
|
||||
case Corner.BottomLeft: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Top: {
|
||||
case Edge.Top: {
|
||||
shape.radius = bounds.height / 2
|
||||
shape.point = [
|
||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
||||
|
@ -134,7 +134,7 @@ const circle = registerShapeUtils<CircleShape>({
|
|||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Right: {
|
||||
case Edge.Right: {
|
||||
shape.radius = bounds.width / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
|
@ -142,7 +142,7 @@ const circle = registerShapeUtils<CircleShape>({
|
|||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Bottom: {
|
||||
case Edge.Bottom: {
|
||||
shape.radius = bounds.height / 2
|
||||
shape.point = [
|
||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
||||
|
@ -150,7 +150,7 @@ const circle = registerShapeUtils<CircleShape>({
|
|||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Left: {
|
||||
case Edge.Left: {
|
||||
shape.radius = bounds.width / 2
|
||||
shape.point = [
|
||||
bounds.minX,
|
||||
|
|
|
@ -4,8 +4,8 @@ import {
|
|||
Shape,
|
||||
Shapes,
|
||||
ShapeType,
|
||||
TransformCorner,
|
||||
TransformEdge,
|
||||
Corner,
|
||||
Edge,
|
||||
} from "types"
|
||||
import circle from "./circle"
|
||||
import dot from "./dot"
|
||||
|
@ -60,10 +60,11 @@ export interface ShapeUtility<K extends Shape> {
|
|||
shape: K,
|
||||
bounds: Bounds,
|
||||
info: {
|
||||
type: TransformEdge | TransformCorner | "center"
|
||||
type: Edge | Corner | "center"
|
||||
initialShape: K
|
||||
scaleX: number
|
||||
scaleY: number
|
||||
transformOrigin: number[]
|
||||
}
|
||||
): K
|
||||
|
||||
|
@ -72,10 +73,11 @@ export interface ShapeUtility<K extends Shape> {
|
|||
shape: K,
|
||||
bounds: Bounds,
|
||||
info: {
|
||||
type: TransformEdge | TransformCorner | "center"
|
||||
type: Edge | Corner | "center"
|
||||
initialShape: K
|
||||
scaleX: number
|
||||
scaleY: number
|
||||
transformOrigin: number[]
|
||||
}
|
||||
): K
|
||||
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import {
|
||||
RectangleShape,
|
||||
ShapeType,
|
||||
TransformCorner,
|
||||
TransformEdge,
|
||||
} from "types"
|
||||
import { RectangleShape, ShapeType, Corner, Edge } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
|
||||
import {
|
||||
getBoundsFromPoints,
|
||||
getRotatedCorners,
|
||||
getRotatedSize,
|
||||
lerp,
|
||||
rotateBounds,
|
||||
translateBounds,
|
||||
} from "utils/utils"
|
||||
|
@ -99,27 +96,33 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
|
||||
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
|
||||
if (shape.rotation === 0) {
|
||||
shape.size = [bounds.width, bounds.height]
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
} else {
|
||||
// Center shape in resized bounds
|
||||
// Size
|
||||
shape.size = vec.mul(
|
||||
initialShape.size,
|
||||
Math.min(Math.abs(scaleX), Math.abs(scaleY))
|
||||
)
|
||||
|
||||
shape.point = vec.sub(
|
||||
vec.med([bounds.minX, bounds.minY], [bounds.maxX, bounds.maxY]),
|
||||
vec.div(shape.size, 2)
|
||||
)
|
||||
}
|
||||
// Point
|
||||
shape.point = [
|
||||
bounds.minX +
|
||||
(bounds.width - shape.size[0]) *
|
||||
(scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
|
||||
bounds.minY +
|
||||
(bounds.height - shape.size[1]) *
|
||||
(scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
|
||||
]
|
||||
|
||||
// Set rotation for flipped shapes
|
||||
shape.rotation = initialShape.rotation
|
||||
if (scaleX < 0) shape.rotation *= -1
|
||||
if (scaleY < 0) shape.rotation *= -1
|
||||
// Rotation
|
||||
shape.rotation =
|
||||
(scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
|
||||
? -initialShape.rotation
|
||||
: initialShape.rotation
|
||||
}
|
||||
|
||||
return shape
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@stitches/react": "^0.1.9",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"framer-motion": "^4.1.16",
|
||||
"ismobilejs": "^1.1.1",
|
||||
"next": "10.2.0",
|
||||
"perfect-freehand": "^0.4.7",
|
||||
"prettier": "^2.3.0",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
@ -11,17 +12,17 @@ export default function registerShapeUtilsCommand(data: Data, shape: Shape) {
|
|||
name: "translate_shapes",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
const page = getPage(data)
|
||||
|
||||
shapes[shape.id] = shape
|
||||
page.shapes[shape.id] = shape
|
||||
data.selectedIds.clear()
|
||||
data.pointedId = undefined
|
||||
data.hoveredId = undefined
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
const page = getPage(data)
|
||||
|
||||
delete shapes[shape.id]
|
||||
delete page.shapes[shape.id]
|
||||
|
||||
data.selectedIds.clear()
|
||||
data.pointedId = undefined
|
||||
|
|
|
@ -2,6 +2,7 @@ import Command from "./command"
|
|||
import history from "../history"
|
||||
import { DirectionSnapshot } from "state/sessions/direction-session"
|
||||
import { Data, LineShape, RayShape } from "types"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
export default function directCommand(
|
||||
data: Data,
|
||||
|
@ -14,7 +15,7 @@ export default function directCommand(
|
|||
name: "set_direction",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapes } = data.document.pages[after.currentPageId]
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (let { id, direction } of after.shapes) {
|
||||
const shape = shapes[id] as RayShape | LineShape
|
||||
|
@ -23,7 +24,7 @@ export default function directCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = data.document.pages[before.currentPageId]
|
||||
const { shapes } = getPage(data, before.currentPageId)
|
||||
|
||||
for (let { id, direction } of after.shapes) {
|
||||
const shape = shapes[id] as RayShape | LineShape
|
||||
|
|
|
@ -2,6 +2,7 @@ import Command from "./command"
|
|||
import history from "../history"
|
||||
import { CodeControl, Data, Shape } from "types"
|
||||
import { current } from "immer"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
export default function generateCommand(
|
||||
data: Data,
|
||||
|
@ -9,12 +10,13 @@ export default function generateCommand(
|
|||
generatedShapes: Shape[]
|
||||
) {
|
||||
const cData = current(data)
|
||||
const page = getPage(cData)
|
||||
|
||||
const prevGeneratedShapes = Object.values(
|
||||
cData.document.pages[currentPageId].shapes
|
||||
).filter((shape) => shape.isGenerated)
|
||||
const currentShapes = page.shapes
|
||||
|
||||
const currentShapes = data.document.pages[currentPageId].shapes
|
||||
const prevGeneratedShapes = Object.values(currentShapes).filter(
|
||||
(shape) => shape.isGenerated
|
||||
)
|
||||
|
||||
// Remove previous generated shapes
|
||||
for (let id in currentShapes) {
|
||||
|
@ -34,7 +36,7 @@ export default function generateCommand(
|
|||
name: "translate_shapes",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
data.selectedIds.clear()
|
||||
|
||||
|
@ -51,7 +53,7 @@ export default function generateCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
// Remove generated shapes
|
||||
for (let id in shapes) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import Command from "./command"
|
|||
import history from "../history"
|
||||
import { Data } from "types"
|
||||
import { RotateSnapshot } from "state/sessions/rotate-session"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
export default function rotateCommand(
|
||||
data: Data,
|
||||
|
@ -14,7 +15,7 @@ export default function rotateCommand(
|
|||
name: "translate_shapes",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapes } = data.document.pages[after.currentPageId]
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (let { id, point, rotation } of after.shapes) {
|
||||
const shape = shapes[id]
|
||||
|
@ -25,7 +26,7 @@ export default function rotateCommand(
|
|||
data.boundsRotation = after.boundsRotation
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = data.document.pages[before.currentPageId]
|
||||
const { shapes } = getPage(data, before.currentPageId)
|
||||
|
||||
for (let { id, point, rotation } of before.shapes) {
|
||||
const shape = shapes[id]
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import Command from "./command"
|
||||
import history from "../history"
|
||||
import { Data, TransformCorner, TransformEdge } from "types"
|
||||
import { Data, Corner, Edge } from "types"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import { current } from "immer"
|
||||
import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
export default function transformSingleCommand(
|
||||
data: Data,
|
||||
|
@ -13,8 +14,7 @@ export default function transformSingleCommand(
|
|||
scaleY: number,
|
||||
isCreating: boolean
|
||||
) {
|
||||
const shape =
|
||||
current(data).document.pages[after.currentPageId].shapes[after.id]
|
||||
const shape = getPage(data, after.currentPageId).shapes[after.id]
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
|
@ -23,32 +23,36 @@ export default function transformSingleCommand(
|
|||
category: "canvas",
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const { id, currentPageId, type, initialShape, initialShapeBounds } =
|
||||
after
|
||||
const { id, type, initialShape, initialShapeBounds } = after
|
||||
|
||||
const { shapes } = getPage(data, after.currentPageId)
|
||||
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(id)
|
||||
|
||||
if (isCreating) {
|
||||
data.document.pages[currentPageId].shapes[id] = shape
|
||||
shapes[id] = shape
|
||||
} else {
|
||||
getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
|
||||
type,
|
||||
initialShape,
|
||||
scaleX,
|
||||
scaleY,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
})
|
||||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { id, currentPageId, type, initialShapeBounds } = before
|
||||
const { id, type, initialShapeBounds } = before
|
||||
|
||||
const { shapes } = getPage(data, before.currentPageId)
|
||||
|
||||
data.selectedIds.clear()
|
||||
|
||||
if (isCreating) {
|
||||
delete data.document.pages[currentPageId].shapes[id]
|
||||
delete shapes[id]
|
||||
} else {
|
||||
const shape = data.document.pages[currentPageId].shapes[id]
|
||||
const shape = shapes[id]
|
||||
data.selectedIds.add(id)
|
||||
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
|
@ -56,6 +60,7 @@ export default function transformSingleCommand(
|
|||
initialShape: after.initialShape,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Command from "./command"
|
||||
import history from "../history"
|
||||
import { Data, TransformCorner, TransformEdge } from "types"
|
||||
import { Data, Corner, Edge } from "types"
|
||||
import { TransformSnapshot } from "state/sessions/transform-session"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
export default function transformCommand(
|
||||
data: Data,
|
||||
|
@ -17,32 +18,40 @@ export default function transformCommand(
|
|||
name: "translate_shapes",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { type, currentPageId, selectedIds } = after
|
||||
const { type, selectedIds } = after
|
||||
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const { initialShape, initialShapeBounds } = after.shapeBounds[id]
|
||||
const shape = data.document.pages[currentPageId].shapes[id]
|
||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||
after.shapeBounds[id]
|
||||
const shape = shapes[id]
|
||||
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
type,
|
||||
initialShape,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
transformOrigin,
|
||||
})
|
||||
})
|
||||
},
|
||||
undo(data) {
|
||||
const { type, currentPageId, selectedIds } = before
|
||||
const { type, selectedIds } = before
|
||||
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const { initialShape, initialShapeBounds } = before.shapeBounds[id]
|
||||
const shape = data.document.pages[currentPageId].shapes[id]
|
||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||
before.shapeBounds[id]
|
||||
const shape = shapes[id]
|
||||
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
type,
|
||||
initialShape,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
transformOrigin,
|
||||
})
|
||||
})
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ import Command from "./command"
|
|||
import history from "../history"
|
||||
import { TranslateSnapshot } from "state/sessions/translate-session"
|
||||
import { Data } from "types"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
export default function translateCommand(
|
||||
data: Data,
|
||||
|
@ -18,8 +19,8 @@ export default function translateCommand(
|
|||
do(data, initial) {
|
||||
if (initial) return
|
||||
|
||||
const { shapes } = data.document.pages[after.currentPageId]
|
||||
const { initialShapes } = after
|
||||
const { initialShapes, currentPageId } = after
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { clones } = before // !
|
||||
|
||||
data.selectedIds.clear()
|
||||
|
@ -36,8 +37,8 @@ export default function translateCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = data.document.pages[before.currentPageId]
|
||||
const { initialShapes, clones } = before
|
||||
const { initialShapes, clones, currentPageId } = before
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
|
||||
data.selectedIds.clear()
|
||||
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import { current } from "immer"
|
||||
import { ShapeUtil, Bounds, Data, Shapes } from "types"
|
||||
import { Bounds, Data } from "types"
|
||||
import BaseSession from "./base-session"
|
||||
import shapes, { getShapeUtils } from "lib/shape-utils"
|
||||
import { getBoundsFromPoints } from "utils/utils"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import { getBoundsFromPoints, getShapes } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
|
||||
interface BrushSnapshot {
|
||||
selectedIds: Set<string>
|
||||
shapes: { id: string; test: (bounds: Bounds) => boolean }[]
|
||||
}
|
||||
|
||||
export default class BrushSession extends BaseSession {
|
||||
origin: number[]
|
||||
snapshot: BrushSnapshot
|
||||
|
@ -19,7 +14,7 @@ export default class BrushSession extends BaseSession {
|
|||
|
||||
this.origin = vec.round(point)
|
||||
|
||||
this.snapshot = BrushSession.getSnapshot(data)
|
||||
this.snapshot = getBrushSnapshot(data)
|
||||
}
|
||||
|
||||
update = (data: Data, point: number[]) => {
|
||||
|
@ -27,7 +22,8 @@ export default class BrushSession extends BaseSession {
|
|||
|
||||
const brushBounds = getBoundsFromPoints([origin, point])
|
||||
|
||||
for (let { test, id } of snapshot.shapes) {
|
||||
for (let id in snapshot.shapeHitTests) {
|
||||
const test = snapshot.shapeHitTests[id]
|
||||
if (test(brushBounds)) {
|
||||
data.selectedIds.add(id)
|
||||
} else if (data.selectedIds.has(id)) {
|
||||
|
@ -46,30 +42,23 @@ export default class BrushSession extends BaseSession {
|
|||
complete = (data: Data) => {
|
||||
data.brush = undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snapshot of the current selected ids, for each shape that is
|
||||
* not already selected, the shape's id and a test to see whether the
|
||||
* brush will intersect that shape. For tests, start broad -> fine.
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
static getSnapshot(data: Data): BrushSnapshot {
|
||||
const {
|
||||
selectedIds,
|
||||
document: { pages },
|
||||
currentPageId,
|
||||
} = current(data)
|
||||
|
||||
return {
|
||||
selectedIds: new Set(data.selectedIds),
|
||||
shapes: Object.values(pages[currentPageId].shapes)
|
||||
.filter((shape) => !selectedIds.has(shape.id))
|
||||
.map((shape) => ({
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds): boolean =>
|
||||
getShapeUtils(shape).hitTestBounds(shape, brushBounds),
|
||||
})),
|
||||
}
|
||||
/**
|
||||
* Get a snapshot of the current selected ids, for each shape that is
|
||||
* not already selected, the shape's id and a test to see whether the
|
||||
* brush will intersect that shape. For tests, start broad -> fine.
|
||||
*/
|
||||
export function getBrushSnapshot(data: Data) {
|
||||
return {
|
||||
selectedIds: new Set(data.selectedIds),
|
||||
shapeHitTests: Object.fromEntries(
|
||||
getShapes(current(data)).map((shape) => [
|
||||
shape.id,
|
||||
(bounds: Bounds) => getShapeUtils(shape).hitTestBounds(shape, bounds),
|
||||
])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as vec from "utils/vec"
|
|||
import BaseSession from "./base-session"
|
||||
import commands from "state/commands"
|
||||
import { current } from "immer"
|
||||
import { getPage } from "utils/utils"
|
||||
|
||||
export default class DirectionSession extends BaseSession {
|
||||
delta = [0, 0]
|
||||
|
@ -16,26 +17,22 @@ export default class DirectionSession extends BaseSession {
|
|||
}
|
||||
|
||||
update(data: Data, point: number[]) {
|
||||
const { currentPageId, shapes } = this.snapshot
|
||||
const { document } = data
|
||||
const { shapes } = this.snapshot
|
||||
|
||||
const page = getPage(data)
|
||||
|
||||
for (let { id } of shapes) {
|
||||
const shape = document.pages[currentPageId].shapes[id] as
|
||||
| RayShape
|
||||
| LineShape
|
||||
const shape = page.shapes[id] as RayShape | LineShape
|
||||
|
||||
shape.direction = vec.uni(vec.vec(shape.point, point))
|
||||
}
|
||||
}
|
||||
|
||||
cancel(data: Data) {
|
||||
const { document } = data
|
||||
const page = getPage(data, this.snapshot.currentPageId)
|
||||
|
||||
for (let { id, direction } of this.snapshot.shapes) {
|
||||
const shape = document.pages[this.snapshot.currentPageId].shapes[id] as
|
||||
| RayShape
|
||||
| LineShape
|
||||
|
||||
const shape = page.shapes[id] as RayShape | LineShape
|
||||
shape.direction = direction
|
||||
}
|
||||
}
|
||||
|
@ -46,12 +43,7 @@ export default class DirectionSession extends BaseSession {
|
|||
}
|
||||
|
||||
export function getDirectionSnapshot(data: Data) {
|
||||
const {
|
||||
document: { pages },
|
||||
currentPageId,
|
||||
} = current(data)
|
||||
|
||||
const { shapes } = pages[currentPageId]
|
||||
const { shapes } = getPage(current(data))
|
||||
|
||||
let snapshapes: { id: string; direction: number[] }[] = []
|
||||
|
||||
|
@ -63,7 +55,7 @@ export function getDirectionSnapshot(data: Data) {
|
|||
})
|
||||
|
||||
return {
|
||||
currentPageId,
|
||||
currentPageId: data.currentPageId,
|
||||
shapes: snapshapes,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,15 @@ import * as vec from "utils/vec"
|
|||
import BaseSession from "./base-session"
|
||||
import commands from "state/commands"
|
||||
import { current } from "immer"
|
||||
import { getCommonBounds } from "utils/utils"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import {
|
||||
getBoundsCenter,
|
||||
getCommonBounds,
|
||||
getPage,
|
||||
getSelectedShapes,
|
||||
getShapeBounds,
|
||||
} from "utils/utils"
|
||||
|
||||
const PI2 = Math.PI * 2
|
||||
|
||||
export default class RotateSession extends BaseSession {
|
||||
delta = [0, 0]
|
||||
|
@ -17,33 +24,34 @@ export default class RotateSession extends BaseSession {
|
|||
this.snapshot = getRotateSnapshot(data)
|
||||
}
|
||||
|
||||
update(data: Data, point: number[]) {
|
||||
const { currentPageId, boundsCenter, shapes } = this.snapshot
|
||||
const { document } = data
|
||||
update(data: Data, point: number[], isLocked: boolean) {
|
||||
const { boundsCenter, shapes } = this.snapshot
|
||||
|
||||
const page = getPage(data)
|
||||
const a1 = vec.angle(boundsCenter, this.origin)
|
||||
const a2 = vec.angle(boundsCenter, point)
|
||||
|
||||
data.boundsRotation =
|
||||
(this.snapshot.boundsRotation + (a2 - a1)) % (Math.PI * 2)
|
||||
let rot = (PI2 + (a2 - a1)) % PI2
|
||||
|
||||
if (isLocked) {
|
||||
rot = Math.floor((rot + Math.PI / 8) / (Math.PI / 4)) * (Math.PI / 4)
|
||||
}
|
||||
|
||||
data.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2
|
||||
|
||||
for (let { id, center, offset, rotation } of shapes) {
|
||||
const shape = document.pages[currentPageId].shapes[id]
|
||||
shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
|
||||
const newCenter = vec.rotWith(
|
||||
center,
|
||||
boundsCenter,
|
||||
(a2 - a1) % (Math.PI * 2)
|
||||
)
|
||||
const shape = page.shapes[id]
|
||||
shape.rotation = (PI2 + (rotation + rot)) % PI2
|
||||
const newCenter = vec.rotWith(center, boundsCenter, rot % PI2)
|
||||
shape.point = vec.sub(newCenter, offset)
|
||||
}
|
||||
}
|
||||
|
||||
cancel(data: Data) {
|
||||
const { document } = data
|
||||
const page = getPage(data, this.snapshot.currentPageId)
|
||||
|
||||
for (let { id, point, rotation } of this.snapshot.shapes) {
|
||||
const shape = document.pages[this.snapshot.currentPageId].shapes[id]
|
||||
const shape = page.shapes[id]
|
||||
shape.rotation = rotation
|
||||
shape.point = point
|
||||
}
|
||||
|
@ -55,38 +63,26 @@ export default class RotateSession extends BaseSession {
|
|||
}
|
||||
|
||||
export function getRotateSnapshot(data: Data) {
|
||||
const {
|
||||
boundsRotation,
|
||||
selectedIds,
|
||||
currentPageId,
|
||||
document: { pages },
|
||||
} = current(data)
|
||||
|
||||
const shapes = Array.from(selectedIds.values()).map(
|
||||
(id) => pages[currentPageId].shapes[id]
|
||||
)
|
||||
const shapes = getSelectedShapes(current(data))
|
||||
|
||||
// A mapping of selected shapes and their bounds
|
||||
const shapesBounds = Object.fromEntries(
|
||||
shapes.map((shape) => [shape.id, getShapeUtils(shape).getBounds(shape)])
|
||||
shapes.map((shape) => [shape.id, getShapeBounds(shape)])
|
||||
)
|
||||
|
||||
// The common (exterior) bounds of the selected shapes
|
||||
const bounds = getCommonBounds(...Object.values(shapesBounds))
|
||||
|
||||
const boundsCenter = [
|
||||
bounds.minX + bounds.width / 2,
|
||||
bounds.minY + bounds.height / 2,
|
||||
]
|
||||
const boundsCenter = getBoundsCenter(bounds)
|
||||
|
||||
return {
|
||||
currentPageId,
|
||||
boundsCenter,
|
||||
boundsRotation,
|
||||
currentPageId: data.currentPageId,
|
||||
boundsRotation: data.boundsRotation,
|
||||
shapes: shapes.map(({ id, point, rotation }) => {
|
||||
const bounds = shapesBounds[id]
|
||||
const offset = [bounds.width / 2, bounds.height / 2]
|
||||
const center = vec.add(offset, [bounds.minX, bounds.minY])
|
||||
const center = getBoundsCenter(bounds)
|
||||
|
||||
return {
|
||||
id,
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
import { Data, TransformEdge, TransformCorner } from "types"
|
||||
import { Data, Edge, Corner } from "types"
|
||||
import * as vec from "utils/vec"
|
||||
import BaseSession from "./base-session"
|
||||
import commands from "state/commands"
|
||||
import { current } from "immer"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import {
|
||||
getBoundsCenter,
|
||||
getBoundsFromPoints,
|
||||
getCommonBounds,
|
||||
getPage,
|
||||
getRelativeTransformedBoundingBox,
|
||||
getShapes,
|
||||
getTransformedBoundingBox,
|
||||
} from "utils/utils"
|
||||
|
||||
export default class TransformSession extends BaseSession {
|
||||
scaleX = 1
|
||||
scaleY = 1
|
||||
transformType: TransformEdge | TransformCorner
|
||||
transformType: Edge | Corner | "center"
|
||||
origin: number[]
|
||||
snapshot: TransformSnapshot
|
||||
|
||||
constructor(
|
||||
data: Data,
|
||||
transformType: TransformCorner | TransformEdge,
|
||||
transformType: Corner | Edge | "center",
|
||||
point: number[]
|
||||
) {
|
||||
super(data)
|
||||
|
@ -31,8 +35,9 @@ export default class TransformSession extends BaseSession {
|
|||
update(data: Data, point: number[], isAspectRatioLocked = false) {
|
||||
const { transformType } = this
|
||||
|
||||
const { currentPageId, selectedIds, shapeBounds, initialBounds } =
|
||||
this.snapshot
|
||||
const { selectedIds, shapeBounds, initialBounds } = this.snapshot
|
||||
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
const newBoundingBox = getTransformedBoundingBox(
|
||||
initialBounds,
|
||||
|
@ -48,7 +53,8 @@ export default class TransformSession extends BaseSession {
|
|||
// Now work backward to calculate a new bounding box for each of the shapes.
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const { initialShape, initialShapeBounds } = shapeBounds[id]
|
||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||
shapeBounds[id]
|
||||
|
||||
const newShapeBounds = getRelativeTransformedBoundingBox(
|
||||
newBoundingBox,
|
||||
|
@ -58,13 +64,27 @@ export default class TransformSession extends BaseSession {
|
|||
this.scaleY < 0
|
||||
)
|
||||
|
||||
const shape = data.document.pages[currentPageId].shapes[id]
|
||||
const shape = shapes[id]
|
||||
|
||||
// const transformOrigins = {
|
||||
// [Edge.Top]: [0.5, 1],
|
||||
// [Edge.Right]: [0, 0.5],
|
||||
// [Edge.Bottom]: [0.5, 0],
|
||||
// [Edge.Left]: [1, 0.5],
|
||||
// [Corner.TopLeft]: [1, 1],
|
||||
// [Corner.TopRight]: [0, 1],
|
||||
// [Corner.BottomLeft]: [1, 0],
|
||||
// [Corner.BottomRight]: [0, 0],
|
||||
// }
|
||||
|
||||
// const origin = transformOrigins[this.transformType]
|
||||
|
||||
getShapeUtils(shape).transform(shape, newShapeBounds, {
|
||||
type: this.transformType,
|
||||
initialShape,
|
||||
scaleX: this.scaleX,
|
||||
scaleY: this.scaleY,
|
||||
transformOrigin,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -72,16 +92,20 @@ export default class TransformSession extends BaseSession {
|
|||
cancel(data: Data) {
|
||||
const { currentPageId, selectedIds, shapeBounds } = this.snapshot
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const shape = data.document.pages[currentPageId].shapes[id]
|
||||
const page = getPage(data, currentPageId)
|
||||
|
||||
const { initialShape, initialShapeBounds } = shapeBounds[id]
|
||||
selectedIds.forEach((id) => {
|
||||
const shape = page.shapes[id]
|
||||
|
||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||
shapeBounds[id]
|
||||
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
type: this.transformType,
|
||||
initialShape,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
transformOrigin,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -99,7 +123,7 @@ export default class TransformSession extends BaseSession {
|
|||
|
||||
export function getTransformSnapshot(
|
||||
data: Data,
|
||||
transformType: TransformEdge | TransformCorner
|
||||
transformType: Edge | Corner | "center"
|
||||
) {
|
||||
const {
|
||||
document: { pages },
|
||||
|
@ -117,8 +141,12 @@ export function getTransformSnapshot(
|
|||
})
|
||||
)
|
||||
|
||||
const boundsArr = Object.values(shapesBounds)
|
||||
|
||||
// The common (exterior) bounds of the selected shapes
|
||||
const bounds = getCommonBounds(...Object.values(shapesBounds))
|
||||
const bounds = getCommonBounds(...boundsArr)
|
||||
|
||||
const initialInnerBounds = getBoundsFromPoints(boundsArr.map(getBoundsCenter))
|
||||
|
||||
// Return a mapping of shapes to bounds together with the relative
|
||||
// positions of the shape's bounds within the common bounds shape.
|
||||
|
@ -129,11 +157,18 @@ export function getTransformSnapshot(
|
|||
initialBounds: bounds,
|
||||
shapeBounds: Object.fromEntries(
|
||||
Array.from(selectedIds.values()).map((id) => {
|
||||
const initialShapeBounds = shapesBounds[id]
|
||||
const ic = getBoundsCenter(initialShapeBounds)
|
||||
|
||||
let ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
|
||||
let iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
|
||||
|
||||
return [
|
||||
id,
|
||||
{
|
||||
initialShape: pageShapes[id],
|
||||
initialShapeBounds: shapesBounds[id],
|
||||
initialShapeBounds,
|
||||
transformOrigin: [ix, iy],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Data, TransformEdge, TransformCorner } from "types"
|
||||
import { Data, Edge, Corner } from "types"
|
||||
import * as vec from "utils/vec"
|
||||
import BaseSession from "./base-session"
|
||||
import commands from "state/commands"
|
||||
|
@ -9,10 +9,13 @@ import {
|
|||
getCommonBounds,
|
||||
getRotatedCorners,
|
||||
getTransformAnchor,
|
||||
getPage,
|
||||
getShape,
|
||||
getSelectedShapes,
|
||||
} from "utils/utils"
|
||||
|
||||
export default class TransformSingleSession extends BaseSession {
|
||||
transformType: TransformEdge | TransformCorner
|
||||
transformType: Edge | Corner
|
||||
origin: number[]
|
||||
scaleX = 1
|
||||
scaleY = 1
|
||||
|
@ -21,7 +24,7 @@ export default class TransformSingleSession extends BaseSession {
|
|||
|
||||
constructor(
|
||||
data: Data,
|
||||
transformType: TransformCorner | TransformEdge,
|
||||
transformType: Corner | Edge,
|
||||
point: number[],
|
||||
isCreating = false
|
||||
) {
|
||||
|
@ -38,7 +41,7 @@ export default class TransformSingleSession extends BaseSession {
|
|||
const { initialShapeBounds, currentPageId, initialShape, id } =
|
||||
this.snapshot
|
||||
|
||||
const shape = data.document.pages[currentPageId].shapes[id]
|
||||
const shape = getShape(data, id, currentPageId)
|
||||
|
||||
const newBoundingBox = getTransformedBoundingBox(
|
||||
initialShapeBounds,
|
||||
|
@ -56,6 +59,7 @@ export default class TransformSingleSession extends BaseSession {
|
|||
type: this.transformType,
|
||||
scaleX: this.scaleX,
|
||||
scaleY: this.scaleY,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -63,15 +67,14 @@ export default class TransformSingleSession extends BaseSession {
|
|||
const { id, initialShape, initialShapeBounds, currentPageId } =
|
||||
this.snapshot
|
||||
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
|
||||
const shape = shapes[id]
|
||||
const shape = getShape(data, id, currentPageId)
|
||||
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
initialShape,
|
||||
type: this.transformType,
|
||||
scaleX: this.scaleX,
|
||||
scaleY: this.scaleY,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -89,21 +92,14 @@ export default class TransformSingleSession extends BaseSession {
|
|||
|
||||
export function getTransformSingleSnapshot(
|
||||
data: Data,
|
||||
transformType: TransformEdge | TransformCorner
|
||||
transformType: Edge | Corner
|
||||
) {
|
||||
const {
|
||||
document: { pages },
|
||||
selectedIds,
|
||||
currentPageId,
|
||||
} = current(data)
|
||||
|
||||
const id = Array.from(selectedIds)[0]
|
||||
const shape = pages[currentPageId].shapes[id]
|
||||
const shape = getSelectedShapes(current(data))[0]
|
||||
const bounds = getShapeUtils(shape).getBounds(shape)
|
||||
|
||||
return {
|
||||
id,
|
||||
currentPageId,
|
||||
id: shape.id,
|
||||
currentPageId: data.currentPageId,
|
||||
type: transformType,
|
||||
initialShape: shape,
|
||||
initialShapeBounds: bounds,
|
||||
|
|
|
@ -4,6 +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"
|
||||
|
||||
export default class TranslateSession extends BaseSession {
|
||||
delta = [0, 0]
|
||||
|
@ -19,7 +20,7 @@ export default class TranslateSession extends BaseSession {
|
|||
|
||||
update(data: Data, point: number[], isAligned: boolean, isCloning: boolean) {
|
||||
const { currentPageId, clones, initialShapes } = this.snapshot
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
|
||||
const delta = vec.vec(this.origin, point)
|
||||
|
||||
|
@ -71,7 +72,7 @@ export default class TranslateSession extends BaseSession {
|
|||
|
||||
cancel(data: Data) {
|
||||
const { initialShapes, clones, currentPageId } = this.snapshot
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
|
||||
for (const { id, point } of initialShapes) {
|
||||
shapes[id].point = point
|
||||
|
@ -93,14 +94,10 @@ export default class TranslateSession extends BaseSession {
|
|||
}
|
||||
|
||||
export function getTranslateSnapshot(data: Data) {
|
||||
const { document, selectedIds, currentPageId } = current(data)
|
||||
|
||||
const shapes = Array.from(selectedIds.values()).map(
|
||||
(id) => document.pages[currentPageId].shapes[id]
|
||||
)
|
||||
const shapes = getSelectedShapes(current(data))
|
||||
|
||||
return {
|
||||
currentPageId,
|
||||
currentPageId: data.currentPageId,
|
||||
initialShapes: shapes.map(({ id, point }) => ({ id, point })),
|
||||
clones: shapes.map((shape) => ({ ...shape, id: uuid() })),
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import { createSelectorHook, createState } from "@state-designer/react"
|
||||
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
|
||||
import {
|
||||
clamp,
|
||||
getCommonBounds,
|
||||
getPage,
|
||||
getShape,
|
||||
screenToWorld,
|
||||
} from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import {
|
||||
Data,
|
||||
PointerInfo,
|
||||
Shape,
|
||||
ShapeType,
|
||||
TransformCorner,
|
||||
TransformEdge,
|
||||
Corner,
|
||||
Edge,
|
||||
CodeControl,
|
||||
} from "types"
|
||||
import inputs from "./inputs"
|
||||
|
@ -99,9 +105,11 @@ const state = createState({
|
|||
SELECTED_ALL: "selectAll",
|
||||
POINTED_CANVAS: { to: "brushSelecting" },
|
||||
POINTED_BOUNDS: { to: "pointingBounds" },
|
||||
POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
|
||||
POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
|
||||
POINTED_ROTATE_HANDLE: { to: "rotatingSelection" },
|
||||
POINTED_BOUNDS_HANDLE: {
|
||||
if: "isPointingRotationHandle",
|
||||
to: "rotatingSelection",
|
||||
else: { to: "transformingSelection" },
|
||||
},
|
||||
MOVED_OVER_SHAPE: {
|
||||
if: "pointHitsShape",
|
||||
then: {
|
||||
|
@ -156,9 +164,12 @@ const state = createState({
|
|||
},
|
||||
rotatingSelection: {
|
||||
onEnter: "startRotateSession",
|
||||
onExit: "clearBoundsRotation",
|
||||
on: {
|
||||
MOVED_POINTER: "updateRotateSession",
|
||||
PANNED_CAMERA: "updateRotateSession",
|
||||
PRESSED_SHIFT_KEY: "keyUpdateRotateSession",
|
||||
RELEASED_SHIFT_KEY: "keyUpdateRotateSession",
|
||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||
},
|
||||
|
@ -420,14 +431,19 @@ const state = createState({
|
|||
return data.hoveredId === payload.target
|
||||
},
|
||||
pointHitsShape(data, payload: { target: string; point: number[] }) {
|
||||
const shape =
|
||||
data.document.pages[data.currentPageId].shapes[payload.target]
|
||||
const shape = getShape(data, payload.target)
|
||||
|
||||
return getShapeUtils(shape).hitTest(
|
||||
shape,
|
||||
screenToWorld(payload.point, data)
|
||||
)
|
||||
},
|
||||
isPointingRotationHandle(
|
||||
data,
|
||||
payload: { target: Edge | Corner | "rotate" }
|
||||
) {
|
||||
return payload.target === "rotate"
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/* --------------------- Shapes --------------------- */
|
||||
|
@ -438,7 +454,7 @@ const state = createState({
|
|||
point: screenToWorld(payload.point, data),
|
||||
})
|
||||
|
||||
data.document.pages[data.currentPageId].shapes[shape.id] = shape
|
||||
getPage(data).shapes[shape.id] = shape
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
@ -449,7 +465,7 @@ const state = createState({
|
|||
point: screenToWorld(payload.point, data),
|
||||
})
|
||||
|
||||
data.document.pages[data.currentPageId].shapes[shape.id] = shape
|
||||
getPage(data).shapes[shape.id] = shape
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
@ -461,7 +477,7 @@ const state = createState({
|
|||
direction: [0, 1],
|
||||
})
|
||||
|
||||
data.document.pages[data.currentPageId].shapes[shape.id] = shape
|
||||
getPage(data).shapes[shape.id] = shape
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
@ -472,7 +488,7 @@ const state = createState({
|
|||
radius: 1,
|
||||
})
|
||||
|
||||
data.document.pages[data.currentPageId].shapes[shape.id] = shape
|
||||
getPage(data).shapes[shape.id] = shape
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
@ -484,7 +500,7 @@ const state = createState({
|
|||
radiusY: 1,
|
||||
})
|
||||
|
||||
data.document.pages[data.currentPageId].shapes[shape.id] = shape
|
||||
getPage(data).shapes[shape.id] = shape
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
@ -495,7 +511,7 @@ const state = createState({
|
|||
size: [1, 1],
|
||||
})
|
||||
|
||||
data.document.pages[data.currentPageId].shapes[shape.id] = shape
|
||||
getPage(data).shapes[shape.id] = shape
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
@ -529,8 +545,15 @@ const state = createState({
|
|||
screenToWorld(payload.point, data)
|
||||
)
|
||||
},
|
||||
keyUpdateRotateSession(data, payload: PointerInfo) {
|
||||
session.update(
|
||||
data,
|
||||
screenToWorld(inputs.pointer.point, data),
|
||||
payload.shiftKey
|
||||
)
|
||||
},
|
||||
updateRotateSession(data, payload: PointerInfo) {
|
||||
session.update(data, screenToWorld(payload.point, data))
|
||||
session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
|
||||
},
|
||||
|
||||
// Dragging / Translating
|
||||
|
@ -564,7 +587,7 @@ const state = createState({
|
|||
// Dragging / Translating
|
||||
startTransformSession(
|
||||
data,
|
||||
payload: PointerInfo & { target: TransformCorner | TransformEdge }
|
||||
payload: PointerInfo & { target: Corner | Edge }
|
||||
) {
|
||||
session =
|
||||
data.selectedIds.size === 1
|
||||
|
@ -583,7 +606,7 @@ const state = createState({
|
|||
startDrawTransformSession(data, payload: PointerInfo) {
|
||||
session = new Sessions.TransformSingleSession(
|
||||
data,
|
||||
TransformCorner.BottomRight,
|
||||
Corner.BottomRight,
|
||||
screenToWorld(payload.point, data),
|
||||
true
|
||||
)
|
||||
|
@ -619,9 +642,10 @@ const state = createState({
|
|||
/* -------------------- Selection ------------------- */
|
||||
|
||||
selectAll(data) {
|
||||
const { selectedIds, document, currentPageId } = data
|
||||
const { selectedIds } = data
|
||||
const page = getPage(data)
|
||||
selectedIds.clear()
|
||||
for (let id in document.pages[currentPageId].shapes) {
|
||||
for (let id in page.shapes) {
|
||||
selectedIds.add(id)
|
||||
}
|
||||
},
|
||||
|
@ -654,6 +678,14 @@ const state = createState({
|
|||
|
||||
document.documentElement.style.setProperty("--camera-zoom", "1")
|
||||
},
|
||||
centerCamera(data) {
|
||||
const { shapes } = getPage(data)
|
||||
getCommonBounds()
|
||||
data.camera.zoom = 1
|
||||
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
|
||||
|
||||
document.documentElement.style.setProperty("--camera-zoom", "1")
|
||||
},
|
||||
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
||||
const { camera } = data
|
||||
const p0 = screenToWorld(payload.point, data)
|
||||
|
@ -678,18 +710,16 @@ const state = createState({
|
|||
)
|
||||
},
|
||||
deleteSelectedIds(data) {
|
||||
const { document, currentPageId } = data
|
||||
const { shapes } = document.pages[currentPageId]
|
||||
const page = getPage(data)
|
||||
|
||||
data.hoveredId = undefined
|
||||
data.pointedId = undefined
|
||||
|
||||
data.selectedIds.forEach((id) => {
|
||||
delete shapes[id]
|
||||
delete page.shapes[id]
|
||||
// TODO: recursively delete children
|
||||
})
|
||||
|
||||
data.document.pages[currentPageId].shapes = shapes
|
||||
data.selectedIds.clear()
|
||||
},
|
||||
|
||||
|
@ -784,14 +814,12 @@ const state = createState({
|
|||
return new Set(data.selectedIds)
|
||||
},
|
||||
selectedBounds(data) {
|
||||
const {
|
||||
selectedIds,
|
||||
currentPageId,
|
||||
document: { pages },
|
||||
} = data
|
||||
const { selectedIds } = data
|
||||
|
||||
const page = getPage(data)
|
||||
|
||||
const shapes = Array.from(selectedIds.values())
|
||||
.map((id) => pages[currentPageId].shapes[id])
|
||||
.map((id) => page.shapes[id])
|
||||
.filter(Boolean)
|
||||
|
||||
if (selectedIds.size === 0) return null
|
||||
|
|
4
types.ts
4
types.ts
|
@ -146,14 +146,14 @@ export interface PointerInfo {
|
|||
altKey: boolean
|
||||
}
|
||||
|
||||
export enum TransformEdge {
|
||||
export enum Edge {
|
||||
Top = "top_edge",
|
||||
Right = "right_edge",
|
||||
Bottom = "bottom_edge",
|
||||
Left = "left_edge",
|
||||
}
|
||||
|
||||
export enum TransformCorner {
|
||||
export enum Corner {
|
||||
TopLeft = "top_left_corner",
|
||||
TopRight = "top_right_corner",
|
||||
BottomRight = "bottom_right_corner",
|
||||
|
|
200
utils/utils.ts
200
utils/utils.ts
|
@ -1,9 +1,9 @@
|
|||
import Vector from "lib/code/vector"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import React from "react"
|
||||
import { Data, Bounds, TransformEdge, TransformCorner, Shape } from "types"
|
||||
import * as svg from "./svg"
|
||||
import { Data, Bounds, Edge, Corner, Shape } from "types"
|
||||
import * as vec from "./vec"
|
||||
import _isMobile from "ismobilejs"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
|
||||
export function screenToWorld(point: number[], data: Data) {
|
||||
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
|
||||
|
@ -42,7 +42,7 @@ export function getCommonBounds(...b: Bounds[]) {
|
|||
return bounds
|
||||
}
|
||||
|
||||
// export function getBoundsFromPoints(a: number[], b: number[]) {
|
||||
// export function getBoundsFromTwoPoints(a: number[], b: number[]) {
|
||||
// const minX = Math.min(a[0], b[0])
|
||||
// const maxX = Math.max(a[0], b[0])
|
||||
// const minY = Math.min(a[1], b[1])
|
||||
|
@ -900,59 +900,59 @@ export function metaKey(e: KeyboardEvent | React.KeyboardEvent) {
|
|||
}
|
||||
|
||||
export function getTransformAnchor(
|
||||
type: TransformEdge | TransformCorner,
|
||||
type: Edge | Corner,
|
||||
isFlippedX: boolean,
|
||||
isFlippedY: boolean
|
||||
) {
|
||||
let anchor: TransformCorner | TransformEdge = type
|
||||
let anchor: Corner | Edge = type
|
||||
|
||||
// Change corner anchors if flipped
|
||||
switch (type) {
|
||||
case TransformCorner.TopLeft: {
|
||||
case Corner.TopLeft: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.BottomRight
|
||||
anchor = Corner.BottomRight
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.TopRight
|
||||
anchor = Corner.TopRight
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.BottomLeft
|
||||
anchor = Corner.BottomLeft
|
||||
} else {
|
||||
anchor = TransformCorner.BottomRight
|
||||
anchor = Corner.BottomRight
|
||||
}
|
||||
break
|
||||
}
|
||||
case TransformCorner.TopRight: {
|
||||
case Corner.TopRight: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.BottomLeft
|
||||
anchor = Corner.BottomLeft
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.TopLeft
|
||||
anchor = Corner.TopLeft
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.BottomRight
|
||||
anchor = Corner.BottomRight
|
||||
} else {
|
||||
anchor = TransformCorner.BottomLeft
|
||||
anchor = Corner.BottomLeft
|
||||
}
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomRight: {
|
||||
case Corner.BottomRight: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.TopLeft
|
||||
anchor = Corner.TopLeft
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.BottomLeft
|
||||
anchor = Corner.BottomLeft
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.TopRight
|
||||
anchor = Corner.TopRight
|
||||
} else {
|
||||
anchor = TransformCorner.TopLeft
|
||||
anchor = Corner.TopLeft
|
||||
}
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomLeft: {
|
||||
case Corner.BottomLeft: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.TopRight
|
||||
anchor = Corner.TopRight
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.BottomRight
|
||||
anchor = Corner.BottomRight
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.TopLeft
|
||||
anchor = Corner.TopLeft
|
||||
} else {
|
||||
anchor = TransformCorner.TopRight
|
||||
anchor = Corner.TopRight
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -1030,6 +1030,18 @@ export function rotateBounds(
|
|||
}
|
||||
}
|
||||
|
||||
export function getRotatedSize(size: number[], rotation: number) {
|
||||
const center = vec.div(size, 2)
|
||||
|
||||
const points = [[0, 0], [size[0], 0], size, [0, size[1]]].map((point) =>
|
||||
vec.rotWith(point, center, rotation)
|
||||
)
|
||||
|
||||
const bounds = getBoundsFromPoints(points)
|
||||
|
||||
return [bounds.width, bounds.height]
|
||||
}
|
||||
|
||||
export function getRotatedCorners(b: Bounds, rotation: number) {
|
||||
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
|
||||
|
||||
|
@ -1043,7 +1055,7 @@ export function getRotatedCorners(b: Bounds, rotation: number) {
|
|||
|
||||
export function getTransformedBoundingBox(
|
||||
bounds: Bounds,
|
||||
handle: TransformCorner | TransformEdge | "center",
|
||||
handle: Corner | Edge | "center",
|
||||
delta: number[],
|
||||
rotation = 0,
|
||||
isAspectRatioLocked = false
|
||||
|
@ -1082,30 +1094,30 @@ export function getTransformedBoundingBox(
|
|||
corners should change.
|
||||
*/
|
||||
switch (handle) {
|
||||
case TransformEdge.Top:
|
||||
case TransformCorner.TopLeft:
|
||||
case TransformCorner.TopRight: {
|
||||
case Edge.Top:
|
||||
case Corner.TopLeft:
|
||||
case Corner.TopRight: {
|
||||
by0 += dy
|
||||
break
|
||||
}
|
||||
case TransformEdge.Bottom:
|
||||
case TransformCorner.BottomLeft:
|
||||
case TransformCorner.BottomRight: {
|
||||
case Edge.Bottom:
|
||||
case Corner.BottomLeft:
|
||||
case Corner.BottomRight: {
|
||||
by1 += dy
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch (handle) {
|
||||
case TransformEdge.Left:
|
||||
case TransformCorner.TopLeft:
|
||||
case TransformCorner.BottomLeft: {
|
||||
case Edge.Left:
|
||||
case Corner.TopLeft:
|
||||
case Corner.BottomLeft: {
|
||||
bx0 += dx
|
||||
break
|
||||
}
|
||||
case TransformEdge.Right:
|
||||
case TransformCorner.TopRight:
|
||||
case TransformCorner.BottomRight: {
|
||||
case Edge.Right:
|
||||
case Corner.TopRight:
|
||||
case Corner.BottomRight: {
|
||||
bx1 += dx
|
||||
break
|
||||
}
|
||||
|
@ -1117,6 +1129,9 @@ export function getTransformedBoundingBox(
|
|||
const scaleX = (bx1 - bx0) / aw
|
||||
const scaleY = (by1 - by0) / ah
|
||||
|
||||
const flipX = scaleX < 0
|
||||
const flipY = scaleY < 0
|
||||
|
||||
const bw = Math.abs(bx1 - bx0)
|
||||
const bh = Math.abs(by1 - by0)
|
||||
|
||||
|
@ -1134,36 +1149,36 @@ export function getTransformedBoundingBox(
|
|||
const th = bh * (scaleX < 0 ? 1 : -1) * ar
|
||||
|
||||
switch (handle) {
|
||||
case TransformCorner.TopLeft: {
|
||||
case Corner.TopLeft: {
|
||||
if (isTall) by0 = by1 + tw
|
||||
else bx0 = bx1 + th
|
||||
break
|
||||
}
|
||||
case TransformCorner.TopRight: {
|
||||
case Corner.TopRight: {
|
||||
if (isTall) by0 = by1 + tw
|
||||
else bx1 = bx0 - th
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomRight: {
|
||||
case Corner.BottomRight: {
|
||||
if (isTall) by1 = by0 - tw
|
||||
else bx1 = bx0 - th
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomLeft: {
|
||||
case Corner.BottomLeft: {
|
||||
if (isTall) by1 = by0 - tw
|
||||
else bx0 = bx1 + th
|
||||
break
|
||||
}
|
||||
case TransformEdge.Bottom:
|
||||
case TransformEdge.Top: {
|
||||
case Edge.Bottom:
|
||||
case Edge.Top: {
|
||||
const m = (bx0 + bx1) / 2
|
||||
const w = bh * ar
|
||||
bx0 = m - w / 2
|
||||
bx1 = m + w / 2
|
||||
break
|
||||
}
|
||||
case TransformEdge.Left:
|
||||
case TransformEdge.Right: {
|
||||
case Edge.Left:
|
||||
case Edge.Right: {
|
||||
const m = (by0 + by1) / 2
|
||||
const h = bw / ar
|
||||
by0 = m - h / 2
|
||||
|
@ -1189,56 +1204,56 @@ export function getTransformedBoundingBox(
|
|||
const c1 = vec.med([bx0, by0], [bx1, by1])
|
||||
|
||||
switch (handle) {
|
||||
case TransformCorner.TopLeft: {
|
||||
case Corner.TopLeft: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith([bx1, by1], c1, rotation),
|
||||
vec.rotWith([ax1, ay1], c0, rotation)
|
||||
)
|
||||
break
|
||||
}
|
||||
case TransformCorner.TopRight: {
|
||||
case Corner.TopRight: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith([bx0, by1], c1, rotation),
|
||||
vec.rotWith([ax0, ay1], c0, rotation)
|
||||
)
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomRight: {
|
||||
case Corner.BottomRight: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith([bx0, by0], c1, rotation),
|
||||
vec.rotWith([ax0, ay0], c0, rotation)
|
||||
)
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomLeft: {
|
||||
case Corner.BottomLeft: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith([bx1, by0], c1, rotation),
|
||||
vec.rotWith([ax1, ay0], c0, rotation)
|
||||
)
|
||||
break
|
||||
}
|
||||
case TransformEdge.Top: {
|
||||
case Edge.Top: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
|
||||
vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
|
||||
)
|
||||
break
|
||||
}
|
||||
case TransformEdge.Left: {
|
||||
case Edge.Left: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
|
||||
vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
|
||||
)
|
||||
break
|
||||
}
|
||||
case TransformEdge.Bottom: {
|
||||
case Edge.Bottom: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
|
||||
vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
|
||||
)
|
||||
break
|
||||
}
|
||||
case TransformEdge.Right: {
|
||||
case Edge.Right: {
|
||||
cv = vec.sub(
|
||||
vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
|
||||
vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
|
||||
|
@ -1273,8 +1288,8 @@ export function getTransformedBoundingBox(
|
|||
maxY: by1,
|
||||
width: bx1 - bx0,
|
||||
height: by1 - by0,
|
||||
scaleX,
|
||||
scaleY,
|
||||
scaleX: ((bx1 - bx0) / (ax1 - ax0)) * (flipX ? -1 : 1),
|
||||
scaleY: ((by1 - by0) / (ay1 - ay0)) * (flipY ? -1 : 1),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1285,25 +1300,23 @@ export function getRelativeTransformedBoundingBox(
|
|||
isFlippedX: boolean,
|
||||
isFlippedY: boolean
|
||||
) {
|
||||
const minX =
|
||||
bounds.minX +
|
||||
bounds.width *
|
||||
((isFlippedX
|
||||
? initialBounds.maxX - initialShapeBounds.maxX
|
||||
: initialShapeBounds.minX - initialBounds.minX) /
|
||||
initialBounds.width)
|
||||
const nx =
|
||||
(isFlippedX
|
||||
? initialBounds.maxX - initialShapeBounds.maxX
|
||||
: initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
|
||||
|
||||
const minY =
|
||||
bounds.minY +
|
||||
bounds.height *
|
||||
((isFlippedY
|
||||
? initialBounds.maxY - initialShapeBounds.maxY
|
||||
: initialShapeBounds.minY - initialBounds.minY) /
|
||||
initialBounds.height)
|
||||
const ny =
|
||||
(isFlippedY
|
||||
? initialBounds.maxY - initialShapeBounds.maxY
|
||||
: initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
|
||||
|
||||
const width = (initialShapeBounds.width / initialBounds.width) * bounds.width
|
||||
const height =
|
||||
(initialShapeBounds.height / initialBounds.height) * bounds.height
|
||||
const nw = initialShapeBounds.width / initialBounds.width
|
||||
const nh = initialShapeBounds.height / initialBounds.height
|
||||
|
||||
const minX = bounds.minX + bounds.width * nx
|
||||
const minY = bounds.minY + bounds.height * ny
|
||||
const width = bounds.width * nw
|
||||
const height = bounds.height * nh
|
||||
|
||||
return {
|
||||
minX,
|
||||
|
@ -1314,3 +1327,42 @@ export function getRelativeTransformedBoundingBox(
|
|||
height,
|
||||
}
|
||||
}
|
||||
|
||||
export function getShape(
|
||||
data: Data,
|
||||
shapeId: string,
|
||||
pageId = data.currentPageId
|
||||
) {
|
||||
return data.document.pages[pageId].shapes[shapeId]
|
||||
}
|
||||
|
||||
export function getPage(data: Data, pageId = data.currentPageId) {
|
||||
return data.document.pages[pageId]
|
||||
}
|
||||
|
||||
export function getCurrentCode(data: Data, fileId = data.currentCodeFileId) {
|
||||
return data.document.code[fileId]
|
||||
}
|
||||
|
||||
export function getShapes(data: Data, pageId = data.currentPageId) {
|
||||
const page = getPage(data, pageId)
|
||||
return Object.values(page.shapes)
|
||||
}
|
||||
|
||||
export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
|
||||
const page = getPage(data, pageId)
|
||||
const ids = Array.from(data.selectedIds.values())
|
||||
return ids.map((id) => page.shapes[id])
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
return _isMobile()
|
||||
}
|
||||
|
||||
export function getShapeBounds(shape: Shape) {
|
||||
return getShapeUtils(shape).getBounds(shape)
|
||||
}
|
||||
|
||||
export function getBoundsCenter(bounds: Bounds) {
|
||||
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
|
||||
}
|
||||
|
|
|
@ -4154,6 +4154,11 @@ isexe@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
ismobilejs@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ismobilejs/-/ismobilejs-1.1.1.tgz#c56ca0ae8e52b24ca0f22ba5ef3215a2ddbbaa0e"
|
||||
integrity sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==
|
||||
|
||||
isobject@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
|
||||
|
|
Loading…
Reference in a new issue