Adds select all, starts on rotation
This commit is contained in:
parent
abd310aa2e
commit
e2aac4b267
33 changed files with 608 additions and 255 deletions
|
@ -6,6 +6,12 @@ import styled from "styles"
|
||||||
export default function BoundsBg() {
|
export default function BoundsBg() {
|
||||||
const rBounds = useRef<SVGRectElement>(null)
|
const rBounds = useRef<SVGRectElement>(null)
|
||||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||||
|
const singleSelection = useSelector((s) => {
|
||||||
|
if (s.data.selectedIds.size === 1) {
|
||||||
|
const selected = Array.from(s.data.selectedIds.values())[0]
|
||||||
|
return s.data.document.pages[s.data.currentPageId].shapes[selected]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!bounds) return null
|
if (!bounds) return null
|
||||||
|
|
||||||
|
@ -24,6 +30,12 @@ export default function BoundsBg() {
|
||||||
rBounds.current.setPointerCapture(e.pointerId)
|
rBounds.current.setPointerCapture(e.pointerId)
|
||||||
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
|
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
|
||||||
}}
|
}}
|
||||||
|
transform={
|
||||||
|
singleSelection &&
|
||||||
|
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
|
||||||
|
minX + width / 2
|
||||||
|
}, ${minY + width / 2})`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,12 @@ import { lerp } from "utils/utils"
|
||||||
export default function Bounds() {
|
export default function Bounds() {
|
||||||
const zoom = useSelector((state) => state.data.camera.zoom)
|
const zoom = useSelector((state) => state.data.camera.zoom)
|
||||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||||
|
const singleSelection = useSelector((s) => {
|
||||||
|
if (s.data.selectedIds.size === 1) {
|
||||||
|
const selected = Array.from(s.data.selectedIds.values())[0]
|
||||||
|
return s.data.document.pages[s.data.currentPageId].shapes[selected]
|
||||||
|
}
|
||||||
|
})
|
||||||
const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
|
const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
|
||||||
|
|
||||||
if (!bounds) return null
|
if (!bounds) return null
|
||||||
|
@ -18,7 +24,15 @@ export default function Bounds() {
|
||||||
const cp = p * 2
|
const cp = p * 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g pointerEvents={isBrushing ? "none" : "all"}>
|
<g
|
||||||
|
pointerEvents={isBrushing ? "none" : "all"}
|
||||||
|
transform={
|
||||||
|
singleSelection &&
|
||||||
|
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
|
||||||
|
minX + width / 2
|
||||||
|
}, ${minY + width / 2})`
|
||||||
|
}
|
||||||
|
>
|
||||||
<StyledBounds
|
<StyledBounds
|
||||||
x={minX}
|
x={minX}
|
||||||
y={minY}
|
y={minY}
|
||||||
|
@ -82,10 +96,35 @@ export default function Bounds() {
|
||||||
height={cp}
|
height={cp}
|
||||||
corner={TransformCorner.BottomLeft}
|
corner={TransformCorner.BottomLeft}
|
||||||
/>
|
/>
|
||||||
|
<RotateHandle x={minX + width / 2} y={minY - cp * 2} r={cp / 2} />
|
||||||
</g>
|
</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({
|
function Corner({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
@ -99,32 +138,10 @@ function Corner({
|
||||||
height: number
|
height: number
|
||||||
corner: TransformCorner
|
corner: TransformCorner
|
||||||
}) {
|
}) {
|
||||||
const rRotateCorner = useRef<SVGRectElement>(null)
|
|
||||||
const rCorner = useRef<SVGRectElement>(null)
|
const rCorner = useRef<SVGRectElement>(null)
|
||||||
|
|
||||||
const isTop = corner.includes("top")
|
|
||||||
const isLeft = corner.includes("bottom")
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
<StyledRotateCorner
|
|
||||||
ref={rRotateCorner}
|
|
||||||
x={x + width * (isLeft ? -1.25 : -0.5)}
|
|
||||||
y={y + width * (isTop ? -1.25 : -0.5)}
|
|
||||||
width={width * 1.75}
|
|
||||||
height={height * 1.75}
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
rRotateCorner.current.setPointerCapture(e.pointerId)
|
|
||||||
state.send("POINTED_ROTATE_CORNER", inputs.pointerDown(e, corner))
|
|
||||||
}}
|
|
||||||
onPointerUp={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
rRotateCorner.current.releasePointerCapture(e.pointerId)
|
|
||||||
rRotateCorner.current.replaceWith(rRotateCorner.current)
|
|
||||||
state.send("STOPPED_POINTING", inputs.pointerDown(e, corner))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<StyledCorner
|
<StyledCorner
|
||||||
ref={rCorner}
|
ref={rCorner}
|
||||||
x={x + width * -0.5}
|
x={x + width * -0.5}
|
||||||
|
@ -252,13 +269,15 @@ const StyledCorner = styled("rect", {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const StyledRotateHandle = styled("circle", {
|
||||||
|
stroke: "$bounds",
|
||||||
|
fill: "#fff",
|
||||||
|
zStrokeWidth: 2,
|
||||||
|
cursor: "grab",
|
||||||
|
})
|
||||||
|
|
||||||
const StyledBounds = styled("rect", {
|
const StyledBounds = styled("rect", {
|
||||||
fill: "none",
|
fill: "none",
|
||||||
stroke: "$bounds",
|
stroke: "$bounds",
|
||||||
zStrokeWidth: 2,
|
zStrokeWidth: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
const StyledRotateCorner = styled("rect", {
|
|
||||||
cursor: "grab",
|
|
||||||
fill: "transparent",
|
|
||||||
})
|
|
||||||
|
|
|
@ -63,7 +63,9 @@ function Shape({ id }: { id: string }) {
|
||||||
ref={rGroup}
|
ref={rGroup}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
transform={`translate(${shape.point})`}
|
transform={`rotate(${shape.rotation * (180 / Math.PI)},${getShapeUtils(
|
||||||
|
shape
|
||||||
|
).getCenter(shape)}) translate(${shape.point})`}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
onPointerEnter={handlePointerEnter}
|
onPointerEnter={handlePointerEnter}
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default function CodePanel() {
|
||||||
const file = useSelector(
|
const file = useSelector(
|
||||||
(s) => s.data.document.code[s.data.currentCodeFileId]
|
(s) => s.data.document.code[s.data.currentCodeFileId]
|
||||||
)
|
)
|
||||||
const isOpen = true
|
const isOpen = useSelector((s) => s.data.settings.isCodeOpen)
|
||||||
const fontSize = useSelector((s) => s.data.settings.fontSize)
|
const fontSize = useSelector((s) => s.data.settings.fontSize)
|
||||||
|
|
||||||
const local = useStateDesigner({
|
const local = useStateDesigner({
|
||||||
|
@ -115,11 +115,11 @@ export default function CodePanel() {
|
||||||
const { error } = local.data
|
const { error } = local.data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Root data-bp-desktop ref={rContainer} isCollapsed={!isOpen}>
|
<Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<Panel.Layout>
|
<Panel.Layout>
|
||||||
<Panel.Header>
|
<Panel.Header>
|
||||||
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
|
<IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
|
||||||
<X />
|
<X />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<h3>Code</h3>
|
<h3>Code</h3>
|
||||||
|
@ -169,7 +169,7 @@ export default function CodePanel() {
|
||||||
</Panel.Footer>
|
</Panel.Footer>
|
||||||
</Panel.Layout>
|
</Panel.Layout>
|
||||||
) : (
|
) : (
|
||||||
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
|
<IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
|
||||||
<Code />
|
<Code />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -14,10 +14,10 @@ export default function ControlPanel() {
|
||||||
(state) => Object.keys(state.data.codeControls),
|
(state) => Object.keys(state.data.codeControls),
|
||||||
deepCompareArrays
|
deepCompareArrays
|
||||||
)
|
)
|
||||||
const isOpen = true
|
const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Root data-bp-desktop ref={rContainer} isCollapsed={!isOpen}>
|
<Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<Panel.Layout>
|
<Panel.Layout>
|
||||||
<Panel.Header>
|
<Panel.Header>
|
||||||
|
|
|
@ -12,9 +12,12 @@ export const Root = styled("div", {
|
||||||
boxShadow: "0px 2px 25px rgba(0,0,0,.16)",
|
boxShadow: "0px 2px 25px rgba(0,0,0,.16)",
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
isCollapsed: {
|
isOpen: {
|
||||||
true: {},
|
true: {},
|
||||||
false: {},
|
false: {
|
||||||
|
height: 34,
|
||||||
|
width: 34,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,6 +19,15 @@ export default function useKeyboardEvents() {
|
||||||
state.send("DELETED", getKeyboardEventInfo(e))
|
state.send("DELETED", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === "s" && metaKey(e)) {
|
||||||
|
e.preventDefault()
|
||||||
|
state.send("SAVED")
|
||||||
|
}
|
||||||
|
if (e.key === "a" && metaKey(e)) {
|
||||||
|
e.preventDefault()
|
||||||
|
state.send("SELECTED_ALL")
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) {
|
||||||
state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
|
state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,8 @@ import state from "state"
|
||||||
export default function useLoadOnMount() {
|
export default function useLoadOnMount() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
state.send("MOUNTED")
|
state.send("MOUNTED")
|
||||||
|
return () => {
|
||||||
|
state.send("UNMOUNTED")
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import state, { useSelector } from "state"
|
||||||
|
|
||||||
export default function useTheme() {
|
export default function useTheme() {
|
||||||
const theme = useSelector((state) =>
|
const theme = useSelector((state) =>
|
||||||
state.data.settings.darkMode ? "dark" : "light"
|
state.data.settings.isDarkMode ? "dark" : "light"
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleTheme = useCallback(() => state.send("TOGGLED_THEME"), [])
|
const toggleTheme = useCallback(() => state.send("TOGGLED_THEME"), [])
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default class Circle extends CodeShape<CircleShape> {
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
radius: 20,
|
radius: 20,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default class Dot extends CodeShape<DotShape> {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class Ellipse extends CodeShape<EllipseShape> {
|
||||||
radiusY: 20,
|
radiusY: 20,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class Line extends CodeShape<LineShape> {
|
||||||
direction: [-0.5, 0.5],
|
direction: [-0.5, 0.5],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class Ray extends CodeShape<RayShape> {
|
||||||
direction: [0, 1],
|
direction: [0, 1],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class Rectangle extends CodeShape<RectangleShape> {
|
||||||
size: [100, 100],
|
size: [100, 100],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,7 +21,7 @@ const circle = createShape<CircleShape>({
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
radius: 20,
|
radius: 20,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
|
@ -56,6 +56,10 @@ const circle = createShape<CircleShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCenter(shape) {
|
||||||
|
return [shape.point[0] + shape.radius, shape.point[1] + shape.radius]
|
||||||
|
},
|
||||||
|
|
||||||
hitTest(shape, point) {
|
hitTest(shape, point) {
|
||||||
return pointInCircle(
|
return pointInCircle(
|
||||||
point,
|
point,
|
||||||
|
|
|
@ -21,7 +21,7 @@ const dot = createShape<DotShape>({
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
|
@ -55,6 +55,10 @@ const dot = createShape<DotShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCenter(shape) {
|
||||||
|
return shape.point
|
||||||
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,7 +22,7 @@ const ellipse = createShape<EllipseShape>({
|
||||||
radiusY: 20,
|
radiusY: 20,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
|
@ -60,6 +60,10 @@ const ellipse = createShape<EllipseShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCenter(shape) {
|
||||||
|
return [shape.point[0] + shape.radiusX, shape.point[1] + shape.radiusY]
|
||||||
|
},
|
||||||
|
|
||||||
hitTest(shape, point) {
|
hitTest(shape, point) {
|
||||||
return pointInEllipse(
|
return pointInEllipse(
|
||||||
point,
|
point,
|
||||||
|
|
|
@ -36,6 +36,9 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
// Get the bounds of the a shape.
|
// Get the bounds of the a shape.
|
||||||
getBounds(this: ShapeUtility<K>, shape: K): Bounds
|
getBounds(this: ShapeUtility<K>, shape: K): Bounds
|
||||||
|
|
||||||
|
// Get the center of the shape
|
||||||
|
getCenter(this: ShapeUtility<K>, shape: K): number[]
|
||||||
|
|
||||||
// Test whether a point lies within a shape.
|
// Test whether a point lies within a shape.
|
||||||
hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean
|
hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ const line = createShape<LineShape>({
|
||||||
direction: [0, 0],
|
direction: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
|
@ -63,6 +63,10 @@ const line = createShape<LineShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCenter(shape) {
|
||||||
|
return shape.point
|
||||||
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,6 +58,11 @@ const polyline = createShape<PolylineShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCenter(shape) {
|
||||||
|
const bounds = this.getBounds(shape)
|
||||||
|
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
|
||||||
|
},
|
||||||
|
|
||||||
hitTest(shape, point) {
|
hitTest(shape, point) {
|
||||||
let pt = vec.sub(point, shape.point)
|
let pt = vec.sub(point, shape.point)
|
||||||
let prev = shape.points[0]
|
let prev = shape.points[0]
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { RayShape, ShapeType } from "types"
|
||||||
import { createShape } from "./index"
|
import { createShape } from "./index"
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from "utils/bounds"
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from "utils/intersections"
|
||||||
import styled from "styles"
|
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from "components/canvas/misc"
|
||||||
|
|
||||||
const ray = createShape<RayShape>({
|
const ray = createShape<RayShape>({
|
||||||
|
@ -22,7 +21,7 @@ const ray = createShape<RayShape>({
|
||||||
direction: [0, 1],
|
direction: [0, 1],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
|
@ -64,6 +63,10 @@ const ray = createShape<RayShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCenter(shape) {
|
||||||
|
return shape.point
|
||||||
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ const rectangle = createShape<RectangleShape>({
|
||||||
size: [1, 1],
|
size: [1, 1],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#777",
|
fill: "rgba(142, 143, 142, 1.000)",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
|
@ -54,6 +54,11 @@ const rectangle = createShape<RectangleShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCenter(shape) {
|
||||||
|
const bounds = this.getBounds(shape)
|
||||||
|
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
|
||||||
|
},
|
||||||
|
|
||||||
hitTest(shape) {
|
hitTest(shape) {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,14 +2,16 @@ import translate from "./translate"
|
||||||
import transform from "./transform"
|
import transform from "./transform"
|
||||||
import generate from "./generate"
|
import generate from "./generate"
|
||||||
import createShape from "./create-shape"
|
import createShape from "./create-shape"
|
||||||
import direction from "./direction"
|
import direct from "./direct"
|
||||||
|
import rotate from "./rotate"
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
translate,
|
translate,
|
||||||
transform,
|
transform,
|
||||||
generate,
|
generate,
|
||||||
createShape,
|
createShape,
|
||||||
direction,
|
direct,
|
||||||
|
rotate,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default commands
|
export default commands
|
||||||
|
|
32
state/commands/rotate.ts
Normal file
32
state/commands/rotate.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import Command from "./command"
|
||||||
|
import history from "../history"
|
||||||
|
import { Data } from "types"
|
||||||
|
import { RotateSnapshot } from "state/sessions/rotate-session"
|
||||||
|
|
||||||
|
export default function translateCommand(
|
||||||
|
data: Data,
|
||||||
|
before: RotateSnapshot,
|
||||||
|
after: RotateSnapshot
|
||||||
|
) {
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: "translate_shapes",
|
||||||
|
category: "canvas",
|
||||||
|
do(data) {
|
||||||
|
const { shapes } = data.document.pages[after.currentPageId]
|
||||||
|
|
||||||
|
for (let { id, rotation } of after.shapes) {
|
||||||
|
shapes[id].rotation = rotation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const { shapes } = data.document.pages[before.currentPageId]
|
||||||
|
|
||||||
|
for (let { id, rotation } of before.shapes) {
|
||||||
|
shapes[id].rotation = rotation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
direction: [0.5, 0.5],
|
direction: [0.5, 0.5],
|
||||||
style: {
|
style: {
|
||||||
fill: "#AAA",
|
fill: "#AAA",
|
||||||
stroke: "#777",
|
stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -28,7 +28,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
// point: [400, 500],
|
// point: [400, 500],
|
||||||
// style: {
|
// style: {
|
||||||
// fill: "#AAA",
|
// fill: "#AAA",
|
||||||
// stroke: "#777",
|
// stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
// strokeWidth: 1,
|
// strokeWidth: 1,
|
||||||
// },
|
// },
|
||||||
// }),
|
// }),
|
||||||
|
@ -40,7 +40,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
radius: 50,
|
radius: 50,
|
||||||
style: {
|
style: {
|
||||||
fill: "#AAA",
|
fill: "#AAA",
|
||||||
stroke: "#777",
|
stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -53,7 +53,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
radiusY: 30,
|
radiusY: 30,
|
||||||
style: {
|
style: {
|
||||||
fill: "#AAA",
|
fill: "#AAA",
|
||||||
stroke: "#777",
|
stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -66,7 +66,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
// radiusY: 30,
|
// radiusY: 30,
|
||||||
// style: {
|
// style: {
|
||||||
// fill: "#AAA",
|
// fill: "#AAA",
|
||||||
// stroke: "#777",
|
// stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
// strokeWidth: 1,
|
// strokeWidth: 1,
|
||||||
// },
|
// },
|
||||||
// }),
|
// }),
|
||||||
|
@ -82,7 +82,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
// ],
|
// ],
|
||||||
// style: {
|
// style: {
|
||||||
// fill: "none",
|
// fill: "none",
|
||||||
// stroke: "#777",
|
// stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
// strokeWidth: 2,
|
// strokeWidth: 2,
|
||||||
// strokeLinecap: "round",
|
// strokeLinecap: "round",
|
||||||
// strokeLinejoin: "round",
|
// strokeLinejoin: "round",
|
||||||
|
@ -96,7 +96,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
size: [200, 200],
|
size: [200, 200],
|
||||||
style: {
|
style: {
|
||||||
fill: "#AAA",
|
fill: "#AAA",
|
||||||
stroke: "#777",
|
stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -108,7 +108,7 @@ export const defaultDocument: Data["document"] = {
|
||||||
// direction: [0.2, 0.2],
|
// direction: [0.2, 0.2],
|
||||||
// style: {
|
// style: {
|
||||||
// fill: "#AAA",
|
// fill: "#AAA",
|
||||||
// stroke: "#777",
|
// stroke: "rgba(142, 143, 142, 1.000)",
|
||||||
// strokeWidth: 1,
|
// strokeWidth: 1,
|
||||||
// },
|
// },
|
||||||
// }),
|
// }),
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default class DirectionSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(data: Data) {
|
complete(data: Data) {
|
||||||
commands.direction(data, this.snapshot, getDirectionSnapshot(data))
|
commands.direct(data, this.snapshot, getDirectionSnapshot(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import BrushSession from "./brush-session"
|
||||||
import TranslateSession from "./translate-session"
|
import TranslateSession from "./translate-session"
|
||||||
import TransformSession from "./transform-session"
|
import TransformSession from "./transform-session"
|
||||||
import DirectionSession from "./direction-session"
|
import DirectionSession from "./direction-session"
|
||||||
|
import RotateSession from "./rotate-session"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BrushSession,
|
BrushSession,
|
||||||
|
@ -10,4 +11,5 @@ export {
|
||||||
TranslateSession,
|
TranslateSession,
|
||||||
TransformSession,
|
TransformSession,
|
||||||
DirectionSession,
|
DirectionSession,
|
||||||
|
RotateSession,
|
||||||
}
|
}
|
||||||
|
|
81
state/sessions/rotate-session.ts
Normal file
81
state/sessions/rotate-session.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { Data } from "types"
|
||||||
|
import * as vec from "utils/vec"
|
||||||
|
import BaseSession from "./base-session"
|
||||||
|
import commands from "state/commands"
|
||||||
|
import { current } from "immer"
|
||||||
|
import { getCommonBounds } from "utils/utils"
|
||||||
|
import { getShapeUtils } from "lib/shapes"
|
||||||
|
|
||||||
|
export default class RotateSession extends BaseSession {
|
||||||
|
delta = [0, 0]
|
||||||
|
origin: number[]
|
||||||
|
snapshot: RotateSnapshot
|
||||||
|
|
||||||
|
constructor(data: Data, point: number[]) {
|
||||||
|
super(data)
|
||||||
|
this.origin = point
|
||||||
|
this.snapshot = getRotateSnapshot(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: Data, point: number[]) {
|
||||||
|
const { currentPageId, center, shapes } = this.snapshot
|
||||||
|
const { document } = data
|
||||||
|
|
||||||
|
const a1 = vec.angle(center, this.origin)
|
||||||
|
const a2 = vec.angle(center, point)
|
||||||
|
|
||||||
|
for (let { id, rotation } of shapes) {
|
||||||
|
const shape = document.pages[currentPageId].shapes[id]
|
||||||
|
shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(data: Data) {
|
||||||
|
const { document } = data
|
||||||
|
|
||||||
|
for (let shape of this.snapshot.shapes) {
|
||||||
|
document.pages[this.snapshot.currentPageId].shapes[shape.id].rotation =
|
||||||
|
shape.rotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(data: Data) {
|
||||||
|
commands.rotate(data, this.snapshot, getRotateSnapshot(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRotateSnapshot(data: Data) {
|
||||||
|
const {
|
||||||
|
selectedIds,
|
||||||
|
document: { pages },
|
||||||
|
currentPageId,
|
||||||
|
} = current(data)
|
||||||
|
|
||||||
|
const shapes = Array.from(selectedIds.values()).map(
|
||||||
|
(id) => pages[currentPageId].shapes[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
// A mapping of selected shapes and their bounds
|
||||||
|
const shapesBounds = Object.fromEntries(
|
||||||
|
shapes.map((shape) => [shape.id, getShapeUtils(shape).getBounds(shape)])
|
||||||
|
)
|
||||||
|
|
||||||
|
// The common (exterior) bounds of the selected shapes
|
||||||
|
const bounds = getCommonBounds(...Object.values(shapesBounds))
|
||||||
|
|
||||||
|
const center = [
|
||||||
|
bounds.minX + bounds.width / 2,
|
||||||
|
bounds.minY + bounds.height / 2,
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPageId,
|
||||||
|
center,
|
||||||
|
shapes: shapes.map(({ id, rotation }) => ({
|
||||||
|
id,
|
||||||
|
rotation,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RotateSnapshot = ReturnType<typeof getRotateSnapshot>
|
|
@ -171,7 +171,8 @@ export default class TransformSession extends BaseSession {
|
||||||
const { shapes } = data.document.pages[currentPageId]
|
const { shapes } = data.document.pages[currentPageId]
|
||||||
|
|
||||||
selectedIds.forEach((id) => {
|
selectedIds.forEach((id) => {
|
||||||
const shape = shapes.shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
||||||
const { initialShape, initialShapeBounds } = shapeBounds[id]
|
const { initialShape, initialShapeBounds } = shapeBounds[id]
|
||||||
|
|
||||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||||
|
|
536
state/state.ts
536
state/state.ts
|
@ -6,7 +6,6 @@ import {
|
||||||
PointerInfo,
|
PointerInfo,
|
||||||
Shape,
|
Shape,
|
||||||
ShapeType,
|
ShapeType,
|
||||||
Shapes,
|
|
||||||
TransformCorner,
|
TransformCorner,
|
||||||
TransformEdge,
|
TransformEdge,
|
||||||
CodeControl,
|
CodeControl,
|
||||||
|
@ -16,14 +15,14 @@ import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
|
||||||
import history from "state/history"
|
import history from "state/history"
|
||||||
import * as Sessions from "./sessions"
|
import * as Sessions from "./sessions"
|
||||||
import commands from "./commands"
|
import commands from "./commands"
|
||||||
import { controls } from "lib/code/control"
|
import { updateFromCode } from "lib/code/generate"
|
||||||
import { generateFromCode, updateFromCode } from "lib/code/generate"
|
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
settings: {
|
settings: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
darkMode: false,
|
isDarkMode: false,
|
||||||
|
isCodeOpen: false,
|
||||||
},
|
},
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
|
@ -56,6 +55,7 @@ const state = createState({
|
||||||
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
|
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
|
||||||
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
|
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
|
||||||
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
|
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
|
||||||
|
TOGGLED_CODE_PANEL_OPEN: "toggleCodePanel",
|
||||||
RESET_CAMERA: "resetCamera",
|
RESET_CAMERA: "resetCamera",
|
||||||
},
|
},
|
||||||
initial: "loading",
|
initial: "loading",
|
||||||
|
@ -64,236 +64,323 @@ const state = createState({
|
||||||
on: {
|
on: {
|
||||||
MOUNTED: {
|
MOUNTED: {
|
||||||
do: "restoreSavedData",
|
do: "restoreSavedData",
|
||||||
to: "selecting",
|
to: "ready",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selecting: {
|
ready: {
|
||||||
on: {
|
on: {
|
||||||
UNDO: { do: "undo" },
|
UNMOUNTED: [
|
||||||
REDO: { do: "redo" },
|
{ unless: "isReadOnly", do: "forceSave" },
|
||||||
CANCELLED: { do: "clearSelectedIds" },
|
{ to: "loading" },
|
||||||
DELETED: { do: "deleteSelectedIds" },
|
],
|
||||||
SAVED_CODE: "saveCode",
|
|
||||||
GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
|
|
||||||
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
|
||||||
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
|
||||||
CHANGED_CODE_CONTROL: "updateControls",
|
|
||||||
},
|
},
|
||||||
initial: "notPointing",
|
initial: "selecting",
|
||||||
states: {
|
states: {
|
||||||
notPointing: {
|
selecting: {
|
||||||
on: {
|
on: {
|
||||||
POINTED_CANVAS: { to: "brushSelecting" },
|
SAVED: "forceSave",
|
||||||
POINTED_BOUNDS: { to: "pointingBounds" },
|
UNDO: { do: "undo" },
|
||||||
POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
|
REDO: { do: "redo" },
|
||||||
POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
|
CANCELLED: { do: "clearSelectedIds" },
|
||||||
MOVED_OVER_SHAPE: {
|
DELETED: { do: "deleteSelectedIds" },
|
||||||
if: "pointHitsShape",
|
SAVED_CODE: "saveCode",
|
||||||
then: {
|
GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
|
||||||
unless: "shapeIsHovered",
|
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
||||||
do: "setHoveredId",
|
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
||||||
},
|
CHANGED_CODE_CONTROL: "updateControls",
|
||||||
else: { if: "shapeIsHovered", do: "clearHoveredId" },
|
},
|
||||||
},
|
initial: "notPointing",
|
||||||
UNHOVERED_SHAPE: "clearHoveredId",
|
states: {
|
||||||
POINTED_SHAPE: [
|
notPointing: {
|
||||||
"setPointedId",
|
on: {
|
||||||
{
|
SELECTED_ALL: "selectAll",
|
||||||
if: "isPressingShiftKey",
|
POINTED_CANVAS: { to: "brushSelecting" },
|
||||||
then: {
|
POINTED_BOUNDS: { to: "pointingBounds" },
|
||||||
if: "isPointedShapeSelected",
|
POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
|
||||||
do: "pullPointedIdFromSelectedIds",
|
POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
|
||||||
else: {
|
POINTED_ROTATE_HANDLE: { to: "rotatingSelection" },
|
||||||
do: "pushPointedIdToSelectedIds",
|
MOVED_OVER_SHAPE: {
|
||||||
to: "pointingBounds",
|
if: "pointHitsShape",
|
||||||
|
then: {
|
||||||
|
unless: "shapeIsHovered",
|
||||||
|
do: "setHoveredId",
|
||||||
},
|
},
|
||||||
|
else: { if: "shapeIsHovered", do: "clearHoveredId" },
|
||||||
},
|
},
|
||||||
else: [
|
UNHOVERED_SHAPE: "clearHoveredId",
|
||||||
|
POINTED_SHAPE: [
|
||||||
|
"setPointedId",
|
||||||
{
|
{
|
||||||
unless: "isPointedShapeSelected",
|
if: "isPressingShiftKey",
|
||||||
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
then: {
|
||||||
},
|
if: "isPointedShapeSelected",
|
||||||
{
|
do: "pullPointedIdFromSelectedIds",
|
||||||
to: "pointingBounds",
|
else: {
|
||||||
|
do: "pushPointedIdToSelectedIds",
|
||||||
|
to: "pointingBounds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
else: [
|
||||||
|
{
|
||||||
|
unless: "isPointedShapeSelected",
|
||||||
|
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: "pointingBounds",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pointingBounds: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: [
|
|
||||||
{
|
|
||||||
unless: ["isPointingBounds", "isPressingShiftKey"],
|
|
||||||
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
|
||||||
},
|
|
||||||
{ to: "notPointing" },
|
|
||||||
],
|
|
||||||
MOVED_POINTER: {
|
|
||||||
unless: "isReadOnly",
|
|
||||||
if: "distanceImpliesDrag",
|
|
||||||
to: "draggingSelection",
|
|
||||||
},
|
},
|
||||||
},
|
pointingBounds: {
|
||||||
},
|
|
||||||
transformingSelection: {
|
|
||||||
onEnter: "startTransformSession",
|
|
||||||
on: {
|
|
||||||
MOVED_POINTER: "updateTransformSession",
|
|
||||||
PANNED_CAMERA: "updateTransformSession",
|
|
||||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
|
||||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
draggingSelection: {
|
|
||||||
onEnter: "startTranslateSession",
|
|
||||||
on: {
|
|
||||||
MOVED_POINTER: "updateTranslateSession",
|
|
||||||
PANNED_CAMERA: "updateTranslateSession",
|
|
||||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
|
||||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
brushSelecting: {
|
|
||||||
onEnter: [
|
|
||||||
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
|
|
||||||
"startBrushSession",
|
|
||||||
],
|
|
||||||
on: {
|
|
||||||
MOVED_POINTER: "updateBrushSession",
|
|
||||||
PANNED_CAMERA: "updateBrushSession",
|
|
||||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
|
||||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dot: {
|
|
||||||
initial: "creating",
|
|
||||||
states: {
|
|
||||||
creating: {
|
|
||||||
on: {
|
|
||||||
POINTED_CANVAS: {
|
|
||||||
do: "createDot",
|
|
||||||
to: "dot.editing",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
|
||||||
CANCELLED: {
|
|
||||||
do: ["cancelSession", "deleteSelectedIds"],
|
|
||||||
to: "selecting",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initial: "inactive",
|
|
||||||
states: {
|
|
||||||
inactive: {
|
|
||||||
on: {
|
on: {
|
||||||
|
STOPPED_POINTING: [
|
||||||
|
{
|
||||||
|
unless: ["isPointingBounds", "isPressingShiftKey"],
|
||||||
|
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
||||||
|
},
|
||||||
|
{ to: "notPointing" },
|
||||||
|
],
|
||||||
MOVED_POINTER: {
|
MOVED_POINTER: {
|
||||||
|
unless: "isReadOnly",
|
||||||
if: "distanceImpliesDrag",
|
if: "distanceImpliesDrag",
|
||||||
to: "dot.editing.active",
|
to: "draggingSelection",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
active: {
|
rotatingSelection: {
|
||||||
|
onEnter: "startRotateSession",
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: "updateRotateSession",
|
||||||
|
PANNED_CAMERA: "updateRotateSession",
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transformingSelection: {
|
||||||
|
onEnter: "startTransformSession",
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: "updateTransformSession",
|
||||||
|
PANNED_CAMERA: "updateTransformSession",
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
draggingSelection: {
|
||||||
onEnter: "startTranslateSession",
|
onEnter: "startTranslateSession",
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: "updateTranslateSession",
|
MOVED_POINTER: "updateTranslateSession",
|
||||||
PANNED_CAMERA: "updateTranslateSession",
|
PANNED_CAMERA: "updateTranslateSession",
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
brushSelecting: {
|
||||||
|
onEnter: [
|
||||||
|
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
|
||||||
|
"startBrushSession",
|
||||||
|
],
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: "updateBrushSession",
|
||||||
|
PANNED_CAMERA: "updateBrushSession",
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
dot: {
|
||||||
},
|
initial: "creating",
|
||||||
circle: {},
|
|
||||||
ellipse: {},
|
|
||||||
ray: {
|
|
||||||
initial: "creating",
|
|
||||||
states: {
|
|
||||||
creating: {
|
|
||||||
on: {
|
|
||||||
POINTED_CANVAS: {
|
|
||||||
do: "createRay",
|
|
||||||
to: "ray.editing",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
|
||||||
CANCELLED: {
|
|
||||||
do: ["cancelSession", "deleteSelectedIds"],
|
|
||||||
to: "selecting",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initial: "inactive",
|
|
||||||
states: {
|
states: {
|
||||||
inactive: {
|
creating: {
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: {
|
POINTED_CANVAS: {
|
||||||
if: "distanceImpliesDrag",
|
do: "createDot",
|
||||||
to: "ray.editing.active",
|
to: "dot.editing",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
active: {
|
editing: {
|
||||||
onEnter: "startDirectionSession",
|
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: "updateDirectionSession",
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
PANNED_CAMERA: "updateDirectionSession",
|
CANCELLED: {
|
||||||
|
do: ["cancelSession", "deleteSelectedIds"],
|
||||||
|
to: "selecting",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
initial: "inactive",
|
||||||
},
|
states: {
|
||||||
},
|
inactive: {
|
||||||
},
|
on: {
|
||||||
},
|
MOVED_POINTER: {
|
||||||
line: {
|
if: "distanceImpliesDrag",
|
||||||
initial: "creating",
|
to: "dot.editing.active",
|
||||||
states: {
|
},
|
||||||
creating: {
|
},
|
||||||
on: {
|
},
|
||||||
POINTED_CANVAS: {
|
active: {
|
||||||
do: "createLine",
|
onEnter: "startTranslateSession",
|
||||||
to: "line.editing",
|
on: {
|
||||||
},
|
MOVED_POINTER: "updateTranslateSession",
|
||||||
},
|
PANNED_CAMERA: "updateTranslateSession",
|
||||||
},
|
},
|
||||||
editing: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
|
||||||
CANCELLED: {
|
|
||||||
do: ["cancelSession", "deleteSelectedIds"],
|
|
||||||
to: "selecting",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initial: "inactive",
|
|
||||||
states: {
|
|
||||||
inactive: {
|
|
||||||
on: {
|
|
||||||
MOVED_POINTER: {
|
|
||||||
if: "distanceImpliesDrag",
|
|
||||||
to: "line.editing.active",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
active: {
|
},
|
||||||
onEnter: "startDirectionSession",
|
},
|
||||||
|
circle: {
|
||||||
|
initial: "creating",
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: "updateDirectionSession",
|
POINTED_CANVAS: {
|
||||||
PANNED_CAMERA: "updateDirectionSession",
|
to: "circle.editing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { to: "selecting" },
|
||||||
|
CANCELLED: { to: "selecting" },
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: "distanceImpliesDrag",
|
||||||
|
do: "createCircle",
|
||||||
|
to: "drawingShape.bounds",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ellipse: {
|
||||||
|
initial: "creating",
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
to: "ellipse.editing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { to: "selecting" },
|
||||||
|
CANCELLED: { to: "selecting" },
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: "distanceImpliesDrag",
|
||||||
|
do: "createEllipse",
|
||||||
|
to: "drawingShape.bounds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rectangle: {
|
||||||
|
initial: "creating",
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
to: "rectangle.editing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { to: "selecting" },
|
||||||
|
CANCELLED: { to: "selecting" },
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: "distanceImpliesDrag",
|
||||||
|
do: "createRectangle",
|
||||||
|
to: "drawingShape.bounds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ray: {
|
||||||
|
initial: "creating",
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
do: "createRay",
|
||||||
|
to: "ray.editing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: {
|
||||||
|
do: ["cancelSession", "deleteSelectedIds"],
|
||||||
|
to: "selecting",
|
||||||
|
},
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: "distanceImpliesDrag",
|
||||||
|
to: "drawingShape.direction",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
initial: "creating",
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
do: "createLine",
|
||||||
|
to: "line.editing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: {
|
||||||
|
do: ["cancelSession", "deleteSelectedIds"],
|
||||||
|
to: "selecting",
|
||||||
|
},
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: "distanceImpliesDrag",
|
||||||
|
to: "drawingShape.direction",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
polyline: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drawingShape: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: {
|
||||||
|
do: ["cancelSession", "deleteSelectedIds"],
|
||||||
|
to: "selecting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initial: "drawingShapeBounds",
|
||||||
|
states: {
|
||||||
|
bounds: {
|
||||||
|
onEnter: "startDrawTransformSession",
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: "updateTransformSession",
|
||||||
|
PANNED_CAMERA: "updateTransformSession",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
direction: {
|
||||||
|
onEnter: "startDirectionSession",
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: "updateDirectionSession",
|
||||||
|
PANNED_CAMERA: "updateDirectionSession",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
polyline: {},
|
|
||||||
rectangle: {},
|
|
||||||
},
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
isPointingBounds(data, payload: PointerInfo) {
|
isPointingBounds(data, payload: PointerInfo) {
|
||||||
|
@ -358,6 +445,36 @@ const state = createState({
|
||||||
data.selectedIds.add(shape.id)
|
data.selectedIds.add(shape.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createCircle(data, payload: PointerInfo) {
|
||||||
|
const shape = shapeUtilityMap[ShapeType.Circle].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
radius: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
commands.createShape(data, shape)
|
||||||
|
data.selectedIds.add(shape.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
createEllipse(data, payload: PointerInfo) {
|
||||||
|
const shape = shapeUtilityMap[ShapeType.Ellipse].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
radiusX: 1,
|
||||||
|
radiusY: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
commands.createShape(data, shape)
|
||||||
|
data.selectedIds.add(shape.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
createRectangle(data, payload: PointerInfo) {
|
||||||
|
const shape = shapeUtilityMap[ShapeType.Rectangle].create({
|
||||||
|
point: screenToWorld(payload.point, data),
|
||||||
|
size: [1, 1],
|
||||||
|
})
|
||||||
|
|
||||||
|
commands.createShape(data, shape)
|
||||||
|
data.selectedIds.add(shape.id)
|
||||||
|
},
|
||||||
/* -------------------- Sessions -------------------- */
|
/* -------------------- Sessions -------------------- */
|
||||||
|
|
||||||
// Shared
|
// Shared
|
||||||
|
@ -381,6 +498,17 @@ const state = createState({
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
session.update(data, screenToWorld(payload.point, data))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Rotating
|
||||||
|
startRotateSession(data, payload: PointerInfo) {
|
||||||
|
session = new Sessions.RotateSession(
|
||||||
|
data,
|
||||||
|
screenToWorld(payload.point, data)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
updateRotateSession(data, payload: PointerInfo) {
|
||||||
|
session.update(data, screenToWorld(payload.point, data))
|
||||||
|
},
|
||||||
|
|
||||||
// Dragging / Translating
|
// Dragging / Translating
|
||||||
startTranslateSession(data, payload: PointerInfo) {
|
startTranslateSession(data, payload: PointerInfo) {
|
||||||
session = new Sessions.TranslateSession(
|
session = new Sessions.TranslateSession(
|
||||||
|
@ -403,6 +531,13 @@ const state = createState({
|
||||||
screenToWorld(payload.point, data)
|
screenToWorld(payload.point, data)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
startDrawTransformSession(data, payload: PointerInfo) {
|
||||||
|
session = new Sessions.TransformSession(
|
||||||
|
data,
|
||||||
|
TransformCorner.BottomRight,
|
||||||
|
screenToWorld(payload.point, data)
|
||||||
|
)
|
||||||
|
},
|
||||||
updateTransformSession(data, payload: PointerInfo) {
|
updateTransformSession(data, payload: PointerInfo) {
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
session.update(data, screenToWorld(payload.point, data))
|
||||||
},
|
},
|
||||||
|
@ -420,6 +555,13 @@ const state = createState({
|
||||||
|
|
||||||
/* -------------------- Selection ------------------- */
|
/* -------------------- Selection ------------------- */
|
||||||
|
|
||||||
|
selectAll(data) {
|
||||||
|
const { selectedIds, document, currentPageId } = data
|
||||||
|
selectedIds.clear()
|
||||||
|
for (let id in document.pages[currentPageId].shapes) {
|
||||||
|
selectedIds.add(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
setHoveredId(data, payload: PointerInfo) {
|
setHoveredId(data, payload: PointerInfo) {
|
||||||
data.hoveredId = payload.target
|
data.hoveredId = payload.target
|
||||||
},
|
},
|
||||||
|
@ -488,9 +630,12 @@ const state = createState({
|
||||||
data.selectedIds.clear()
|
data.selectedIds.clear()
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ---------------------- Misc ---------------------- */
|
/* ---------------------- History ---------------------- */
|
||||||
|
|
||||||
// History
|
// History
|
||||||
|
forceSave(data) {
|
||||||
|
history.save(data)
|
||||||
|
},
|
||||||
enableHistory() {
|
enableHistory() {
|
||||||
history.enable()
|
history.enable()
|
||||||
},
|
},
|
||||||
|
@ -504,7 +649,16 @@ const state = createState({
|
||||||
history.redo(data)
|
history.redo(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Code
|
/* ---------------------- Code ---------------------- */
|
||||||
|
closeCodePanel(data) {
|
||||||
|
data.settings.isCodeOpen = false
|
||||||
|
},
|
||||||
|
openCodePanel(data) {
|
||||||
|
data.settings.isCodeOpen = true
|
||||||
|
},
|
||||||
|
toggleCodePanel(data) {
|
||||||
|
data.settings.isCodeOpen = !data.settings.isCodeOpen
|
||||||
|
},
|
||||||
setGeneratedShapes(
|
setGeneratedShapes(
|
||||||
data,
|
data,
|
||||||
payload: { shapes: Shape[]; controls: CodeControl[] }
|
payload: { shapes: Shape[]; controls: CodeControl[] }
|
||||||
|
|
3
types.ts
3
types.ts
|
@ -10,7 +10,8 @@ export interface Data {
|
||||||
isReadOnly: boolean
|
isReadOnly: boolean
|
||||||
settings: {
|
settings: {
|
||||||
fontSize: number
|
fontSize: number
|
||||||
darkMode: boolean
|
isDarkMode: boolean
|
||||||
|
isCodeOpen: boolean
|
||||||
}
|
}
|
||||||
camera: {
|
camera: {
|
||||||
point: number[]
|
point: number[]
|
||||||
|
|
Loading…
Reference in a new issue