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